diff --git a/src/components/CrudTable.vue b/src/components/CrudTable.vue index bce9df6..6f491e1 100644 --- a/src/components/CrudTable.vue +++ b/src/components/CrudTable.vue @@ -209,11 +209,15 @@ const draft = useDraftAutosave({ editingRow: editingRow as Record, dialogVisible: () => dialogVisible.value, isEdit: () => isEdit.value, + // 只有"表单里用户可填的列"变化才算草稿内容: + // 隐藏列/父级注入列/自动时间戳(CreateTime)的变化一律忽略,避免生成"看起来是空的"草稿 + editableFields: () => formColumns.value.map((c) => c.field), }) // 新增:先走 useCrudTable 初始化,再注入父级上下文关联字段; // 自动时间戳字段(CreateTime 等)预填当前 UTC 时间——不要求用户输入(后端仍以服务器时间为准); -// 若存在未提交草稿,提示恢复或放弃 +// 若存在未提交草稿(且草稿里有用户真正录入的内容),提示恢复或删除; +// 空草稿(可填列没有有效录入)直接丢弃,不弹窗 async function onHandleCreate(): Promise { // 新增前拦截:父页面可通过 beforeCreate 前置校验(返回 false 阻止打开弹窗) if (props.beforeCreate && props.beforeCreate() === false) return diff --git a/src/composables/useDraftAutosave.ts b/src/composables/useDraftAutosave.ts index 6b37202..7188caf 100644 --- a/src/composables/useDraftAutosave.ts +++ b/src/composables/useDraftAutosave.ts @@ -27,9 +27,15 @@ export function useDraftAutosave(options: { dialogVisible: () => boolean /** 是否编辑态(编辑态不存草稿) */ isEdit: () => boolean + /** + * 用户真正可填的列(表单可见列 field 列表):**只有这些列的有效值才算"录入内容"**。 + * 隐藏列/上下文注入列/自动时间戳的变化(如 CreateTime 每次打开都不同、 + * 多选列由 '' 规范化为 [])一律忽略 —— 否则会生成"用户视角下明明是空的"草稿。 + */ + editableFields: () => string[] }) { const draftKey = `gerp-draft:add:${options.table}` - /** 打开新增弹窗时的表单基线(JSON),内容未改动时不写草稿,避免"空草稿"打扰 */ + /** 打开新增弹窗时的表单基线(JSON),用于判断某个可填列是否真的被改过 */ let draftBaseline: string | null = null let draftTimer: ReturnType | null = null @@ -46,11 +52,38 @@ export function useDraftAutosave(options: { localStorage.removeItem(draftKey) } + /** + * 判断表单数据里是否存在"用户真正录入"的内容(写入与恢复共用同一判定,保证对称) + * + * 判定规则:只看可填列(editableFields),且值非空、与本次打开表单的基线不同。 + * 基线为 null(草稿刚恢复、允许增量保存)时以"非空即可"为准。 + */ + function hasUserInput(data: Record): boolean { + const fields = options.editableFields() + let base: Record = {} + if (draftBaseline) { + try { + base = JSON.parse(draftBaseline) as Record + } catch { + base = {} + } + } + return fields.some((field) => { + const value = data[field] + if (value === null || value === undefined || value === '') return false + if (Array.isArray(value) && value.length === 0) return false + return JSON.stringify(value) !== JSON.stringify(base[field]) + }) + } + function writeDraft(): void { if (!options.dialogVisible() || options.isEdit()) return + if (!hasUserInput(options.editingRow)) { + // 用户已把内容清空 → 一并清掉旧草稿,不在 localStorage 里留"空草稿" + clearDraft() + return + } try { - const cur = JSON.stringify({ ...options.editingRow }) - if (draftBaseline !== null && cur === draftBaseline) return // 未录入任何内容,不存 localStorage.setItem( draftKey, JSON.stringify({ data: { ...options.editingRow }, updatedAt: Date.now() } satisfies FormDraft), @@ -80,36 +113,15 @@ export function useDraftAutosave(options: { draftBaseline = JSON.stringify({ ...options.editingRow }) } - /** - * 草稿是否"为空":没有任何"用户真正录入"的内容 —— 全部是空值, - * 或与本次打开表单的基线完全一致(只有 CreateTime 等系统预填字段,用户没动过) - */ - function isDraftEmpty(draft: FormDraft): boolean { - const data = draft.data || {} - let base: Record = {} - if (draftBaseline) { - try { - base = JSON.parse(draftBaseline) as Record - } catch { - base = {} - } - } - return !Object.entries(data).some(([key, value]) => { - if (value === null || value === undefined || value === '' || (Array.isArray(value) && value.length === 0)) { - return false - } - return JSON.stringify(value) !== JSON.stringify(base[key]) - }) - } - /** * 检测并提示恢复/删除草稿;恢复则写入表单(在新增弹窗初始化后调用)。 - * 空草稿(无有效内容/与基线一致)直接丢弃,不弹窗打扰用户。 + * 空草稿(可填列没有任何有效录入)直接丢弃,不弹窗打扰用户 —— + * 历史遗留的"只有隐藏列/时间戳变化"的草稿也在这里被清掉。 * 弹窗:确定=恢复草稿,取消=删除草稿,右上角关闭=稍后再看(保留草稿) */ async function restoreDraftIfAny(): Promise { const draft = readDraft() - if (!draft || isDraftEmpty(draft)) { + if (!draft || !hasUserInput(draft.data || {})) { clearDraft() // 空草稿:直接丢弃 return }