mirror of https://github.com/halo-dev/halo-admin
feat: post editing pages support caching of content to the browser (#731)
#### What type of PR is this? /kind feature #### What this PR does / why we need it: 支持将文章内容实时保存到浏览器,防止意外操作丢失内容。 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/2773 #### Does this PR introduce a user-facing change? ```release-note Console 端支持实时保存内容到浏览器,防止意外操作丢失内容。 ```pull/736/head
parent
82988078e5
commit
c20767a30a
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<string | undefined>
|
||||
): useContentCacheReturn {
|
||||
const content_caches = useLocalStorage<ContentCache[]>(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,
|
||||
};
|
||||
}
|
|
@ -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")
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -275,6 +286,7 @@ onMounted(async () => {
|
|||
:owner="formState.page.spec.owner"
|
||||
:permalink="formState.page.status?.permalink"
|
||||
:publish-time="formState.page.spec.publishTime"
|
||||
@update="handleSetContentCache"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -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")
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -284,6 +294,7 @@ onMounted(async () => {
|
|||
:owner="formState.post.spec.owner"
|
||||
:permalink="formState.post.status?.permalink"
|
||||
:publish-time="formState.post.spec.publishTime"
|
||||
@update="handleSetContentCache"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
Loading…
Reference in New Issue