diff --git a/package.json b/package.json index f79bf06f..d0ad1a5c 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "floating-vue": "2.0.0-beta.20", "fuse.js": "^6.6.2", "lodash.clonedeep": "^4.5.0", + "lodash.debounce": "^4.0.8", "lodash.isequal": "^4.5.0", "lodash.merge": "^4.6.2", "lodash.sortby": "^4.7.0", @@ -78,6 +79,7 @@ "@tailwindcss/aspect-ratio": "^0.4.2", "@types/jsdom": "^20.0.1", "@types/lodash.clonedeep": "4.5.7", + "@types/lodash.debounce": "^4.0.7", "@types/lodash.isequal": "^4.5.6", "@types/lodash.merge": "^4.6.7", "@types/node": "^18.11.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71ea42bf..ee5bdc73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,7 @@ importers: '@tiptap/extension-character-count': ^2.0.0-beta.202 '@types/jsdom': ^20.0.1 '@types/lodash.clonedeep': 4.5.7 + '@types/lodash.debounce': ^4.0.7 '@types/lodash.isequal': ^4.5.6 '@types/lodash.merge': ^4.6.7 '@types/node': ^18.11.9 @@ -64,6 +65,7 @@ importers: husky: ^8.0.2 jsdom: ^20.0.3 lodash.clonedeep: ^4.5.0 + lodash.debounce: ^4.0.8 lodash.isequal: ^4.5.0 lodash.merge: ^4.6.2 lodash.sortby: ^4.7.0 @@ -129,6 +131,7 @@ importers: floating-vue: 2.0.0-beta.20_vue@3.2.45 fuse.js: 6.6.2 lodash.clonedeep: 4.5.0 + lodash.debounce: 4.0.8 lodash.isequal: 4.5.0 lodash.merge: 4.6.2 lodash.sortby: 4.7.0 @@ -149,6 +152,7 @@ importers: '@tailwindcss/aspect-ratio': 0.4.2_tailwindcss@3.2.4 '@types/jsdom': 20.0.1 '@types/lodash.clonedeep': 4.5.7 + '@types/lodash.debounce': 4.0.7 '@types/lodash.isequal': 4.5.6 '@types/lodash.merge': 4.6.7 '@types/node': 18.11.9 @@ -176,7 +180,7 @@ importers: randomstring: 1.2.3 sass: 1.56.1 start-server-and-test: 1.14.0 - tailwindcss: 3.2.4_postcss@8.4.19 + tailwindcss: 3.2.4 tailwindcss-safe-area: 0.2.2 tailwindcss-themer: 2.0.2_tailwindcss@3.2.4 typescript: 4.7.4 @@ -1927,7 +1931,7 @@ packages: optional: true dependencies: '@formkit/core': 1.0.0-beta.12-e579559 - tailwindcss: 3.2.4_postcss@8.4.19 + tailwindcss: 3.2.4 dev: false /@formkit/utils/1.0.0-beta.12-e579559: @@ -2671,7 +2675,7 @@ packages: peerDependencies: tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1' dependencies: - tailwindcss: 3.2.4_postcss@8.4.19 + tailwindcss: 3.2.4 dev: true /@tiptap/core/2.0.0-beta.202: @@ -3131,6 +3135,12 @@ packages: '@types/lodash': 4.14.186 dev: true + /@types/lodash.debounce/4.0.7: + resolution: {integrity: sha512-X1T4wMZ+gT000M2/91SYj0d/7JfeNZ9PeeOldSNoE/lunLeQXKvkmIumI29IaKMotU/ln/McOIvgzZcQ/3TrSA==} + dependencies: + '@types/lodash': 4.14.186 + dev: true + /@types/lodash.isequal/4.5.6: resolution: {integrity: sha512-Ww4UGSe3DmtvLLJm2F16hDwEQSv7U0Rr8SujLUA2wHI2D2dm8kPu6Et+/y303LfjTIwSBKXB/YTUcAKpem/XEg==} dependencies: @@ -8522,15 +8532,13 @@ packages: just-unique: 4.1.1 lodash.merge: 4.6.2 lodash.mergewith: 4.6.2 - tailwindcss: 3.2.4_postcss@8.4.19 + tailwindcss: 3.2.4 dev: true - /tailwindcss/3.2.4_postcss@8.4.19: + /tailwindcss/3.2.4: resolution: {integrity: sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==} engines: {node: '>=12.13.0'} hasBin: true - peerDependencies: - postcss: ^8.0.9 dependencies: arg: 5.0.2 chokidar: 3.5.3 diff --git a/src/composables/use-content-cache.ts b/src/composables/use-content-cache.ts new file mode 100644 index 00000000..b2e57eae --- /dev/null +++ b/src/composables/use-content-cache.ts @@ -0,0 +1,91 @@ +import { useLocalStorage } from "@vueuse/core"; +import { Toast } from "@halo-dev/components"; +import type { Ref } from "vue"; + +interface ContentCache { + name: string; + content?: string; +} +import debounce from "lodash.debounce"; + +interface useContentCacheReturn { + handleResetCache: () => void; + handleSetContentCache: () => void; + handleClearCache: (name: string) => void; +} + +export function useContentCache( + key: string, + name: string, + raw: Ref +): useContentCacheReturn { + const content_caches = useLocalStorage(key, []); + + const handleResetCache = () => { + if (name) { + const cache = content_caches.value.find( + (c: ContentCache) => c.name === name + ); + if (cache) { + Toast.info("已从缓存中恢复未保存的内容"); + raw.value = cache.content; + } + } else { + const cache = content_caches.value.find( + (c: ContentCache) => c.name === "" && c.content + ); + if (cache) { + Toast.info("已从缓存中恢复未保存的内容"); + raw.value = cache.content; + } + } + }; + + const handleSetContentCache = debounce(() => { + if (name) { + const cache = content_caches.value.find( + (c: ContentCache) => c.name === name + ); + if (cache) { + cache.content = raw?.value; + } else { + content_caches.value.push({ + name: name, + content: raw?.value, + }); + } + } else { + const cache = content_caches.value.find( + (c: ContentCache) => c.name === "" + ); + if (cache) { + cache.content = raw?.value; + } else { + content_caches.value.push({ + name: "", + content: raw?.value, + }); + } + } + }, 500); + + const handleClearCache = (name: string) => { + if (name) { + const index = content_caches.value.findIndex( + (c: ContentCache) => c.name === name + ); + index > -1 && content_caches.value.splice(index, 1); + } else { + const index = content_caches.value.findIndex( + (c: ContentCache) => c.name === "" + ); + index > -1 && content_caches.value.splice(index, 1); + } + }; + + return { + handleClearCache, + handleResetCache, + handleSetContentCache, + }; +} diff --git a/src/modules/contents/pages/SinglePageEditor.vue b/src/modules/contents/pages/SinglePageEditor.vue index 868df535..571335ec 100644 --- a/src/modules/contents/pages/SinglePageEditor.vue +++ b/src/modules/contents/pages/SinglePageEditor.vue @@ -13,12 +13,13 @@ import DefaultEditor from "@/components/editor/DefaultEditor.vue"; import SinglePageSettingModal from "./components/SinglePageSettingModal.vue"; import PostPreviewModal from "../posts/components/PostPreviewModal.vue"; import type { SinglePage, SinglePageRequest } from "@halo-dev/api-client"; -import { computed, onMounted, ref } from "vue"; +import { computed, onMounted, ref, toRef } from "vue"; import { apiClient } from "@/utils/api-client"; import { useRouteQuery } from "@vueuse/router"; import cloneDeep from "lodash.clonedeep"; import { useRouter } from "vue-router"; import { randomUUID } from "@/utils/id"; +import { useContentCache } from "@/composables/use-content-cache"; const router = useRouter(); @@ -99,6 +100,7 @@ const handleSave = async () => { Toast.success("保存成功"); + handleClearCache(routeQueryName.value as string); await handleFetchContent(); } catch (error) { console.error("Failed to save single page", error); @@ -144,6 +146,7 @@ const handlePublish = async () => { } Toast.success("发布成功"); + handleClearCache(routeQueryName.value as string); } catch (error) { console.error("Failed to publish single page", error); Toast.error("发布失败,请重试"); @@ -168,7 +171,7 @@ const handleFetchContent = async () => { snapshotName: formState.value.page.spec.headSnapshot, }); - formState.value.content = data; + formState.value.content = Object.assign(formState.value.content, data); }; const handleOpenSettingModal = async () => { @@ -211,7 +214,15 @@ onMounted(async () => { // fetch single page content await handleFetchContent(); } + handleResetCache(); }); + +const { handleSetContentCache, handleResetCache, handleClearCache } = + useContentCache( + "singlePage-content-cache", + routeQueryName.value as string, + toRef(formState.value.content, "raw") + ); diff --git a/src/modules/contents/posts/PostEditor.vue b/src/modules/contents/posts/PostEditor.vue index 5ed9dbca..b73c48e5 100644 --- a/src/modules/contents/posts/PostEditor.vue +++ b/src/modules/contents/posts/PostEditor.vue @@ -13,12 +13,13 @@ import DefaultEditor from "@/components/editor/DefaultEditor.vue"; import PostSettingModal from "./components/PostSettingModal.vue"; import PostPreviewModal from "./components/PostPreviewModal.vue"; import type { Post, PostRequest } from "@halo-dev/api-client"; -import { computed, onMounted, ref } from "vue"; +import { computed, onMounted, ref, toRef } from "vue"; import cloneDeep from "lodash.clonedeep"; import { apiClient } from "@/utils/api-client"; import { useRouteQuery } from "@vueuse/router"; import { useRouter } from "vue-router"; import { randomUUID } from "@/utils/id"; +import { useContentCache } from "@/composables/use-content-cache"; const router = useRouter(); @@ -99,7 +100,7 @@ const handleSave = async () => { } Toast.success("保存成功"); - + handleClearCache(name.value as string); await handleFetchContent(); } catch (e) { console.error("Failed to save post", e); @@ -149,6 +150,7 @@ const handlePublish = async () => { } Toast.success("发布成功", { duration: 2000 }); + handleClearCache(name.value as string); } catch (error) { console.error("Failed to publish post", error); Toast.error("发布失败,请重试"); @@ -174,7 +176,7 @@ const handleFetchContent = async () => { snapshotName: formState.value.post.spec.headSnapshot, }); - formState.value.content = data; + formState.value.content = Object.assign(formState.value.content, data); }; const handleOpenSettingModal = async () => { @@ -220,7 +222,15 @@ onMounted(async () => { // fetch post content await handleFetchContent(); } + handleResetCache(); }); + +const { handleSetContentCache, handleResetCache, handleClearCache } = + useContentCache( + "post-content-cache", + name.value as string, + toRef(formState.value.content, "raw") + );