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/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 new file mode 100644 index 000000000..6138f4ac2 --- /dev/null +++ b/frontend/web/src/utils/form-draft.ts @@ -0,0 +1,43 @@ +/** + * 创建表单草稿缓存的底层工具(localStorage 读写)。 + * + * 用于「新建/新增/添加」表单:用户填了一半误关窗口,再次打开时恢复已填写内容。 + * 仅缓存创建态表单,编辑态表单由详情接口回填、不缓存。 + * + * 本文件只提供底层读写;自动绑定对话框生命周期的组合式函数见 + * ``hooks/core/useFormDraft.ts`` 的 ``useFormDraft``。 + * + * 存储键统一加 ``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 { + // 忽略 + } +}