refactor(crud): 提取通用CRUD逻辑与清理样式

This commit is contained in:
zhangtao
2026-05-15 20:48:14 +08:00
parent 0344765e5a
commit d56ec934a6
23 changed files with 1229 additions and 1521 deletions
+39
View File
@@ -0,0 +1,39 @@
/**
* 确认弹窗 —— 封装 ElMessageBox.confirm 常用配置
*/
/** 删除确认 */
export async function confirmDelete(message = "确认删除该项数据?"): Promise<void> {
await ElMessageBox.confirm(message, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
}
/** 批量删除确认 */
export async function confirmBatchDelete(count: number): Promise<void> {
await ElMessageBox.confirm(`确定删除选中的 ${count} 条数据吗?`, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
}
/** 状态切换确认 */
export async function confirmToggleStatus(status: string): Promise<void> {
await ElMessageBox.confirm(`确认${status === "0" ? "启用" : "停用"}该项数据?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
}
/** 通用确认 */
export async function confirmAction(message: string, title = "警告"): Promise<void> {
await ElMessageBox.confirm(message, title, {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
}
@@ -0,0 +1,49 @@
export type DialogType = "create" | "update" | "detail";
export interface CrudDialogState {
title: string;
visible: boolean;
type: DialogType;
}
/**
* CRUD 对话框状态管理
*
* @example
* ```ts
* const { dialogVisible, openDialog, closeDialog } = useCrudDialog();
* openDialog("create"); // 新增
* openDialog("update", "修改XX"); // 编辑
* openDialog("detail", "XX详情"); // 详情
* closeDialog(); // 关闭
* ```
*/
export function useCrudDialog() {
const dialogVisible = reactive<CrudDialogState>({
title: "",
visible: false,
type: "create",
});
function openDialog(type: DialogType, title?: string) {
dialogVisible.type = type;
if (type === "create") {
dialogVisible.title = title ?? "新增";
} else if (type === "update") {
dialogVisible.title = title ?? "修改";
} else {
dialogVisible.title = title ?? "详情";
}
dialogVisible.visible = true;
}
function closeDialog() {
dialogVisible.visible = false;
}
return {
dialogVisible,
openDialog,
closeDialog,
};
}
+160
View File
@@ -0,0 +1,160 @@
import type { Ref } from "vue";
import type { CrudDialogState, DialogType } from "./useCrudDialog";
/**
* CRUD 表单管理
*
* 将对话框 + 表单 + 提交逻辑统一管理,减少每个 CRUD 页面 ~60 行重复代码
*
* @example
* ```ts
* const crud = useCrudForm<DeptForm>({
* formData: ref({ id: undefined, name: "", status: "0" }),
* initialFormData: { id: undefined, name: "", status: "0" },
* dialogVisible, // 来自 useCrudDialog()
* dataFormRef, // FaForm 的 ref
* formRenderKey, // 用于 :key 重新渲染
* detailApi: DeptAPI.detailDept,
* createApi: DeptAPI.createDept,
* updateApi: DeptAPI.updateDept,
* onCreateSuccess: async () => { await refreshCreate(); },
* onUpdateSuccess: async () => { await refreshUpdate(); },
* });
*
* // 模板中
* crud.handleOpenDialog("create");
* crud.handleOpenDialog("update", row.id);
* crud.handleSubmit();
* crud.handleCloseDialog();
* ```
*/
export function useCrudForm<T extends object>(options: {
/** 响应式表单数据 */
formData: Ref<T>;
/** 初始表单值(用于重置) */
initialFormData: T;
/** 对话框状态(来自 useCrudDialog) */
dialogVisible: CrudDialogState;
/** FaForm 实例 ref */
dataFormRef: Ref<InstanceType<
(typeof import("./../../components/forms/fa-form/index.vue"))["default"]
> | null>;
/** 表单渲染 key(用于 :key 重新渲染) */
formRenderKey: Ref<number>;
/** 详情 API(传入 id,返回 response) */
detailApi?: (id: number) => Promise<{ data: { data?: T } }>;
/** 新增 API */
createApi?: (form: T) => Promise<unknown>;
/** 修改 API(接收 id + form) */
updateApi?: (id: number, form: T) => Promise<unknown>;
/** 各类型打开时的标题映射 */
titles?: Partial<Record<DialogType, string>>;
/** 详情数据(用于 FaDescriptions 展示) */
detailFormData?: Ref<Partial<T>>;
/** 新增成功回调 */
onCreateSuccess?: () => Promise<void> | void;
/** 修改成功回调 */
onUpdateSuccess?: () => Promise<void> | void;
/** 提交成功后额外操作(如刷新字典、配置等) */
onSubmitSuccess?: (formData: T) => Promise<void> | void;
}) {
const {
formData,
initialFormData,
dialogVisible,
dataFormRef,
formRenderKey,
detailApi,
createApi,
updateApi,
titles,
detailFormData,
onCreateSuccess,
onUpdateSuccess,
onSubmitSuccess,
} = options;
const submitLoading = ref(false);
/** 重置表单 */
async function resetForm() {
dataFormRef.value?.resetFields();
dataFormRef.value?.clearValidate();
Object.assign(formData.value, initialFormData);
}
/** 关闭对话框并重置表单 */
async function handleCloseDialog() {
dialogVisible.visible = false;
await resetForm();
}
/** 打开对话框 */
async function handleOpenDialog(type: DialogType, id?: number, extra?: Record<string, unknown>) {
dialogVisible.type = type;
const titleMap = titles ?? {};
const defaultTitles: Record<DialogType, string> = {
create: "新增",
update: "修改",
detail: "详情",
};
if (id && detailApi) {
const response = await detailApi(id);
const data = response.data.data;
if (type === "detail") {
dialogVisible.title = titleMap.detail ?? defaultTitles.detail;
if (detailFormData) {
Object.assign(detailFormData.value, data ?? {});
}
} else if (type === "update") {
dialogVisible.title = titleMap.update ?? defaultTitles.update;
Object.assign(formData.value, data);
}
} else {
dialogVisible.title = titleMap.create ?? defaultTitles.create;
Object.assign(formData.value, initialFormData);
(formData.value as Record<string, unknown>).id = undefined;
if (extra) {
Object.assign(formData.value, extra);
}
}
formRenderKey.value += 1;
dialogVisible.visible = true;
}
/** 提交表单 */
async function handleSubmit() {
const form = dataFormRef.value;
if (!form) return;
const valid = await (form.validate as () => Promise<boolean>)().catch(() => false);
if (!valid) return;
submitLoading.value = true;
const id = (formData.value as Record<string, unknown>).id as number | undefined;
try {
if (id && updateApi) {
await updateApi(id, { id, ...formData.value });
await onUpdateSuccess?.();
} else if (createApi) {
await createApi(formData.value);
await onCreateSuccess?.();
}
dialogVisible.visible = false;
await resetForm();
await onSubmitSuccess?.(formData.value);
} catch (error: unknown) {
console.error(error);
} finally {
submitLoading.value = false;
}
}
return {
submitLoading,
resetForm,
handleCloseDialog,
handleOpenDialog,
handleSubmit,
};
}
@@ -0,0 +1,36 @@
import type { ComputedRef, Ref } from "vue";
/**
* 表格多选状态管理
*
* @example
* ```ts
* const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } =
* useTableSelection<DictTable>();
* ```
*/
export function useTableSelection<T extends object>() {
const selectedRows = ref<T[]>([]) as Ref<T[]>;
const selectedIds = computed(() =>
selectedRows.value
.map((r) => (r as Record<string, unknown>).id)
.filter((id): id is number => id != null && !Number.isNaN(id))
) as ComputedRef<number[]>;
const batchDeleting = ref(false);
function onTableSelectionChange(rows: T[]) {
selectedRows.value = rows;
}
function clearSelection() {
selectedRows.value = [];
}
return {
selectedRows,
selectedIds,
batchDeleting,
onTableSelectionChange,
clearSelection,
};
}