From 293e09fc5d44cc41b54c5c647d4464a430caf6e4 Mon Sep 17 00:00:00 2001 From: laogao Date: Thu, 29 Oct 2026 15:31:57 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E8=A1=A8=E5=8D=95=E8=8D=89=E7=A8=BF=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E8=83=BD=E5=8A=9B=EF=BC=88=E8=AF=AF=E5=85=B3=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 form-draft 工具与 useCrudForm cacheKey 参数,实现「新建/新增」表单误关后本地草稿自动恢复;编辑态表单由详情接口回填、不缓存。 --- frontend/web/src/hooks/core/useCrudForm.ts | 29 ++++++++- frontend/web/src/utils/form-draft.ts | 70 ++++++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 frontend/web/src/utils/form-draft.ts diff --git a/frontend/web/src/hooks/core/useCrudForm.ts b/frontend/web/src/hooks/core/useCrudForm.ts index cb6628fa5..265016216 100644 --- a/frontend/web/src/hooks/core/useCrudForm.ts +++ b/frontend/web/src/hooks/core/useCrudForm.ts @@ -1,4 +1,5 @@ import { onMounted, onUnmounted, ref, type Ref } from "vue"; +import { clearFormDraft, loadFormDraft, saveFormDraft } from "@/utils/form-draft"; import type { CrudDialogState, DialogType } from "./useCrudDialog"; /** @@ -41,6 +42,8 @@ export function useCrudForm(options: { > | null>; /** 表单渲染 key(用于 :key 重新渲染) */ formRenderKey: Ref; + /** 创建表单草稿缓存键(提供后启用「新增表单误关恢复」,编辑表单不缓存) */ + cacheKey?: string; /** 详情 API(传入 id,返回 response) */ detailApi?: (id: number) => Promise<{ data: { data?: T } }>; /** 新增 API */ @@ -64,6 +67,7 @@ export function useCrudForm(options: { dialogVisible, dataFormRef, formRenderKey, + cacheKey, detailApi, createApi, updateApi, @@ -75,6 +79,8 @@ export function useCrudForm(options: { } = options; const submitLoading = ref(false); + // 提交成功后跳过本次「关闭时保存草稿」(防止提交后残留草稿) + let skipDraftOnce = false; /** 重置表单 */ async function resetForm() { @@ -83,8 +89,12 @@ export function useCrudForm(options: { Object.assign(formData.value, initialFormData); } - /** 关闭对话框并重置表单 */ + /** 关闭对话框并重置表单(新增表单关闭时保存草稿) */ async function handleCloseDialog() { + if (cacheKey && dialogVisible.type === "create" && !skipDraftOnce) { + saveFormDraft(cacheKey, formData.value as Record); + } + skipDraftOnce = false; dialogVisible.visible = false; await resetForm(); } @@ -127,6 +137,13 @@ export function useCrudForm(options: { if (extra) { Object.assign(formData.value, extra); } + if (cacheKey) { + // 恢复上次未完成的新增草稿 + const draft = loadFormDraft>(cacheKey); + if (draft) { + Object.assign(formData.value, draft); + } + } formRenderKey.value += 1; dialogVisible.visible = true; } @@ -149,6 +166,8 @@ export function useCrudForm(options: { await resetForm(); } else if (createApi) { await createApi(formData.value); + if (cacheKey) clearFormDraft(cacheKey); + skipDraftOnce = true; // 阻止随后的 handleCloseDialog 再次保存草稿 dialogVisible.visible = false; await resetForm(); await onCreateSuccess?.(); @@ -172,6 +191,7 @@ export function useCrudForm(options: { try { if (createApi) { await createApi(formData.value); + if (cacheKey) clearFormDraft(cacheKey); await onCreateSuccess?.(); } await resetForm(); @@ -183,6 +203,13 @@ export function useCrudForm(options: { } } + // 页面卸载兜底:新增表单未关闭时(如直接切换路由)保存草稿 + onUnmounted(() => { + if (cacheKey && dialogVisible.type === "create" && dialogVisible.visible) { + saveFormDraft(cacheKey, formData.value as Record); + } + }); + return { submitLoading, resetForm, diff --git a/frontend/web/src/utils/form-draft.ts b/frontend/web/src/utils/form-draft.ts new file mode 100644 index 000000000..109c48398 --- /dev/null +++ b/frontend/web/src/utils/form-draft.ts @@ -0,0 +1,70 @@ +/** + * 创建表单草稿缓存(localStorage)。 + * + * 用于「新建/新增/添加」表单:用户填了一半误关窗口,再次打开时恢复已填写内容。 + * 仅缓存创建态表单,编辑态表单由详情接口回填、不缓存。 + * + * 存储键统一加 ``market-form-draft:`` 前缀,与业务数据隔离。 + */ + +const PREFIX = "market-form-draft:"; + +/** 读取创建表单草稿,无草稿或解析失败返回 null */ +export function loadFormDraft(key: string): T | null { + try { + const raw = localStorage.getItem(`${PREFIX}${key}`); + return raw ? (JSON.parse(raw) as T) : null; + } catch { + return null; + } +} + +/** 保存创建表单草稿(剔除 id,避免把编辑态带入新建) */ +export function saveFormDraft(key: string, data: Record): void { + try { + const { id: _id, ...rest } = data; + void _id; + localStorage.setItem(`${PREFIX}${key}`, JSON.stringify(rest)); + } catch { + // 存储不可用/超限时静默忽略,不影响主流程 + } +} + +/** 清除创建表单草稿(提交成功后调用) */ +export function clearFormDraft(key: string): void { + try { + localStorage.removeItem(`${PREFIX}${key}`); + } catch { + // 忽略 + } +} + +/** + * 创建表单草稿帮助器(供手写表单页面使用)。 + * + * 内置「提交成功后跳过本次关闭保存」标志,避免提交后残留草稿: + * - ``restore``:打开新增表单时调用,恢复上次草稿; + * - ``persist``:关闭新增表单时调用(重置前),保存草稿; + * - ``done``:提交成功后调用,清除草稿并标记跳过本次关闭保存。 + */ +export function createFormDraft(key: string) { + let submitted = false; + return { + restore>(form: T): void { + const draft = loadFormDraft>(key); + if (draft) Object.assign(form, draft); + submitted = false; + }, + persist(form: Record): void { + if (submitted) { + submitted = false; + return; + } + saveFormDraft(key, form); + }, + done(): void { + submitted = true; + clearFormDraft(key); + }, + }; +} From cb3ad647d6bc972c5dc40f5051e1e163063dfea0 Mon Sep 17 00:00:00 2001 From: laogao Date: Thu, 29 Oct 2026 16:07:41 +0800 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20=E8=8D=89=E7=A8=BF=E8=83=BD?= =?UTF-8?q?=E5=8A=9B=E6=94=B9=E4=B8=BA=20useFormDraft=20=E7=BB=84=E5=90=88?= =?UTF-8?q?=E5=BC=8F=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除 createFormDraft,新增 useFormDraft(watch 自动保存 + openCreate 恢复/预填),调用方只需提交成功后 done。 --- frontend/web/src/hooks/core/useFormDraft.ts | 58 +++++++++++++++++++++ frontend/web/src/utils/form-draft.ts | 35 ++----------- 2 files changed, 62 insertions(+), 31 deletions(-) create mode 100644 frontend/web/src/hooks/core/useFormDraft.ts diff --git a/frontend/web/src/hooks/core/useFormDraft.ts b/frontend/web/src/hooks/core/useFormDraft.ts new file mode 100644 index 000000000..662153c92 --- /dev/null +++ b/frontend/web/src/hooks/core/useFormDraft.ts @@ -0,0 +1,58 @@ +import { watch, type Ref } from "vue"; +import { clearFormDraft, loadFormDraft, saveFormDraft } from "@/utils/form-draft"; + +/** + * 创建表单草稿组合式函数:自动绑定对话框生命周期,实现「误关恢复」。 + * + * 与 ``useCrudForm`` 的 ``cacheKey`` 定位一致,但面向「自管对话框」的手写表单: + * - ``openCreate``:打开新建表单(可选预设 preset),自动重置 + 恢复草稿; + * - 内部 ``watch`` 监听对话框关闭,自动保存草稿(仅新建态、且非提交成功); + * - ``done``:提交成功后调用,清除草稿并标记跳过本次关闭保存。 + * + * 编辑态(``mode`` 为 update)不参与草稿读写;不传 ``mode`` 视为恒为新建态。 + */ +export interface UseFormDraftOptions { + /** 草稿缓存键 */ + key: string; + /** 对话框可见性(v-model) */ + dialogVisible: Ref; + /** 表单数据 */ + form: Ref; + /** 新建态初始值工厂 */ + blank: () => T; + /** 对话框模式;不传则视为恒为新建态(无编辑态的表单) */ + mode?: Ref<"create" | "update">; +} + +export function useFormDraft(options: UseFormDraftOptions) { + const { key, dialogVisible, form, blank, mode } = options; + // 提交成功后标记跳过本次「关闭时保存草稿」,防止提交后残留草稿 + let submitted = false; + + // 关闭对话框时自动保存草稿(仅新建态,且非提交成功) + watch(dialogVisible, (visible) => { + if (visible) return; + if ((!mode || mode.value === "create") && !submitted) { + saveFormDraft(key, form.value as Record); + } + submitted = false; + }); + + /** 打开新建表单:重置为初始值 → 恢复草稿 → 应用预设(预设优先级最高) */ + function openCreate(preset?: Partial): void { + form.value = blank(); + const draft = loadFormDraft>(key); + if (draft) Object.assign(form.value, draft); + if (preset) Object.assign(form.value, preset); + submitted = false; + dialogVisible.value = true; + } + + /** 提交成功后调用:清除草稿并标记跳过本次关闭保存 */ + function done(): void { + submitted = true; + clearFormDraft(key); + } + + return { openCreate, done }; +} diff --git a/frontend/web/src/utils/form-draft.ts b/frontend/web/src/utils/form-draft.ts index 109c48398..6138f4ac2 100644 --- a/frontend/web/src/utils/form-draft.ts +++ b/frontend/web/src/utils/form-draft.ts @@ -1,9 +1,12 @@ /** - * 创建表单草稿缓存(localStorage)。 + * 创建表单草稿缓存的底层工具(localStorage 读写)。 * * 用于「新建/新增/添加」表单:用户填了一半误关窗口,再次打开时恢复已填写内容。 * 仅缓存创建态表单,编辑态表单由详情接口回填、不缓存。 * + * 本文件只提供底层读写;自动绑定对话框生命周期的组合式函数见 + * ``hooks/core/useFormDraft.ts`` 的 ``useFormDraft``。 + * * 存储键统一加 ``market-form-draft:`` 前缀,与业务数据隔离。 */ @@ -38,33 +41,3 @@ export function clearFormDraft(key: string): void { // 忽略 } } - -/** - * 创建表单草稿帮助器(供手写表单页面使用)。 - * - * 内置「提交成功后跳过本次关闭保存」标志,避免提交后残留草稿: - * - ``restore``:打开新增表单时调用,恢复上次草稿; - * - ``persist``:关闭新增表单时调用(重置前),保存草稿; - * - ``done``:提交成功后调用,清除草稿并标记跳过本次关闭保存。 - */ -export function createFormDraft(key: string) { - let submitted = false; - return { - restore>(form: T): void { - const draft = loadFormDraft>(key); - if (draft) Object.assign(form, draft); - submitted = false; - }, - persist(form: Record): void { - if (submitted) { - submitted = false; - return; - } - saveFormDraft(key, form); - }, - done(): void { - submitted = true; - clearFormDraft(key); - }, - }; -}