Browse Source

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
ZXSheng 2 years ago committed by GitHub
parent
commit
c20767a30a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      package.json
  2. 22
      pnpm-lock.yaml
  3. 91
      src/composables/use-content-cache.ts
  4. 16
      src/modules/contents/pages/SinglePageEditor.vue
  5. 17
      src/modules/contents/posts/PostEditor.vue

2
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",

22
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

91
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<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,
};
}

16
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")
);
</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>

17
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")
);
</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…
Cancel
Save