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",
|
"floating-vue": "2.0.0-beta.20",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"lodash.merge": "^4.6.2",
|
"lodash.merge": "^4.6.2",
|
||||||
"lodash.sortby": "^4.7.0",
|
"lodash.sortby": "^4.7.0",
|
||||||
|
@ -78,6 +79,7 @@
|
||||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||||
"@types/jsdom": "^20.0.1",
|
"@types/jsdom": "^20.0.1",
|
||||||
"@types/lodash.clonedeep": "4.5.7",
|
"@types/lodash.clonedeep": "4.5.7",
|
||||||
|
"@types/lodash.debounce": "^4.0.7",
|
||||||
"@types/lodash.isequal": "^4.5.6",
|
"@types/lodash.isequal": "^4.5.6",
|
||||||
"@types/lodash.merge": "^4.6.7",
|
"@types/lodash.merge": "^4.6.7",
|
||||||
"@types/node": "^18.11.9",
|
"@types/node": "^18.11.9",
|
||||||
|
|
|
@ -23,6 +23,7 @@ importers:
|
||||||
'@tiptap/extension-character-count': ^2.0.0-beta.202
|
'@tiptap/extension-character-count': ^2.0.0-beta.202
|
||||||
'@types/jsdom': ^20.0.1
|
'@types/jsdom': ^20.0.1
|
||||||
'@types/lodash.clonedeep': 4.5.7
|
'@types/lodash.clonedeep': 4.5.7
|
||||||
|
'@types/lodash.debounce': ^4.0.7
|
||||||
'@types/lodash.isequal': ^4.5.6
|
'@types/lodash.isequal': ^4.5.6
|
||||||
'@types/lodash.merge': ^4.6.7
|
'@types/lodash.merge': ^4.6.7
|
||||||
'@types/node': ^18.11.9
|
'@types/node': ^18.11.9
|
||||||
|
@ -64,6 +65,7 @@ importers:
|
||||||
husky: ^8.0.2
|
husky: ^8.0.2
|
||||||
jsdom: ^20.0.3
|
jsdom: ^20.0.3
|
||||||
lodash.clonedeep: ^4.5.0
|
lodash.clonedeep: ^4.5.0
|
||||||
|
lodash.debounce: ^4.0.8
|
||||||
lodash.isequal: ^4.5.0
|
lodash.isequal: ^4.5.0
|
||||||
lodash.merge: ^4.6.2
|
lodash.merge: ^4.6.2
|
||||||
lodash.sortby: ^4.7.0
|
lodash.sortby: ^4.7.0
|
||||||
|
@ -129,6 +131,7 @@ importers:
|
||||||
floating-vue: 2.0.0-beta.20_vue@3.2.45
|
floating-vue: 2.0.0-beta.20_vue@3.2.45
|
||||||
fuse.js: 6.6.2
|
fuse.js: 6.6.2
|
||||||
lodash.clonedeep: 4.5.0
|
lodash.clonedeep: 4.5.0
|
||||||
|
lodash.debounce: 4.0.8
|
||||||
lodash.isequal: 4.5.0
|
lodash.isequal: 4.5.0
|
||||||
lodash.merge: 4.6.2
|
lodash.merge: 4.6.2
|
||||||
lodash.sortby: 4.7.0
|
lodash.sortby: 4.7.0
|
||||||
|
@ -149,6 +152,7 @@ importers:
|
||||||
'@tailwindcss/aspect-ratio': 0.4.2_tailwindcss@3.2.4
|
'@tailwindcss/aspect-ratio': 0.4.2_tailwindcss@3.2.4
|
||||||
'@types/jsdom': 20.0.1
|
'@types/jsdom': 20.0.1
|
||||||
'@types/lodash.clonedeep': 4.5.7
|
'@types/lodash.clonedeep': 4.5.7
|
||||||
|
'@types/lodash.debounce': 4.0.7
|
||||||
'@types/lodash.isequal': 4.5.6
|
'@types/lodash.isequal': 4.5.6
|
||||||
'@types/lodash.merge': 4.6.7
|
'@types/lodash.merge': 4.6.7
|
||||||
'@types/node': 18.11.9
|
'@types/node': 18.11.9
|
||||||
|
@ -176,7 +180,7 @@ importers:
|
||||||
randomstring: 1.2.3
|
randomstring: 1.2.3
|
||||||
sass: 1.56.1
|
sass: 1.56.1
|
||||||
start-server-and-test: 1.14.0
|
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-safe-area: 0.2.2
|
||||||
tailwindcss-themer: 2.0.2_tailwindcss@3.2.4
|
tailwindcss-themer: 2.0.2_tailwindcss@3.2.4
|
||||||
typescript: 4.7.4
|
typescript: 4.7.4
|
||||||
|
@ -1927,7 +1931,7 @@ packages:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@formkit/core': 1.0.0-beta.12-e579559
|
'@formkit/core': 1.0.0-beta.12-e579559
|
||||||
tailwindcss: 3.2.4_postcss@8.4.19
|
tailwindcss: 3.2.4
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@formkit/utils/1.0.0-beta.12-e579559:
|
/@formkit/utils/1.0.0-beta.12-e579559:
|
||||||
|
@ -2671,7 +2675,7 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1'
|
tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1'
|
||||||
dependencies:
|
dependencies:
|
||||||
tailwindcss: 3.2.4_postcss@8.4.19
|
tailwindcss: 3.2.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@tiptap/core/2.0.0-beta.202:
|
/@tiptap/core/2.0.0-beta.202:
|
||||||
|
@ -3131,6 +3135,12 @@ packages:
|
||||||
'@types/lodash': 4.14.186
|
'@types/lodash': 4.14.186
|
||||||
dev: true
|
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:
|
/@types/lodash.isequal/4.5.6:
|
||||||
resolution: {integrity: sha512-Ww4UGSe3DmtvLLJm2F16hDwEQSv7U0Rr8SujLUA2wHI2D2dm8kPu6Et+/y303LfjTIwSBKXB/YTUcAKpem/XEg==}
|
resolution: {integrity: sha512-Ww4UGSe3DmtvLLJm2F16hDwEQSv7U0Rr8SujLUA2wHI2D2dm8kPu6Et+/y303LfjTIwSBKXB/YTUcAKpem/XEg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -8522,15 +8532,13 @@ packages:
|
||||||
just-unique: 4.1.1
|
just-unique: 4.1.1
|
||||||
lodash.merge: 4.6.2
|
lodash.merge: 4.6.2
|
||||||
lodash.mergewith: 4.6.2
|
lodash.mergewith: 4.6.2
|
||||||
tailwindcss: 3.2.4_postcss@8.4.19
|
tailwindcss: 3.2.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/tailwindcss/3.2.4_postcss@8.4.19:
|
/tailwindcss/3.2.4:
|
||||||
resolution: {integrity: sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==}
|
resolution: {integrity: sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==}
|
||||||
engines: {node: '>=12.13.0'}
|
engines: {node: '>=12.13.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
|
||||||
postcss: ^8.0.9
|
|
||||||
dependencies:
|
dependencies:
|
||||||
arg: 5.0.2
|
arg: 5.0.2
|
||||||
chokidar: 3.5.3
|
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 SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
|
||||||
import PostPreviewModal from "../posts/components/PostPreviewModal.vue";
|
import PostPreviewModal from "../posts/components/PostPreviewModal.vue";
|
||||||
import type { SinglePage, SinglePageRequest } from "@halo-dev/api-client";
|
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 { apiClient } from "@/utils/api-client";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { randomUUID } from "@/utils/id";
|
import { randomUUID } from "@/utils/id";
|
||||||
|
import { useContentCache } from "@/composables/use-content-cache";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -99,6 +100,7 @@ const handleSave = async () => {
|
||||||
|
|
||||||
Toast.success("保存成功");
|
Toast.success("保存成功");
|
||||||
|
|
||||||
|
handleClearCache(routeQueryName.value as string);
|
||||||
await handleFetchContent();
|
await handleFetchContent();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save single page", error);
|
console.error("Failed to save single page", error);
|
||||||
|
@ -144,6 +146,7 @@ const handlePublish = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.success("发布成功");
|
Toast.success("发布成功");
|
||||||
|
handleClearCache(routeQueryName.value as string);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to publish single page", error);
|
console.error("Failed to publish single page", error);
|
||||||
Toast.error("发布失败,请重试");
|
Toast.error("发布失败,请重试");
|
||||||
|
@ -168,7 +171,7 @@ const handleFetchContent = async () => {
|
||||||
snapshotName: formState.value.page.spec.headSnapshot,
|
snapshotName: formState.value.page.spec.headSnapshot,
|
||||||
});
|
});
|
||||||
|
|
||||||
formState.value.content = data;
|
formState.value.content = Object.assign(formState.value.content, data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenSettingModal = async () => {
|
const handleOpenSettingModal = async () => {
|
||||||
|
@ -211,7 +214,15 @@ onMounted(async () => {
|
||||||
// fetch single page content
|
// fetch single page content
|
||||||
await handleFetchContent();
|
await handleFetchContent();
|
||||||
}
|
}
|
||||||
|
handleResetCache();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { handleSetContentCache, handleResetCache, handleClearCache } =
|
||||||
|
useContentCache(
|
||||||
|
"singlePage-content-cache",
|
||||||
|
routeQueryName.value as string,
|
||||||
|
toRef(formState.value.content, "raw")
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -275,6 +286,7 @@ onMounted(async () => {
|
||||||
:owner="formState.page.spec.owner"
|
:owner="formState.page.spec.owner"
|
||||||
:permalink="formState.page.status?.permalink"
|
:permalink="formState.page.status?.permalink"
|
||||||
:publish-time="formState.page.spec.publishTime"
|
:publish-time="formState.page.spec.publishTime"
|
||||||
|
@update="handleSetContentCache"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -13,12 +13,13 @@ import DefaultEditor from "@/components/editor/DefaultEditor.vue";
|
||||||
import PostSettingModal from "./components/PostSettingModal.vue";
|
import PostSettingModal from "./components/PostSettingModal.vue";
|
||||||
import PostPreviewModal from "./components/PostPreviewModal.vue";
|
import PostPreviewModal from "./components/PostPreviewModal.vue";
|
||||||
import type { Post, PostRequest } from "@halo-dev/api-client";
|
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 cloneDeep from "lodash.clonedeep";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { randomUUID } from "@/utils/id";
|
import { randomUUID } from "@/utils/id";
|
||||||
|
import { useContentCache } from "@/composables/use-content-cache";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -99,7 +100,7 @@ const handleSave = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.success("保存成功");
|
Toast.success("保存成功");
|
||||||
|
handleClearCache(name.value as string);
|
||||||
await handleFetchContent();
|
await handleFetchContent();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to save post", e);
|
console.error("Failed to save post", e);
|
||||||
|
@ -149,6 +150,7 @@ const handlePublish = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.success("发布成功", { duration: 2000 });
|
Toast.success("发布成功", { duration: 2000 });
|
||||||
|
handleClearCache(name.value as string);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to publish post", error);
|
console.error("Failed to publish post", error);
|
||||||
Toast.error("发布失败,请重试");
|
Toast.error("发布失败,请重试");
|
||||||
|
@ -174,7 +176,7 @@ const handleFetchContent = async () => {
|
||||||
snapshotName: formState.value.post.spec.headSnapshot,
|
snapshotName: formState.value.post.spec.headSnapshot,
|
||||||
});
|
});
|
||||||
|
|
||||||
formState.value.content = data;
|
formState.value.content = Object.assign(formState.value.content, data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenSettingModal = async () => {
|
const handleOpenSettingModal = async () => {
|
||||||
|
@ -220,7 +222,15 @@ onMounted(async () => {
|
||||||
// fetch post content
|
// fetch post content
|
||||||
await handleFetchContent();
|
await handleFetchContent();
|
||||||
}
|
}
|
||||||
|
handleResetCache();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { handleSetContentCache, handleResetCache, handleClearCache } =
|
||||||
|
useContentCache(
|
||||||
|
"post-content-cache",
|
||||||
|
name.value as string,
|
||||||
|
toRef(formState.value.content, "raw")
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -284,6 +294,7 @@ onMounted(async () => {
|
||||||
:owner="formState.post.spec.owner"
|
:owner="formState.post.spec.owner"
|
||||||
:permalink="formState.post.status?.permalink"
|
:permalink="formState.post.status?.permalink"
|
||||||
:publish-time="formState.post.spec.publishTime"
|
:publish-time="formState.post.spec.publishTime"
|
||||||
|
@update="handleSetContentCache"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
Loading…
Reference in New Issue