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
@@ -145,6 +145,11 @@
<script setup lang="ts">
import { useTable } from "@/hooks/core/useTable";
import { useImportExport } from "@/hooks/core/useImportExport";
import { useCrudDialog } from "@/hooks/core/useCrudDialog";
import { useTableSelection } from "@/hooks/core/useTableSelection";
import { useCrudForm } from "@/hooks/core/useCrudForm";
import { confirmDelete, confirmBatchDelete, confirmToggleStatus } from "@/hooks/core/useConfirm";
import { cleanEmptyArrayParams, stripPaginationParams } from "@/utils/query";
import { renderTableOperationCell, type TableOperationAction } from "@utils/table";
import type { IObject } from "@/components/modal/types";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
@@ -173,14 +178,7 @@ type RoleSearchForm = {
};
function normalizeRoleQuery(params: Record<string, unknown>): TablePageQuery {
const p = { ...params } as Record<string, unknown>;
if (Array.isArray(p.created_time) && p.created_time.length === 0) p.created_time = undefined;
if (Array.isArray(p.updated_time) && p.updated_time.length === 0) p.updated_time = undefined;
if (typeof p.status === "string") {
if (p.status === "true" || p.status === "false") {
/* 与列表查询一致,保留字符串 */
}
}
const p = cleanEmptyArrayParams({ ...params });
return p as unknown as TablePageQuery;
}
@@ -353,15 +351,10 @@ const roleSearchItems = computed<SearchFormItem[]>(() => [
]);
const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);
const selectedRows = ref<RoleTable[]>([]);
const selectedIds = computed(() =>
selectedRows.value.map((r) => r.id).filter((id): id is number => id != null && !Number.isNaN(id))
);
const batchDeleting = ref(false);
function onTableSelectionChange(rows: RoleTable[]) {
selectedRows.value = rows;
}
// ─── 表格多选 ───
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } =
useTableSelection<RoleTable>();
const drawerVisible = ref(false);
const checkedRole = ref({ id: 0, name: "" });
@@ -373,11 +366,7 @@ function handleOpenAssignPermDialog(roleId: number, roleName: string) {
async function deleteRoleRow(id: number) {
try {
await ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await confirmDelete();
await RoleAPI.deleteRole([id]);
const userStore = useUserStore();
await userStore.getUserInfo();
@@ -389,6 +378,91 @@ async function deleteRoleRow(id: number) {
}
}
// ─── 对话框状态 ───
const { dialogVisible } = useCrudDialog();
const detailFormData = ref<RoleTable>({} as RoleTable);
const roleDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "角色名称", prop: "name" },
{ label: "排序", prop: "order" },
{ label: "角色编码", prop: "code" },
{ label: "数据权限", prop: "data_scope", slot: "data_scope" },
{ label: "所属部门", prop: "depts", slot: "depts" },
{
label: "状态",
prop: "status",
tag: {
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } },
},
},
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
{ label: "描述", prop: "description", span: 4 },
];
const formData = ref<RoleForm>({
id: undefined,
name: undefined,
order: 1,
code: "",
status: "0",
description: undefined,
});
const CODE_PATTERN = /^[A-Za-z][A-Za-z0-9_]{1,15}$/;
const rules = reactive({
name: [{ required: true, message: "请输入角色名称", trigger: "blur" }],
code: [
{ required: true, message: "请输入角色编码", trigger: "blur" },
{
pattern: CODE_PATTERN,
message: "字母开头,2-16位字母/数字/下划线",
trigger: "blur",
},
],
order: [{ required: true, message: "请输入角色排序", trigger: "blur" }],
status: [{ required: true, message: "请选择状态", trigger: "blur" }],
});
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const roleFormRenderKey = ref(0);
const initialFormData: RoleForm = {
id: undefined,
name: undefined,
order: 1,
code: "",
status: "0",
description: undefined,
};
// ─── CRUD 表单 ───
const { submitLoading, handleCloseDialog, handleOpenDialog, handleSubmit } = useCrudForm<RoleForm>({
formData,
initialFormData,
dialogVisible,
dataFormRef,
formRenderKey: roleFormRenderKey,
detailApi: RoleAPI.detailRole,
createApi: RoleAPI.createRole,
updateApi: RoleAPI.updateRole,
titles: { create: "新增角色", update: "修改角色", detail: "角色详情" },
detailFormData,
onCreateSuccess: async () => {
await refreshCreate();
},
onUpdateSuccess: async () => {
await refreshUpdate();
},
onSubmitSuccess: async () => {
const userStore = useUserStore();
await userStore.getUserInfo();
},
});
const opCtx = {
onPerm: handleOpenAssignPermDialog,
onDetail: (id: number) => void handleOpenDialog("detail", id),
@@ -396,6 +470,58 @@ const opCtx = {
onDelete: deleteRoleRow,
};
const roleDialogFormItems = computed<FormItem[]>(() => [
{
label: "角色名称",
key: "name",
type: "input",
span: 24,
props: { placeholder: "请输入角色名称" },
},
{
label: "排序",
key: "order",
type: "number",
span: 24,
props: {
controlsPosition: "right",
min: 0,
style: { width: "100px" },
},
},
{
label: "角色编码",
key: "code",
type: "input",
span: 24,
props: {
placeholder: "字母开头,2-16位字母/数字/下划线",
maxlength: 16,
showWordLimit: true,
},
},
{
label: "状态",
key: "status",
type: "input",
span: 24,
placeholder: "",
},
{
label: "描述",
key: "description",
type: "input",
span: 24,
props: {
type: "textarea",
rows: 4,
maxlength: 100,
showWordLimit: true,
placeholder: "请输入描述",
},
},
]);
const {
columns,
columnChecks,
@@ -474,19 +600,8 @@ const roleCrudCols = computed(() =>
);
const exportQueryParams = computed(() => {
const sp = { ...(searchParams as object) } as Record<string, unknown>;
delete sp.current;
delete sp.size;
delete sp.page_no;
delete sp.page_size;
const q = normalizeRoleQuery(sp);
if (typeof q.status === "string") {
const s = q.status;
if (s === "true" || s === "false") {
(q as unknown as Record<string, unknown>).status = s === "true";
}
}
return q as unknown as Record<string, unknown>;
const sp = stripPaginationParams(searchParams as Record<string, unknown>);
return normalizeRoleQuery(sp) as unknown as Record<string, unknown>;
});
const roleExportContentConfig = computed(() => ({
@@ -494,138 +609,12 @@ const roleExportContentConfig = computed(() => ({
cols: roleCrudCols.value,
exportsBlobAction: async (params: IObject) => {
const base = { ...(exportQueryParams.value as Record<string, unknown>) };
const merged = normalizeRoleQuery({
...base,
...params,
} as Record<string, unknown>);
if (typeof merged.status === "string") {
const s = merged.status;
if (s === "true" || s === "false") {
(merged as unknown as Record<string, unknown>).status = s === "true";
}
}
const merged = normalizeRoleQuery({ ...base, ...params } as Record<string, unknown>);
const res = await RoleAPI.exportRole(merged as TablePageQuery);
return res.data as Blob;
},
}));
const detailFormData = ref<RoleTable>({} as RoleTable);
const roleDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "角色名称", prop: "name" },
{ label: "排序", prop: "order" },
{ label: "角色编码", prop: "code" },
{ label: "数据权限", prop: "data_scope", slot: "data_scope" },
{ label: "所属部门", prop: "depts", slot: "depts" },
{
label: "状态",
prop: "status",
tag: {
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } },
},
},
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
{ label: "描述", prop: "description", span: 4 },
];
const formData = ref<RoleForm>({
id: undefined,
name: undefined,
order: 1,
code: "",
status: "0",
description: undefined,
});
const dialogVisible = reactive({
title: "",
visible: false,
type: "create" as "create" | "update" | "detail",
});
const CODE_PATTERN = /^[A-Za-z][A-Za-z0-9_]{1,15}$/;
const rules = reactive({
name: [{ required: true, message: "请输入角色名称", trigger: "blur" }],
code: [
{ required: true, message: "请输入角色编码", trigger: "blur" },
{
pattern: CODE_PATTERN,
message: "字母开头,2-16位字母/数字/下划线",
trigger: "blur",
},
],
order: [{ required: true, message: "请输入角色排序", trigger: "blur" }],
status: [{ required: true, message: "请选择状态", trigger: "blur" }],
});
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const roleFormRenderKey = ref(0);
const roleDialogFormItems = computed<FormItem[]>(() => [
{
label: "角色名称",
key: "name",
type: "input",
span: 24,
props: { placeholder: "请输入角色名称" },
},
{
label: "排序",
key: "order",
type: "number",
span: 24,
props: {
controlsPosition: "right",
min: 0,
style: { width: "100px" },
},
},
{
label: "角色编码",
key: "code",
type: "input",
span: 24,
props: {
placeholder: "字母开头,2-16位字母/数字/下划线",
maxlength: 16,
showWordLimit: true,
},
},
{
label: "状态",
key: "status",
type: "input",
span: 24,
placeholder: "",
},
{
label: "描述",
key: "description",
type: "input",
span: 24,
props: {
type: "textarea",
rows: 4,
maxlength: 100,
showWordLimit: true,
placeholder: "请输入描述",
},
},
]);
const submitLoading = ref(false);
const initialFormData: RoleForm = {
id: undefined,
name: undefined,
order: 1,
code: "",
status: "0",
description: undefined,
};
const { exportVisible, openExport } = useImportExport();
async function handleSearchBarSearch(params: RoleSearchForm) {
@@ -643,71 +632,11 @@ function onResetSearch() {
void resetSearchParams();
}
async function resetForm() {
dataFormRef.value?.resetFields();
dataFormRef.value?.clearValidate();
Object.assign(formData, initialFormData);
}
async function handleCloseDialog() {
dialogVisible.visible = false;
await resetForm();
}
async function handleOpenDialog(type: "create" | "update" | "detail", id?: number) {
dialogVisible.type = type;
if (id) {
const response = await RoleAPI.detailRole(id);
if (type === "detail") {
dialogVisible.title = "角色详情";
Object.assign(detailFormData.value, response.data.data ?? {});
} else if (type === "update") {
dialogVisible.title = "修改角色";
Object.assign(formData, response.data.data);
}
} else {
dialogVisible.title = "新增角色";
Object.assign(formData.value, initialFormData);
formData.value.id = undefined;
}
roleFormRenderKey.value += 1;
dialogVisible.visible = true;
}
async function handleSubmit() {
dataFormRef.value?.validate(async (valid: boolean) => {
if (!valid) return;
submitLoading.value = true;
const id = formData.value.id;
try {
if (id) {
await RoleAPI.updateRole(id, { id, ...formData.value });
await refreshUpdate();
} else {
await RoleAPI.createRole(formData.value);
await refreshCreate();
}
dialogVisible.visible = false;
await resetForm();
const userStore = useUserStore();
await userStore.getUserInfo();
} catch (error: unknown) {
console.error(error);
} finally {
submitLoading.value = false;
}
});
}
async function handleBatchDelete() {
const ids = selectedIds.value;
if (ids.length === 0) return;
try {
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条数据吗?`, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await confirmBatchDelete(ids.length);
batchDeleting.value = true;
await RoleAPI.deleteRole(ids);
const userStore = useUserStore();
@@ -729,11 +658,7 @@ async function handleMoreClick(status: string) {
return;
}
try {
await ElMessageBox.confirm(`确认${status === "0" ? "启用" : "停用"}该项数据?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await confirmToggleStatus(status);
await RoleAPI.batchRole({ ids, status });
await refreshData();
const userStore = useUserStore();
@@ -743,17 +668,3 @@ async function handleMoreClick(status: string) {
}
}
</script>
<style scoped lang="scss">
.crud-dialog-art-form :deep(.el-row > .el-col:last-child) {
display: none;
}
.crud-dialog-art-form :deep(.el-form-item__content) {
max-width: 100%;
}
:deep(.role-table-actions .inline-flex) {
vertical-align: middle;
}
</style>