Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion frontend/web/src/hooks/core/useCrudForm.ts
Original file line number Diff line number Diff line change
@@ -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";

/**
Expand Down Expand Up @@ -41,6 +42,8 @@ export function useCrudForm<T extends object>(options: {
> | null>;
/** 表单渲染 key(用于 :key 重新渲染) */
formRenderKey: Ref<number>;
/** 创建表单草稿缓存键(提供后启用「新增表单误关恢复」,编辑表单不缓存) */
cacheKey?: string;
/** 详情 API(传入 id,返回 response) */
detailApi?: (id: number) => Promise<{ data: { data?: T } }>;
/** 新增 API */
Expand All @@ -64,6 +67,7 @@ export function useCrudForm<T extends object>(options: {
dialogVisible,
dataFormRef,
formRenderKey,
cacheKey,
detailApi,
createApi,
updateApi,
Expand All @@ -75,6 +79,8 @@ export function useCrudForm<T extends object>(options: {
} = options;

const submitLoading = ref(false);
// 提交成功后跳过本次「关闭时保存草稿」(防止提交后残留草稿)
let skipDraftOnce = false;

/** 重置表单 */
async function resetForm() {
Expand All @@ -83,8 +89,12 @@ export function useCrudForm<T extends object>(options: {
Object.assign(formData.value, initialFormData);
}

/** 关闭对话框并重置表单 */
/** 关闭对话框并重置表单(新增表单关闭时保存草稿) */
async function handleCloseDialog() {
if (cacheKey && dialogVisible.type === "create" && !skipDraftOnce) {
saveFormDraft(cacheKey, formData.value as Record<string, unknown>);
}
skipDraftOnce = false;
dialogVisible.visible = false;
await resetForm();
}
Expand Down Expand Up @@ -127,6 +137,13 @@ export function useCrudForm<T extends object>(options: {
if (extra) {
Object.assign(formData.value, extra);
}
if (cacheKey) {
// 恢复上次未完成的新增草稿
const draft = loadFormDraft<Record<string, unknown>>(cacheKey);
if (draft) {
Object.assign(formData.value, draft);
}
}
formRenderKey.value += 1;
dialogVisible.visible = true;
}
Expand All @@ -149,6 +166,8 @@ export function useCrudForm<T extends object>(options: {
await resetForm();
} else if (createApi) {
await createApi(formData.value);
if (cacheKey) clearFormDraft(cacheKey);
skipDraftOnce = true; // 阻止随后的 handleCloseDialog 再次保存草稿
dialogVisible.visible = false;
await resetForm();
await onCreateSuccess?.();
Expand All @@ -172,6 +191,7 @@ export function useCrudForm<T extends object>(options: {
try {
if (createApi) {
await createApi(formData.value);
if (cacheKey) clearFormDraft(cacheKey);
await onCreateSuccess?.();
}
await resetForm();
Expand All @@ -183,6 +203,13 @@ export function useCrudForm<T extends object>(options: {
}
}

// 页面卸载兜底:新增表单未关闭时(如直接切换路由)保存草稿
onUnmounted(() => {
if (cacheKey && dialogVisible.type === "create" && dialogVisible.visible) {
saveFormDraft(cacheKey, formData.value as Record<string, unknown>);
}
});

return {
submitLoading,
resetForm,
Expand Down
58 changes: 58 additions & 0 deletions frontend/web/src/hooks/core/useFormDraft.ts
Original file line number Diff line number Diff line change
@@ -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<T extends object> {
/** 草稿缓存键 */
key: string;
/** 对话框可见性(v-model) */
dialogVisible: Ref<boolean>;
/** 表单数据 */
form: Ref<T>;
/** 新建态初始值工厂 */
blank: () => T;
/** 对话框模式;不传则视为恒为新建态(无编辑态的表单) */
mode?: Ref<"create" | "update">;
}

export function useFormDraft<T extends object>(options: UseFormDraftOptions<T>) {
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<string, unknown>);
}
submitted = false;
});

/** 打开新建表单:重置为初始值 → 恢复草稿 → 应用预设(预设优先级最高) */
function openCreate(preset?: Partial<T>): void {
form.value = blank();
const draft = loadFormDraft<Record<string, unknown>>(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 };
}
43 changes: 43 additions & 0 deletions frontend/web/src/utils/form-draft.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* 创建表单草稿缓存的底层工具(localStorage 读写)。
*
* 用于「新建/新增/添加」表单:用户填了一半误关窗口,再次打开时恢复已填写内容。
* 仅缓存创建态表单,编辑态表单由详情接口回填、不缓存。
*
* 本文件只提供底层读写;自动绑定对话框生命周期的组合式函数见
* ``hooks/core/useFormDraft.ts`` 的 ``useFormDraft``。
*
* 存储键统一加 ``market-form-draft:`` 前缀,与业务数据隔离。
*/

const PREFIX = "market-form-draft:";

/** 读取创建表单草稿,无草稿或解析失败返回 null */
export function loadFormDraft<T>(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<string, unknown>): 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 {
// 忽略
}
}
Loading