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
@@ -108,6 +108,10 @@
<script setup lang="ts">
import { useTableColumns } from "@/hooks/core/useTableColumns";
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 DeptAPI, {
type DeptForm,
type DeptPageQuery,
@@ -250,16 +254,11 @@ const tableRef = ref<{
const tableData = ref<DeptTable[]>([]);
const loading = ref(false);
const isExpanded = ref(false);
const selectedRows = ref<DeptTable[]>([]);
const selectedIds = computed(() =>
selectedRows.value.map((r) => r.id).filter((id): id is number => id != null && !Number.isNaN(id))
);
const batchDeleting = ref(false);
const deptOptions = ref<OptionType[]>([]);
function onTableSelectionChange(rows: DeptTable[]) {
selectedRows.value = rows;
}
// ─── 表格多选 ───
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } =
useTableSelection<DeptTable>();
async function loadDeptData() {
loading.value = true;
@@ -277,11 +276,7 @@ async function loadDeptData() {
async function deleteDeptRow(id: number) {
try {
await ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await confirmDelete();
await DeptAPI.deleteDept([id]);
await userStore.getUserInfo();
ElMessage.success("删除成功");
@@ -292,8 +287,94 @@ async function deleteDeptRow(id: number) {
}
}
// ─── 对话框状态 ───
const { dialogVisible } = useCrudDialog();
const detailFormData = ref<DeptTable>({ code: "" });
const deptDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "部门名称", prop: "name" },
{ label: "部门编码", prop: "code" },
{ label: "上级部门", prop: "parent_name" },
{
label: "状态",
prop: "status",
tag: {
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } },
},
},
{ label: "排序", prop: "order" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
{ label: "描述", prop: "description", span: 4 },
];
const formData = ref<DeptForm>({
id: undefined,
name: undefined,
code: "",
order: 1,
parent_id: undefined,
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 initialFormData: DeptForm = {
id: undefined,
name: undefined,
code: "",
order: 1,
parent_id: undefined,
status: "0",
description: undefined,
};
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const deptFormRenderKey = ref(0);
// ─── CRUD 表单 ───
const { submitLoading, handleCloseDialog, handleOpenDialog, handleSubmit } = useCrudForm<DeptForm>({
formData,
initialFormData,
dialogVisible,
dataFormRef,
formRenderKey: deptFormRenderKey,
detailApi: DeptAPI.detailDept,
createApi: DeptAPI.createDept,
updateApi: DeptAPI.updateDept,
titles: { create: "新增部门", update: "修改部门", detail: "部门详情" },
detailFormData,
onCreateSuccess: async () => {
await loadDeptData();
},
onUpdateSuccess: async () => {
await loadDeptData();
},
onSubmitSuccess: async () => {
await userStore.getUserInfo();
},
});
const opCtx = {
onAddChild: (parentId: number) => void handleOpenDialog("create", undefined, parentId),
onAddChild: (parentId: number) =>
void handleOpenDialog("create", undefined, { parent_id: parentId }),
onDetail: (id: number) => void handleOpenDialog("detail", id),
onEdit: (id: number) => void handleOpenDialog("update", id),
onDelete: deleteDeptRow,
@@ -327,72 +408,6 @@ const { columnChecks, columns } = useTableColumns<DeptTable>(() => [
},
]);
const detailFormData = ref<DeptTable>({ code: "" });
const deptDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "部门名称", prop: "name" },
{ label: "部门编码", prop: "code" },
{ label: "上级部门", prop: "parent_name" },
{
label: "状态",
prop: "status",
tag: {
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } },
},
},
{ label: "排序", prop: "order" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
{ label: "描述", prop: "description", span: 4 },
];
const formData = ref<DeptForm>({
id: undefined,
name: undefined,
code: "",
order: 1,
parent_id: undefined,
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 initialFormData: DeptForm = {
id: undefined,
name: undefined,
code: "",
order: 1,
parent_id: undefined,
status: "0",
description: undefined,
};
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const submitLoading = ref(false);
const deptFormRenderKey = ref(0);
const deptDialogFormItems = computed<FormItem[]>(() => [
{
label: "部门名称",
@@ -473,73 +488,11 @@ function onResetSearch() {
void loadDeptData();
}
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,
parentId?: number
) {
dialogVisible.type = type;
if (id) {
const response = await DeptAPI.detailDept(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;
if (parentId) {
formData.value.parent_id = parentId;
}
}
deptFormRenderKey.value += 1;
dialogVisible.visible = true;
}
async function handleSubmit() {
dataFormRef.value?.validate(async (valid: boolean) => {
if (!valid) return;
const id = formData.value.id;
try {
if (id) {
await DeptAPI.updateDept(id, { id, ...formData.value });
} else {
await DeptAPI.createDept(formData.value);
}
dialogVisible.visible = false;
await resetForm();
await loadDeptData();
await userStore.getUserInfo();
} catch (error: unknown) {
console.error(error);
}
});
}
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 DeptAPI.deleteDept(ids);
await userStore.getUserInfo();
@@ -560,11 +513,7 @@ async function handleMoreClick(status: string) {
return;
}
try {
await ElMessageBox.confirm(`确认${status === "0" ? "启用" : "停用"}该项数据?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await confirmToggleStatus(status);
await DeptAPI.batchDept({ ids, status });
await loadDeptData();
await userStore.getUserInfo();
@@ -594,17 +543,3 @@ onMounted(() => {
void loadDeptData();
});
</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(.dept-table-actions .inline-flex) {
vertical-align: middle;
}
</style>
@@ -198,6 +198,10 @@
<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 { confirmDelete, confirmBatchDelete, confirmToggleStatus } from "@/hooks/core/useConfirm";
import { cleanEmptyArrayParams, stripPaginationParams } from "@/utils/query";
import type { IObject } from "@/components/modal/types";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue";
@@ -280,10 +284,10 @@ type DictDataSearchForm = {
};
function normalizeDictDataQuery(params: Record<string, unknown>): DictDataPageQuery {
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;
return p as unknown as DictDataPageQuery;
return cleanEmptyArrayParams({ ...params }, [
"created_time",
"updated_time",
]) as unknown as DictDataPageQuery;
}
async function fetchDictDataListMerged(params: Record<string, unknown>) {
@@ -360,15 +364,8 @@ const dictDataSearchItems = computed<SearchFormItem[]>(() => [
]);
const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);
const selectedRows = ref<DictDataTable[]>([]);
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: DictDataTable[]) {
selectedRows.value = rows;
}
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } =
useTableSelection<DictDataTable>();
const {
columns,
@@ -458,11 +455,7 @@ const dictDataCrudCols = 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 sp = stripPaginationParams(searchParams as Record<string, unknown>);
return normalizeDictDataQuery({
...sp,
dict_type: props.dictType,
@@ -485,11 +478,7 @@ const dictDataExportContentConfig = computed(() => ({
},
}));
const dialogVisible = reactive({
title: "",
visible: false,
type: "create" as "create" | "update" | "detail",
});
const { dialogVisible } = useCrudDialog();
const detailFormData = ref<DictDataTable>({});
@@ -768,11 +757,7 @@ function formatDictDataOperationCell(row: DictDataTable) {
async function deleteDictDataRow(id: number) {
try {
await ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await confirmDelete();
await DictAPI.deleteDictData([id]);
dictStore.clearDictData();
if (props.dictType) await dictStore.getDict([props.dictType]);
@@ -788,11 +773,7 @@ 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 DictAPI.deleteDictData(ids);
dictStore.clearDictData();
@@ -814,11 +795,7 @@ async function handleMoreClick(status: string) {
return;
}
try {
await ElMessageBox.confirm(`确认${status === "0" ? "启用" : "停用"}该项数据?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await confirmToggleStatus(status);
await DictAPI.batchDictData({ ids, status });
await refreshData();
dictStore.clearDictData();
@@ -860,12 +837,4 @@ async function handleMoreClick(status: string) {
background: var(--el-fill-color-light);
border-color: var(--el-border-color-lighter);
}
.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%;
}
</style>
@@ -1,4 +1,4 @@
<!-- 字典类型:Art 布局;操作列最多 3 个外露 +「更多」 -->
<!-- 字典类型:Fa 布局;操作列最多 3 个外露 +「更多」 -->
<template>
<div class="fa-full-height">
<FaSearchBar
@@ -128,6 +128,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 type { IObject } from "@/components/modal/types";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue";
@@ -154,13 +159,6 @@ type DictTypeSearchForm = {
created_time?: string[];
};
function normalizeDictTypeQuery(params: Record<string, unknown>): DictPageQuery {
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;
return p as unknown as DictPageQuery;
}
const dictStore = useDictStore();
const { hasAuth } = useAuth();
@@ -226,15 +224,119 @@ const dictTypeSearchItems = computed<SearchFormItem[]>(() => [
]);
const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);
const selectedRows = ref<DictTable[]>([]);
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: DictTable[]) {
selectedRows.value = rows;
}
// ─── 表格多选 ───
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } =
useTableSelection<DictTable>();
// ─── 对话框状态 ───
const { dialogVisible } = useCrudDialog();
const detailFormData = ref<DictTable>({});
const dictDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "字典名称", prop: "dict_name" },
{ label: "字典类型", prop: "dict_type", slot: "dict_type" },
{
label: "状态",
prop: "status",
tag: {
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } },
},
},
{ label: "描述", prop: "description" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
];
const formData = ref<DictForm>({
id: undefined,
dict_name: "",
dict_type: "",
status: "0",
description: undefined,
});
const rules = reactive({
dict_name: [{ required: true, message: "请输入字典名称", trigger: "blur" }],
dict_type: [{ required: true, message: "请选择字典类型", trigger: "blur" }],
status: [{ required: true, message: "请选择字典状态", trigger: "blur" }],
});
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const dictFormRenderKey = ref(0);
const initialFormData: DictForm = {
id: undefined,
dict_name: "",
dict_type: "",
status: "0",
description: undefined,
};
// ─── CRUD 表单 ───
const { submitLoading, handleCloseDialog, handleOpenDialog, handleSubmit } = useCrudForm<DictForm>({
formData,
initialFormData,
dialogVisible,
dataFormRef,
formRenderKey: dictFormRenderKey,
detailApi: DictAPI.detailDictType,
createApi: DictAPI.createDictType,
updateApi: DictAPI.updateDictType,
titles: { create: "新增字典", update: "修改字典", detail: "字典详情" },
detailFormData,
onCreateSuccess: async () => {
await refreshCreate();
},
onUpdateSuccess: async () => {
await refreshUpdate();
},
onSubmitSuccess: async () => {
dictStore.clearDictData();
if (formData.value.dict_type) {
await dictStore.getDict([formData.value.dict_type]);
}
},
});
const dictDialogFormItems = computed<FormItem[]>(() => [
{
label: "字典名称",
key: "dict_name",
type: "input",
span: 24,
props: { placeholder: "请输入字典名称", maxlength: 50 },
},
{
label: "字典类型",
key: "dict_type",
type: "input",
span: 24,
props: { placeholder: "请输入字典类型", maxlength: 50 },
},
{
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,
@@ -309,114 +411,23 @@ const dictTypeCrudCols = 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;
return normalizeDictTypeQuery(sp);
const sp = stripPaginationParams(searchParams as Record<string, unknown>);
return cleanEmptyArrayParams(sp) as unknown as DictPageQuery;
});
const dictTypeExportContentConfig = computed(() => ({
permPrefix: "module_system:dict_type",
cols: dictTypeCrudCols.value,
exportsBlobAction: async (params: IObject) => {
const merged = normalizeDictTypeQuery({
const merged = cleanEmptyArrayParams({
...(exportQueryParams.value as unknown as Record<string, unknown>),
...params,
} as Record<string, unknown>);
const res = await DictAPI.exportDictType(merged as DictPageQuery);
const res = await DictAPI.exportDictType(merged as unknown as DictPageQuery);
return res.data as Blob;
},
}));
const dialogVisible = reactive({
title: "",
visible: false,
type: "create" as "create" | "update" | "detail",
});
const detailFormData = ref<DictTable>({});
const dictDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "字典名称", prop: "dict_name" },
{ label: "字典类型", prop: "dict_type", slot: "dict_type" },
{
label: "状态",
prop: "status",
tag: {
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } },
},
},
{ label: "描述", prop: "description" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
];
const formData = ref<DictForm>({
id: undefined,
dict_name: "",
dict_type: "",
status: "0",
description: undefined,
});
const rules = reactive({
dict_name: [{ required: true, message: "请输入字典名称", trigger: "blur" }],
dict_type: [{ required: true, message: "请选择字典类型", trigger: "blur" }],
status: [{ required: true, message: "请选择字典状态", trigger: "blur" }],
});
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const submitLoading = ref(false);
const dictFormRenderKey = ref(0);
const dictDialogFormItems = computed<FormItem[]>(() => [
{
label: "字典名称",
key: "dict_name",
type: "input",
span: 24,
props: { placeholder: "请输入字典名称", maxlength: 50 },
},
{
label: "字典类型",
key: "dict_type",
type: "input",
span: 24,
props: { placeholder: "请输入字典类型", maxlength: 50 },
},
{
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 initialFormData: DictForm = {
id: undefined,
dict_name: "",
dict_type: "",
status: "0",
description: undefined,
};
const { exportVisible, openExport } = useImportExport();
const drawerVisible = ref(false);
@@ -501,68 +512,9 @@ function handleDictDataDrawer(dictTypeRow: DictTable) {
drawerVisible.value = true;
}
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 DictAPI.detailDictType(id);
if (type === "detail") {
dialogVisible.title = "字典详情";
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;
}
dictFormRenderKey.value += 1;
dialogVisible.visible = true;
}
async function handleSubmit() {
dataFormRef.value?.validate(async (valid: boolean) => {
if (!valid) return;
const id = formData.value.id;
try {
if (id) {
await DictAPI.updateDictType(id, { id, ...formData.value });
await refreshUpdate();
} else {
await DictAPI.createDictType(formData.value);
await refreshCreate();
}
dialogVisible.visible = false;
await resetForm();
dictStore.clearDictData();
if (formData.value.dict_type) {
await dictStore.getDict([formData.value.dict_type]);
}
} catch (error: unknown) {
console.error(error);
}
});
}
async function deleteDictTypeRow(id: number) {
try {
await ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await confirmDelete();
await DictAPI.deleteDictType([id]);
dictStore.clearDictData();
const dictTypes = Object.keys(dictStore.dictData);
@@ -579,11 +531,7 @@ 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 DictAPI.deleteDictType(ids);
dictStore.clearDictData();
@@ -606,11 +554,7 @@ async function handleMoreClick(status: string) {
return;
}
try {
await ElMessageBox.confirm(`确认${status === "0" ? "启用" : "停用"}该项数据?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await confirmToggleStatus(status);
await DictAPI.batchDictType({ ids, status });
await refreshData();
dictStore.clearDictData();
@@ -621,13 +565,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%;
}
</style>
@@ -1,4 +1,4 @@
<!-- 日志管理:Art 布局 + useTable,与 dict 页一致 -->
<!-- 日志管理:Fa 布局 + useTable,与 dict 页一致 -->
<template>
<div class="fa-full-height">
<FaSearchBar
@@ -114,6 +114,10 @@
<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 { confirmDelete, confirmBatchDelete } from "@/hooks/core/useConfirm";
import { cleanEmptyArrayParams, stripPaginationParams } from "@/utils/query";
import type { IObject } from "@/components/modal/types";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import type { ColumnOption } from "@/types/component";
@@ -137,10 +141,7 @@ type LogSearchForm = {
};
function normalizeLogQuery(params: Record<string, unknown>): LogPageQuery {
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;
return p as unknown as LogPageQuery;
return cleanEmptyArrayParams({ ...params }) as unknown as LogPageQuery;
}
function buildLogReplaceParams(p: LogSearchForm): Record<string, unknown> {
@@ -216,15 +217,8 @@ const logSearchItems = computed<SearchFormItem[]>(() => [
]);
const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);
const selectedRows = ref<LogTable[]>([]);
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: LogTable[]) {
selectedRows.value = rows;
}
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } =
useTableSelection<LogTable>();
const {
columns,
@@ -333,11 +327,7 @@ const logCrudCols = 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 sp = stripPaginationParams(searchParams as Record<string, unknown>);
return normalizeLogQuery(sp);
});
@@ -375,10 +365,7 @@ const logDetailItems: import("@/components/others/fa-descriptions/index.vue").De
{ label: "更新时间", prop: "updated_time" },
];
const dialogVisible = ref({
title: "",
visible: false,
});
const { dialogVisible, closeDialog } = useCrudDialog();
function getStatusCodeType(code?: number) {
if (code === undefined) return "info";
@@ -430,24 +417,20 @@ async function resetForm() {
}
async function handleCloseDialog() {
dialogVisible.value.visible = false;
closeDialog();
await resetForm();
}
async function handleOpenDialog(id: number) {
dialogVisible.value.title = "日志详情";
dialogVisible.title = "日志详情";
const response = await LogAPI.detailLog(id);
Object.assign(formData, response.data.data ?? {});
dialogVisible.value.visible = true;
dialogVisible.visible = true;
}
async function deleteLogRow(id: number) {
try {
await ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await confirmDelete();
await LogAPI.deleteLog([id]);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
@@ -492,11 +475,7 @@ 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 LogAPI.deleteLog(ids);
ElMessage.success("删除成功");
@@ -1,4 +1,4 @@
<!-- 公告通知:Art 布局 + useTable,与 dict 页一致 -->
<!-- 公告通知:Fa 布局 + useTable,与 dict 页一致 -->
<template>
<div class="fa-full-height">
<FaSearchBar
@@ -100,7 +100,6 @@
</FaDescriptions>
</template>
<template v-else>
<!-- FaForm + items + 栅格;弹窗内关闭内置提交/重置,仍用底部按钮 -->
<FaForm
:key="noticeFormRenderKey"
scrollbar
@@ -150,6 +149,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 type { IObject } from "@/components/modal/types";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import type { ColumnOption } from "@/types/component";
@@ -182,10 +186,7 @@ type NoticeSearchForm = {
};
function normalizeNoticeQuery(params: Record<string, unknown>): NoticePageQuery {
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;
return p as unknown as NoticePageQuery;
return cleanEmptyArrayParams({ ...params }) as unknown as NoticePageQuery;
}
function noticeTypeLabel(val?: string) {
@@ -274,15 +275,157 @@ const noticeSearchItems = computed<SearchFormItem[]>(() => [
]);
const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);
const selectedRows = ref<NoticeTable[]>([]);
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: NoticeTable[]) {
selectedRows.value = rows;
}
// ─── 表格多选 ───
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } =
useTableSelection<NoticeTable>();
// ─── 对话框状态 ───
const { dialogVisible } = useCrudDialog();
const detailFormData = ref<NoticeTable>({});
const noticeDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "标题", prop: "notice_title" },
{ label: "类型", prop: "notice_type", slot: "notice_type" },
{
label: "状态",
prop: "status",
tag: {
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } },
},
},
{ label: "描述", prop: "description" },
{ label: "内容", prop: "notice_content", slot: "notice_content", span: 4 },
{ label: "创建人", prop: "created_by.name" },
{ label: "更新人", prop: "updated_by.name" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
];
/** 详情富文本 HTML(用于预览) */
const detailContentHtml = computed({
get: () => detailFormData.value.notice_content ?? "",
set: (v: string) => {
detailFormData.value.notice_content = v;
},
});
/** 详情是否有可视文本 */
const detailHasRenderableContent = computed(() => {
const raw = detailFormData.value.notice_content ?? "";
if (!raw.trim()) return false;
const plain = raw
.replace(/<[^>]+>/g, " ")
.replace(/\s+/g, " ")
.trim();
return plain.length > 0;
});
const formData = ref<NoticeForm>({
id: undefined,
notice_title: "",
notice_type: "",
notice_content: "",
status: "0",
description: undefined,
});
const rules = reactive({
notice_title: [{ required: true, message: "请输入公告通知标题", trigger: "blur" }],
notice_type: [{ required: true, message: "请选择公告通知类型", trigger: "blur" }],
notice_content: [{ required: true, message: "请输入公告通知内容", trigger: "blur" }],
status: [{ required: true, message: "请选择公告通知状态", trigger: "blur" }],
});
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const noticeFormRenderKey = ref(0);
const initialFormData: NoticeForm = {
id: undefined,
notice_title: "",
notice_type: "",
notice_content: "",
status: "0",
description: undefined,
};
// ─── CRUD 表单 ───
const { submitLoading, handleCloseDialog, handleOpenDialog, handleSubmit } =
useCrudForm<NoticeForm>({
formData,
initialFormData,
dialogVisible,
dataFormRef,
formRenderKey: noticeFormRenderKey,
detailApi: NoticeAPI.detailNotice,
createApi: NoticeAPI.createNotice,
updateApi: NoticeAPI.updateNotice,
titles: { create: "新增公告通知", update: "修改公告通知", detail: "公告通知详情" },
detailFormData,
onCreateSuccess: async () => {
await refreshCreate();
},
onUpdateSuccess: async () => {
await refreshUpdate();
},
onSubmitSuccess: async () => {
await noticeStore.getNotice();
},
});
const noticeDialogFormItems = computed<FormItem[]>(() => [
{
label: "标题",
key: "notice_title",
type: "input",
span: 24,
props: { placeholder: "请输入标题", maxlength: 50 },
},
{
label: "描述",
key: "description",
type: "input",
span: 24,
props: {
type: "textarea",
rows: 2,
maxlength: 100,
showWordLimit: true,
placeholder: "请输入描述",
},
},
{
label: "类型",
key: "notice_type",
type: "select",
span: 24,
props: {
placeholder: "请选择类型",
clearable: true,
class: "!w-full max-w-md",
options: dictStore.getDictArray("sys_notice_type").map((item) => ({
label: item.dict_label,
value: item.dict_value,
})),
},
},
{
label: "状态",
key: "status",
type: "input",
span: 24,
placeholder: "",
},
{
label: "内容",
key: "notice_content",
type: "input",
span: 24,
placeholder: "",
},
]);
const {
columns,
@@ -373,11 +516,7 @@ const noticeCrudCols = 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 sp = stripPaginationParams(searchParams as Record<string, unknown>);
return normalizeNoticeQuery(sp);
});
@@ -394,136 +533,6 @@ const noticeExportContentConfig = computed(() => ({
},
}));
const detailFormData = ref<NoticeTable>({});
const noticeDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "标题", prop: "notice_title" },
{ label: "类型", prop: "notice_type", slot: "notice_type" },
{
label: "状态",
prop: "status",
tag: {
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } },
},
},
{ label: "描述", prop: "description" },
{ label: "内容", prop: "notice_content", slot: "notice_content", span: 4 },
{ label: "创建人", prop: "created_by.name" },
{ label: "更新人", prop: "updated_by.name" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
];
/** 详情富文本 HTML(用于预览) */
const detailContentHtml = computed({
get: () => detailFormData.value.notice_content ?? "",
set: (v: string) => {
detailFormData.value.notice_content = v;
},
});
/** 详情是否有可视文本(排除仅空标签) */
const detailHasRenderableContent = computed(() => {
const raw = detailFormData.value.notice_content ?? "";
if (!raw.trim()) return false;
const plain = raw
.replace(/<[^>]+>/g, " ")
.replace(/\s+/g, " ")
.trim();
return plain.length > 0;
});
const formData = ref<NoticeForm>({
id: undefined,
notice_title: "",
notice_type: "",
notice_content: "",
status: "0",
description: undefined,
});
const dialogVisible = reactive({
title: "",
visible: false,
type: "create" as "create" | "update" | "detail",
});
const rules = reactive({
notice_title: [{ required: true, message: "请输入公告通知标题", trigger: "blur" }],
notice_type: [{ required: true, message: "请选择公告通知类型", trigger: "blur" }],
notice_content: [{ required: true, message: "请输入公告通知内容", trigger: "blur" }],
status: [{ required: true, message: "请选择公告通知状态", trigger: "blur" }],
});
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const submitLoading = ref(false);
/** 每次打开弹窗递增,令 FaForm 重新挂载并同步初始 model(与示例页声明式 items 一致) */
const noticeFormRenderKey = ref(0);
/** 公告编辑表单字段配置(FaForm + items) */
const noticeDialogFormItems = computed<FormItem[]>(() => [
{
label: "标题",
key: "notice_title",
type: "input",
span: 24,
props: { placeholder: "请输入标题", maxlength: 50 },
},
{
label: "描述",
key: "description",
type: "input",
span: 24,
props: {
type: "textarea",
rows: 2,
maxlength: 100,
showWordLimit: true,
placeholder: "请输入描述",
},
},
{
label: "类型",
key: "notice_type",
type: "select",
span: 24,
props: {
placeholder: "请选择类型",
clearable: true,
class: "!w-full max-w-md",
options: dictStore.getDictArray("sys_notice_type").map((item) => ({
label: item.dict_label,
value: item.dict_value,
})),
},
},
{
label: "状态",
key: "status",
type: "input",
span: 24,
placeholder: "",
},
{
label: "内容",
key: "notice_content",
type: "input",
span: 24,
placeholder: "",
},
]);
const initialFormData: NoticeForm = {
id: undefined,
notice_title: "",
notice_type: "",
notice_content: "",
status: "0",
description: undefined,
};
const { exportVisible, openExport } = useImportExport();
function buildNoticeReplaceParams(p: NoticeSearchForm): Record<string, unknown> {
@@ -565,70 +574,9 @@ function onResetSearch() {
void resetSearchParams();
}
async function resetForm() {
const inner = dataFormRef.value?.ref;
inner?.resetFields();
inner?.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 NoticeAPI.detailNotice(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;
}
noticeFormRenderKey.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 NoticeAPI.updateNotice(id, { id, ...formData.value });
await refreshUpdate();
} else {
await NoticeAPI.createNotice(formData.value);
await refreshCreate();
}
dialogVisible.visible = false;
await resetForm();
await noticeStore.getNotice();
} catch (error: unknown) {
console.error(error);
} finally {
submitLoading.value = false;
}
});
}
async function deleteNoticeRow(id: number) {
try {
await ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await confirmDelete();
await NoticeAPI.deleteNotice([id]);
await noticeStore.getNotice();
ElMessage.success("删除成功");
@@ -684,11 +632,7 @@ 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 NoticeAPI.deleteNotice(ids);
await noticeStore.getNotice();
@@ -708,12 +652,8 @@ async function handleMoreClick(status: string) {
ElMessage.warning("请先选择要操作的数据");
return;
}
ElMessageBox.confirm(`确认${status === "0" ? "启用" : "停用"}该项数据?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
try {
await confirmToggleStatus(status);
await NoticeAPI.batchNotice({ ids, status });
await refreshData();
await noticeStore.getNotice();
@@ -728,15 +668,6 @@ onMounted(async () => {
</script>
<style scoped lang="scss">
/* FaForm 底部预留的操作栏列在弹窗内不需要占位 */
.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%;
}
/* 富文本预览区域阅读样式(与 FaWangEditor 输出 HTML 展示一致) */
.notice-html-preview {
box-sizing: border-box;
@@ -1,4 +1,4 @@
<!-- 系统配置:Art 布局 + useTable,与 notice 页一致 -->
<!-- 系统配置:Fa 布局 + useTable,与 notice 页一致 -->
<template>
<div class="fa-full-height">
<FaSearchBar
@@ -114,6 +114,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 } from "@/hooks/core/useConfirm";
import { cleanEmptyArrayParams, stripPaginationParams } from "@/utils/query";
import type { IObject } from "@/components/modal/types";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue";
@@ -143,9 +148,7 @@ type ParamSearchForm = {
};
function normalizeParamQuery(params: Record<string, unknown>): ConfigPageQuery {
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;
const p = cleanEmptyArrayParams({ ...params });
if (p.config_type === "true" || p.config_type === true) p.config_type = true;
else if (p.config_type === "false" || p.config_type === false) p.config_type = false;
return p as unknown as ConfigPageQuery;
@@ -223,15 +226,129 @@ const paramSearchItems = computed<SearchFormItem[]>(() => [
]);
const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);
const selectedRows = ref<ConfigTable[]>([]);
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: ConfigTable[]) {
selectedRows.value = rows;
}
// ─── 表格多选 ───
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } =
useTableSelection<ConfigTable>();
// ─── 对话框状态 ───
const { dialogVisible } = useCrudDialog();
const detailFormData = ref<ConfigTable>({} as ConfigTable);
const paramDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "配置名称", prop: "config_name" },
{
label: "系统内置",
prop: "config_type",
tag: {
map: { true: { type: "success", text: "是" }, false: { type: "danger", text: "否" } },
},
},
{ label: "配置键", prop: "config_key" },
{ label: "配置值", prop: "config_value" },
{ label: "描述", prop: "description" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
];
const formData = ref<ConfigForm>({
id: undefined,
config_name: "",
config_key: "",
config_value: "",
config_type: false,
description: "",
});
const rules = reactive({
config_name: [{ required: true, message: "请输入系统配置名称", trigger: "blur" }],
config_key: [{ required: true, message: "请输入系统配置键", trigger: "blur" }],
config_value: [{ required: true, message: "请输入系统配置值", trigger: "blur" }],
config_type: [{ required: true, message: "请选择系统配置类型", trigger: "blur" }],
});
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const paramFormRenderKey = ref(0);
const initialFormData: ConfigForm = {
id: undefined,
config_name: "",
config_key: "",
config_value: "",
config_type: false,
description: "",
};
// ─── CRUD 表单 ───
const { submitLoading, handleCloseDialog, handleOpenDialog, handleSubmit } =
useCrudForm<ConfigForm>({
formData,
initialFormData,
dialogVisible,
dataFormRef,
formRenderKey: paramFormRenderKey,
detailApi: ParamsAPI.detailParams,
createApi: ParamsAPI.createParams,
updateApi: ParamsAPI.updateParams,
titles: { create: "新增系统配置", update: "修改系统配置", detail: "系统配置详情" },
detailFormData,
onCreateSuccess: async () => {
await refreshCreate();
},
onUpdateSuccess: async () => {
await refreshUpdate();
},
onSubmitSuccess: async () => {
configStore.isConfigLoaded = false;
await configStore.getConfig();
},
});
const paramDialogFormItems = computed<FormItem[]>(() => [
{
label: "配置名称",
key: "config_name",
type: "input",
span: 24,
props: { placeholder: "请输入配置名称", maxlength: 50 },
},
{
label: "配置键",
key: "config_key",
type: "input",
span: 24,
props: { placeholder: "请输入配置键", maxlength: 50 },
},
{
label: "配置值",
key: "config_value",
type: "input",
span: 24,
props: { placeholder: "请输入配置值", maxlength: 100 },
},
{
label: "系统内置",
key: "config_type",
type: "input",
span: 24,
placeholder: "",
},
{
label: "描述",
key: "description",
type: "input",
span: 24,
props: {
type: "textarea",
rows: 4,
maxlength: 100,
showWordLimit: true,
placeholder: "请输入描述",
},
},
]);
const {
columns,
@@ -299,11 +416,7 @@ const paramCrudCols = 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 sp = stripPaginationParams(searchParams as Record<string, unknown>);
return normalizeParamQuery(sp);
});
@@ -320,104 +433,6 @@ const paramExportContentConfig = computed(() => ({
},
}));
const detailFormData = ref<ConfigTable>({} as ConfigTable);
const paramDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "配置名称", prop: "config_name" },
{
label: "系统内置",
prop: "config_type",
tag: {
map: { true: { type: "success", text: "是" }, false: { type: "danger", text: "否" } },
},
},
{ label: "配置键", prop: "config_key" },
{ label: "配置值", prop: "config_value" },
{ label: "描述", prop: "description" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
];
const formData = ref<ConfigForm>({
id: undefined,
config_name: "",
config_key: "",
config_value: "",
config_type: false,
description: "",
});
const dialogVisible = reactive({
title: "",
visible: false,
type: "create" as "create" | "update" | "detail",
});
const rules = reactive({
config_name: [{ required: true, message: "请输入系统配置名称", trigger: "blur" }],
config_key: [{ required: true, message: "请输入系统配置键", trigger: "blur" }],
config_value: [{ required: true, message: "请输入系统配置值", trigger: "blur" }],
config_type: [{ required: true, message: "请选择系统配置类型", trigger: "blur" }],
});
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const paramFormRenderKey = ref(0);
const paramDialogFormItems = computed<FormItem[]>(() => [
{
label: "配置名称",
key: "config_name",
type: "input",
span: 24,
props: { placeholder: "请输入配置名称", maxlength: 50 },
},
{
label: "配置键",
key: "config_key",
type: "input",
span: 24,
props: { placeholder: "请输入配置键", maxlength: 50 },
},
{
label: "配置值",
key: "config_value",
type: "input",
span: 24,
props: { placeholder: "请输入配置值", maxlength: 100 },
},
{
label: "系统内置",
key: "config_type",
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: ConfigForm = {
id: undefined,
config_name: "",
config_key: "",
config_value: "",
config_type: false,
description: "",
};
const { exportVisible, openExport } = useImportExport();
async function handleSearchBarSearch(params: ParamSearchForm) {
@@ -436,70 +451,9 @@ 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 ParamsAPI.detailParams(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;
}
paramFormRenderKey.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 ParamsAPI.updateParams(id, { id, ...formData.value });
await refreshUpdate();
} else {
await ParamsAPI.createParams(formData.value);
await refreshCreate();
}
dialogVisible.visible = false;
await resetForm();
configStore.isConfigLoaded = false;
await configStore.getConfig();
} catch (error: unknown) {
console.error(error);
} finally {
submitLoading.value = false;
}
});
}
async function deleteParamRow(id: number) {
try {
await ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await confirmDelete();
await ParamsAPI.deleteParams([id]);
configStore.isConfigLoaded = false;
await configStore.getConfig();
@@ -556,11 +510,7 @@ 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 ParamsAPI.deleteParams(ids);
configStore.isConfigLoaded = false;
@@ -575,13 +525,3 @@ async function handleBatchDelete() {
}
}
</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%;
}
</style>
@@ -125,6 +125,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 type { IObject } from "@/components/modal/types";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue";
@@ -155,10 +160,7 @@ type PositionSearchForm = {
};
function normalizePositionQuery(params: Record<string, unknown>): PositionPageQuery {
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;
return p as unknown as PositionPageQuery;
return cleanEmptyArrayParams({ ...params }) as unknown as PositionPageQuery;
}
function buildPositionReplaceParams(p: PositionSearchForm): Record<string, unknown> {
@@ -345,33 +347,8 @@ const positionSearchItems = computed<SearchFormItem[]>(() => [
]);
const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);
const selectedRows = ref<PositionTable[]>([]);
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: PositionTable[]) {
selectedRows.value = rows;
}
async function deletePositionRow(id: number) {
try {
await ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await PositionAPI.deletePosition([id]);
await userStore.getUserInfo();
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} catch {
// 用户取消
}
}
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } =
useTableSelection<PositionTable>();
const opCtx = {
onDetail: (id: number) => void handleOpenDialog("detail", id),
@@ -456,12 +433,10 @@ const positionCrudCols = 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;
return normalizePositionQuery(sp) as unknown as Record<string, unknown>;
return normalizePositionQuery(stripPaginationParams(searchParams)) as unknown as Record<
string,
unknown
>;
});
const positionExportContentConfig = computed(() => ({
@@ -505,11 +480,7 @@ const formData = ref<PositionForm>({
description: undefined,
});
const dialogVisible = reactive({
title: "",
visible: false,
type: "create" as "create" | "update" | "detail",
});
const { dialogVisible } = useCrudDialog();
const rules = reactive({
name: [{ required: true, message: "请输入岗位名称", trigger: "blur" }],
@@ -528,6 +499,30 @@ const initialFormData: PositionForm = {
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const positionFormRenderKey = ref(0);
// ─── CRUD 表单 ───
const { submitLoading, handleCloseDialog, handleOpenDialog, handleSubmit } =
useCrudForm<PositionForm>({
formData,
initialFormData,
dialogVisible,
dataFormRef,
formRenderKey: positionFormRenderKey,
detailApi: PositionAPI.detailPosition,
createApi: PositionAPI.createPosition,
updateApi: PositionAPI.updatePosition,
titles: { create: "新增岗位", update: "修改岗位", detail: "岗位详情" },
detailFormData,
onCreateSuccess: async () => {
await refreshCreate();
},
onUpdateSuccess: async () => {
await refreshUpdate();
},
onSubmitSuccess: async () => {
await userStore.getUserInfo();
},
});
const positionDialogFormItems = computed<FormItem[]>(() => [
{
label: "岗位名称",
@@ -564,7 +559,6 @@ const positionDialogFormItems = computed<FormItem[]>(() => [
},
},
]);
const submitLoading = ref(false);
const { exportVisible, openExport } = useImportExport();
async function handleSearchBarSearch(params: PositionSearchForm) {
@@ -594,70 +588,24 @@ 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 PositionAPI.detailPosition(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;
async function deletePositionRow(id: number) {
try {
await confirmDelete();
await PositionAPI.deletePosition([id]);
await userStore.getUserInfo();
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} catch {
// 用户取消
}
positionFormRenderKey.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 PositionAPI.updatePosition(id, { id, ...formData.value });
await refreshUpdate();
} else {
await PositionAPI.createPosition(formData.value);
await refreshCreate();
}
dialogVisible.visible = false;
await resetForm();
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 PositionAPI.deletePosition(ids);
await userStore.getUserInfo();
@@ -678,11 +626,7 @@ async function handleMoreClick(status: string) {
return;
}
try {
await ElMessageBox.confirm(`确认${status === "0" ? "启用" : "停用"}该项数据?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await confirmToggleStatus(status);
await PositionAPI.batchPosition({ ids, status });
await refreshData();
await userStore.getUserInfo();
@@ -693,14 +637,6 @@ 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(.position-table-actions .inline-flex) {
vertical-align: middle;
}
@@ -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>
@@ -96,6 +96,9 @@
<script setup lang="ts">
import { useTable } from "@/hooks/core/useTable";
import { useCrudDialog } from "@/hooks/core/useCrudDialog";
import { useTableSelection } from "@/hooks/core/useTableSelection";
import { confirmDelete, confirmBatchDelete } from "@/hooks/core/useConfirm";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue";
import type { ColumnOption } from "@/types/component";
@@ -308,24 +311,13 @@ const tenantSearchItems = computed<SearchFormItem[]>(() => [
]);
const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);
const selectedRows = ref<TenantTable[]>([]);
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: TenantTable[]) {
selectedRows.value = rows;
}
// ─── 表格多选 ───
const { selectedIds, batchDeleting, onTableSelectionChange } = useTableSelection<TenantTable>();
async function deleteTenantRow(id: number) {
try {
await ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await confirmDelete();
await TenantAPI.deleteTenant([id]);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
@@ -413,8 +405,6 @@ const tenantDetailItems: import("@/components/others/fa-descriptions/index.vue")
{ label: "创建时间", prop: "created_time" },
];
const currentEditId = ref<number | null>(null);
const formData = ref<TenantForm>({
name: "",
code: "",
@@ -424,15 +414,12 @@ const formData = ref<TenantForm>({
end_time: undefined,
});
const dialogVisible = reactive({
title: "",
visible: false,
type: "create" as "create" | "update" | "detail",
});
// ─── 对话框状态 ───
const { dialogVisible } = useCrudDialog();
const CODE_PATTERN = /^[A-Za-z0-9]+$/;
const validateTimeRange = (rule: unknown, value: unknown, callback: (e?: Error) => void) => {
const validateTimeRange = (_rule: unknown, _value: unknown, callback: (e?: Error) => void) => {
if (
formData.value.start_time &&
formData.value.end_time &&
@@ -470,6 +457,33 @@ const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const submitLoading = ref(false);
const tenantFormRenderKey = ref(0);
async function handleOpenDialog(type: "create" | "update" | "detail", id?: number) {
dialogVisible.type = type;
if (id) {
const response = await TenantAPI.detailTenant(id);
if (type === "detail") {
dialogVisible.title = "租户详情";
Object.assign(detailFormData.value, response.data.data);
} else if (type === "update") {
dialogVisible.title = "修改租户";
Object.assign(formData.value, response.data.data);
}
} else {
dialogVisible.title = "新增租户";
Object.assign(formData.value, initialFormData);
formData.value.id = undefined;
}
tenantFormRenderKey.value += 1;
dialogVisible.visible = true;
}
async function handleCloseDialog() {
dialogVisible.visible = false;
dataFormRef.value?.resetFields();
dataFormRef.value?.clearValidate();
Object.assign(formData.value, initialFormData);
}
const tenantDialogFormItems = computed<FormItem[]>(() => [
{
label: "租户名称",
@@ -541,38 +555,6 @@ const tenantDialogFormItems = computed<FormItem[]>(() => [
},
]);
async function resetForm() {
dataFormRef.value?.resetFields();
dataFormRef.value?.clearValidate();
Object.assign(formData, initialFormData);
currentEditId.value = null;
}
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 TenantAPI.detailTenant(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);
currentEditId.value = id;
}
} else {
dialogVisible.title = "新增租户";
await resetForm();
}
tenantFormRenderKey.value += 1;
dialogVisible.visible = true;
}
async function handleSearchBarSearch(params: TenantSearchForm) {
await searchBarRef.value?.validate?.();
replaceSearchParams(buildTenantReplaceParams(params));
@@ -590,48 +572,45 @@ function onResetSearch() {
}
async function handleSubmit() {
dataFormRef.value?.validate(async (valid: boolean) => {
if (!valid) return;
submitLoading.value = true;
const id = currentEditId.value;
try {
if (id) {
const payload: TenantUpdateForm = {
name: formData.value.name,
start_time: formData.value.start_time,
end_time: formData.value.end_time,
};
await TenantAPI.updateTenant(id, payload);
await refreshUpdate();
} else {
const payload: TenantCreateForm = {
name: formData.value.name as string,
code: formData.value.code as string,
start_time: formData.value.start_time,
end_time: formData.value.end_time,
};
await TenantAPI.createTenant(payload);
await refreshCreate();
}
dialogVisible.visible = false;
await resetForm();
} catch (error: unknown) {
console.error(error);
} finally {
submitLoading.value = false;
const valid = await dataFormRef.value!.validate().catch(() => false);
if (!valid) return;
submitLoading.value = true;
const id = formData.value.id as number | undefined;
try {
if (id) {
const payload: TenantUpdateForm = {
name: formData.value.name,
start_time: formData.value.start_time,
end_time: formData.value.end_time,
};
await TenantAPI.updateTenant(id, payload);
await refreshUpdate();
} else {
const payload: TenantCreateForm = {
name: formData.value.name as string,
code: formData.value.code as string,
start_time: formData.value.start_time,
end_time: formData.value.end_time,
};
await TenantAPI.createTenant(payload);
await refreshCreate();
}
});
dialogVisible.visible = false;
dataFormRef.value?.resetFields();
dataFormRef.value?.clearValidate();
Object.assign(formData.value, initialFormData);
} 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 TenantAPI.deleteTenant(ids);
ElMessage.success("删除成功");
@@ -646,15 +625,7 @@ async function handleBatchDelete() {
</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(.tenant-table-actions .inline-flex) {
::deep(.tenant-table-actions .inline-flex) {
vertical-align: middle;
}
</style>
@@ -210,11 +210,16 @@ defineOptions({
});
import { UserFilled } from "@element-plus/icons-vue";
import { ElAvatar } from "element-plus";
import { useAppStore } from "@stores/modules/app.store";
import { DeviceEnum } from "@/enums/settings/device.enum";
import { ResultEnum } from "@/enums/api/result.enum";
import { useTable } from "@/hooks/core/useTable";
import { useImportExport } from "@/hooks/core/useImportExport";
import { useTableSelection } from "@/hooks/core/useTableSelection";
import { useCrudDialog } from "@/hooks/core/useCrudDialog";
import { confirmDelete, confirmBatchDelete, confirmToggleStatus } from "@/hooks/core/useConfirm";
import { cleanEmptyArrayParams, stripPaginationParams } from "@/utils/query";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue";
import type { IContentConfig, IObject } from "@/components/modal/types";
@@ -230,7 +235,6 @@ import RoleAPI from "@/api/module_system/role";
import DeptTree from "./components/DeptTree.vue";
import UserTableSelect from "./components/UserTableSelect.vue";
import { useUserStore } from "@stores";
import { ElMessage, ElMessageBox, ElTag, ElAvatar } from "element-plus";
import type { DescriptionsItem } from "@/components/others/fa-descriptions/index.vue";
import { useAuth } from "@/hooks/core/useAuth";
import type { ColumnOption } from "@/types/component";
@@ -248,12 +252,6 @@ type UserSearchForm = {
created_time?: string[];
};
function normalizeUserQuery(params: Record<string, unknown>): UserPageQuery {
const p = { ...params } as Record<string, unknown>;
if (Array.isArray(p.created_time) && p.created_time.length === 0) p.created_time = undefined;
return p as unknown as UserPageQuery;
}
function buildUserReplaceParams(u: UserSearchForm): Record<string, unknown> {
return {
username: u.username,
@@ -344,7 +342,6 @@ const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const userFormRenderKey = ref(0);
const submitLoading = ref(false);
const uploadLoading = ref(false);
const batchDeleting = ref(false);
const deptFilterId = ref<string | number | undefined>(undefined);
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "450px" : "90%"));
@@ -527,14 +524,8 @@ const userSearchItems = computed<SearchFormItem[]>(() => [
]);
const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);
const selectedRows = ref<UserInfo[]>([]);
const selectedIds = computed(() =>
selectedRows.value.map((r) => r.id).filter((id): id is number => id != null && !Number.isNaN(id))
);
function onTableSelectionChange(rows: UserInfo[]) {
selectedRows.value = rows;
}
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } =
useTableSelection<UserInfo>();
async function handleResetPassword(row: UserInfo) {
try {
@@ -556,11 +547,7 @@ async function handleResetPassword(row: UserInfo) {
async function deleteUserRow(id: number) {
try {
await ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await confirmDelete();
await UserAPI.deleteUser([id]);
const idSet = [id];
if (userStore.basicInfo.id && idSet.includes(userStore.basicInfo.id)) {
@@ -677,26 +664,22 @@ const userCrudCols = 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 sp = stripPaginationParams(searchParams);
if (
deptFilterId.value !== undefined &&
deptFilterId.value !== null &&
deptFilterId.value !== ""
) {
sp.dept_id = Number(deptFilterId.value);
(sp as Record<string, unknown>).dept_id = Number(deptFilterId.value);
}
const q = normalizeUserQuery(sp);
const q = cleanEmptyArrayParams(sp) as Record<string, unknown>;
if (typeof q.status === "string") {
const s = q.status;
if (s === "true" || s === "false") {
(q as unknown as Record<string, unknown>).status = s === "true";
q.status = s === "true";
}
}
return q as unknown as Record<string, unknown>;
return q;
});
const userImportContentConfig = computed<IContentConfig>(() => ({
@@ -710,14 +693,14 @@ const userExportContentConfig = computed(() => ({
permPrefix: "module_system:user",
cols: userCrudCols.value,
exportsBlobAction: async (params: IObject) => {
const merged = normalizeUserQuery({
const merged = cleanEmptyArrayParams({
...(exportQueryParams.value as Record<string, unknown>),
...params,
} as Record<string, unknown>);
} as Record<string, unknown>) 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";
merged.status = s === "true";
}
}
const res = await UserAPI.exportUser(merged as unknown as UserPageQuery);
@@ -744,11 +727,7 @@ const formData = ref<UserForm>({
description: undefined,
});
const dialogVisible = reactive({
title: "",
visible: false,
type: "create" as "create" | "update" | "detail",
});
const { dialogVisible } = useCrudDialog();
const rules = reactive({
username: [{ required: true, message: "请输入账号", trigger: "blur" }],
@@ -849,7 +828,7 @@ async function resetForm() {
dataFormRef.value.resetFields();
dataFormRef.value.clearValidate();
}
Object.assign(formData, initialFormData);
Object.assign(formData.value, initialFormData);
}
async function handleCloseDialog() {
@@ -940,11 +919,7 @@ 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 UserAPI.deleteUser(ids);
if (userStore.basicInfo.id && ids.includes(userStore.basicInfo.id)) {
@@ -968,11 +943,7 @@ async function handleMoreClick(status: string) {
return;
}
try {
await ElMessageBox.confirm("确认启用或停用该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await confirmToggleStatus(status);
batchDeleting.value = true;
await UserAPI.batchUser({ ids, status });
await refreshData();