Files
FastapiAdmin/frontend/web/src/views/module_system/dept/index.vue
T
zhangtao 0ce31936aa chore: 清理冗余代码与配置,优化项目结构
1.  删除无用文件与废弃代码:移除locale枚举、element-plus插件、sse路由、api token模块等
2.  简化类型导入与依赖:移除大量未使用的类型导入,统一echarts导入方式
3.  优化配置与样式:调整gitignore、样式引入顺序,新增列表动画样式
4.  修复接口与模型:修正接口返回类型、查询参数配置,更新部门模型字段
5.  优化性能与体验:添加图片懒加载,优化加载逻辑与表格渲染
6.  调整环境配置:新增并更新开发/生产环境配置文件
2026-07-23 20:49:52 +08:00

549 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 部门配置:FA + 树形表格(对齐 system/menu 模版) -->
<template>
<div class="fa-full-height">
<FaSearchBar
v-show="showSearchBar"
ref="searchBarRef"
v-model="searchForm"
:items="deptSearchItems"
:rules="searchBarRules"
:is-expand="false"
:show-expand="true"
:show-reset="true"
:show-search="true"
:disabled-search="false"
:default-expanded="false"
include-audit
@search="handleSearchBarSearch"
@reset="onResetSearch"
/>
<ElCard class="fa-table-card" :style="{ 'margin-top': showSearchBar ? '12px' : '0' }">
<FaTableHeader
v-model:columns="columnChecks"
v-model:showSearchBar="showSearchBar"
:loading="loading"
@refresh="loadDeptData"
>
<template #left>
<div class="inline-flex flex-wrap items-center gap-2">
<FaTableHeaderLeft
:remove-ids="selectedIds"
:perm-create="['module_system:dept:create']"
:perm-delete="['module_system:dept:delete']"
:perm-patch="['module_system:dept:patch']"
:delete-loading="batchDeleting"
:create-loading="createLoading"
:more-loading="moreLoading"
@add="handleAdd"
@delete="handleBatchDelete"
@more="handleMoreClick"
/>
<ElButton @click="toggleExpand" v-ripple>{{ isExpanded ? "收起" : "展开" }}</ElButton>
</div>
</template>
</FaTableHeader>
<FaTable
ref="tableRef"
row-key="id"
:loading="loading"
:columns="columns"
:data="tableData"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:default-expand-all="false"
@selection-change="onTableSelectionChange"
/>
</ElCard>
<FaDialog
v-model="dialogVisible.visible"
:title="dialogVisible.title"
width="640px"
dialog-class="crud-embed-dialog"
modal-class="crud-embed-dialog"
:form-mode="dialogVisible.type"
:confirm-loading="submitLoading"
@cancel="handleCloseDialog"
@confirm="dialogVisible.type === 'detail' ? handleCloseDialog() : handleSubmit()"
>
<template v-if="dialogVisible.type === 'detail'">
<FaDescriptions
:column="4"
:data="detailFormData"
:items="deptDetailItems"
max-height="75vh"
/>
</template>
<template v-else>
<FaForm
:key="deptFormRenderKey"
scrollbar
max-height="75vh"
ref="dataFormRef"
v-model="formData"
:items="deptDialogFormItems"
:rules="rules"
label-suffix=":"
:label-width="100"
label-position="right"
:span="24"
:gutter="16"
:show-reset="false"
:show-submit="false"
class="crud-dialog-art-form"
>
<template #status>
<ElRadioGroup v-model="formData.status">
<ElRadio :value="0">启用</ElRadio>
<ElRadio :value="1">停用</ElRadio>
</ElRadioGroup>
</template>
</FaForm>
</template>
</FaDialog>
</div>
</template>
<script setup lang="ts">
import { useTableColumns } from "@/hooks/core/useTableColumns";
import { useCrudForm } from "@/hooks/core/useCrudForm";
import { confirmToggleStatus } from "@/hooks/core/useConfirm";
import DeptAPI, {
type DeptForm,
type DeptPageQuery,
type DeptTable,
} from "@/api/module_system/dept";
import { useUserStore } from "@stores";
import { formatTree, renderTableOperationCell, type TableOperationAction } from "@utils";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue";
import type FaForm from "@/components/forms/fa-form/index.vue";
import { ElMessage } from "@/utils/message";
defineOptions({
name: "Dept",
inheritAttrs: false,
});
const { hasAuth } = useAuth();
const userStore = useUserStore();
type DeptSearchForm = {
name?: string;
status?: number;
created_time?: string[];
};
function buildDeptQuery(p: DeptSearchForm): DeptPageQuery {
return {
name: p.name,
status: p.status,
created_time:
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
};
}
function buildDeptRowActions(
row: DeptTable,
ctx: {
onAddChild: (parentId: number) => void;
onDetail: (id: number) => void;
onEdit: (id: number) => void;
onDelete: (id: number, name: string) => void;
}
): TableOperationAction[] {
const all: TableOperationAction[] = [
{
key: "add",
label: "新增",
artType: "add",
perm: "module_system:dept:create",
run: () => ctx.onAddChild(row.id!),
},
{
key: "detail",
label: "详情",
artType: "view",
perm: "module_system:dept:detail",
run: () => ctx.onDetail(row.id!),
},
{
key: "edit",
label: "编辑",
artType: "edit",
perm: "module_system:dept:update",
run: () => ctx.onEdit(row.id!),
},
{
key: "delete",
label: "删除",
artType: "delete",
perm: "module_system:dept:delete",
run: () => ctx.onDelete(row.id!, row.name ?? ""),
},
];
return all.filter((a) => a.perm != null && hasAuth(a.perm));
}
function formatDeptOperationCell(row: DeptTable, ctx: Parameters<typeof buildDeptRowActions>[1]) {
const actions = buildDeptRowActions(row, ctx);
return renderTableOperationCell(actions, {
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 dept-table-actions",
});
}
const searchForm = ref<DeptSearchForm>({
name: undefined,
status: undefined,
created_time: undefined,
});
const showSearchBar = ref(true);
const searchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
const searchBarRules: Record<string, unknown> = {};
const statusOptions = ref([
{ label: "启用", value: 0 },
{ label: "停用", value: 1 },
]);
const deptSearchItems = computed<SearchFormItem[]>(() => [
{
label: "部门名称",
key: "name",
type: "input",
placeholder: "请输入部门名称",
clearable: true,
span: 6,
},
{
label: "状态",
key: "status",
type: "select",
props: {
placeholder: "请选择状态",
options: statusOptions.value,
clearable: true,
},
span: 6,
},
]);
const tableRef = ref<{
elTableRef?: { toggleRowExpansion: (row: DeptTable, expanded?: boolean) => void };
} | null>(null);
const tableData = ref<DeptTable[]>([]);
const loading = ref(false);
const isExpanded = ref(false);
const deptOptions = ref<OptionType[]>([]);
// ─── 表格多选 ───
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } =
useTableSelection<DeptTable>();
const createLoading = ref(false);
const moreLoading = ref(false);
async function loadDeptData() {
loading.value = true;
try {
const res = await DeptAPI.listDept(buildDeptQuery(searchForm.value));
const tree = res.data.data || [];
tableData.value = tree;
deptOptions.value = formatTree(tree);
} catch (e: unknown) {
if (import.meta.env.DEV) console.error(e);
} finally {
loading.value = false;
}
}
async function deleteDeptRow(id: number, name: string) {
try {
await confirmDelete(`确定删除「${name}」吗?`);
await DeptAPI.deleteDept([id]);
await userStore.getUserInfo();
selectedRows.value = [];
await loadDeptData();
} catch {
// 用户取消
}
}
// ─── 对话框状态 ───
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: "created_by.name" },
{ label: "更新人", prop: "updated_by.name" },
{ 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();
},
});
async function handleAdd() {
createLoading.value = true;
try {
await handleOpenDialog("create");
} finally {
createLoading.value = false;
}
}
const opCtx = {
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,
};
const { columnChecks, columns } = useTableColumns<DeptTable>(
resolveStatusColumns(() => [
{ type: "selection", width: 48, fixed: "left" },
{ type: "globalIndex", width: 56, label: "序号" },
{ prop: "name", label: "部门名称", minWidth: 120, showOverflowTooltip: true },
{ prop: "code", label: "部门编码", minWidth: 120, showOverflowTooltip: true },
{
prop: "status",
label: "状态",
width: 88,
status: {
0: { type: "success", text: "启用" },
1: { type: "danger", text: "停用" },
},
},
{ prop: "order", label: "排序", width: 88, showOverflowTooltip: true },
{ prop: "description", label: "描述", minWidth: 100, showOverflowTooltip: true },
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
{ prop: "updated_time", label: "更新时间", width: 168, showOverflowTooltip: true },
{
prop: "operation",
label: "操作",
width: 220,
fixed: "right",
align: "center",
formatter: (row: DeptTable) => formatDeptOperationCell(row, opCtx),
},
])
);
const deptDialogFormItems = computed<FormItem[]>(() => [
{
label: "部门名称",
key: "name",
type: "input",
span: 24,
props: { placeholder: "请输入部门名称", maxlength: 50 },
},
{
label: "部门编码",
key: "code",
type: "input",
span: 24,
props: {
placeholder: "字母开头,2-16位字母/数字/下划线",
maxlength: 16,
showWordLimit: true,
},
},
{
label: "上级部门",
key: "parent_id",
type: "treeselect",
span: 24,
props: {
placeholder: "请选择上级部门",
data: deptOptions.value,
filterable: true,
checkStrictly: true,
renderAfterExpand: false,
},
},
{
label: "排序",
key: "order",
type: "number",
span: 24,
props: {
controlsPosition: "right",
min: 1,
max: 999,
},
},
{
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: "请输入描述",
},
},
]);
async function handleSearchBarSearch(params: DeptSearchForm) {
await searchBarRef.value?.validate?.();
searchForm.value = { ...params };
await loadDeptData();
}
function onResetSearch() {
searchForm.value = {
name: undefined,
status: undefined,
created_time: undefined,
};
void loadDeptData();
}
async function handleBatchDelete() {
const ids = selectedIds.value;
if (ids.length === 0) return;
try {
await confirmBatchDelete(
ids.length,
selectedRows.value.map((r) => String(r.name ?? r.id))
);
batchDeleting.value = true;
await DeptAPI.deleteDept(ids);
await userStore.getUserInfo();
selectedRows.value = [];
await loadDeptData();
} catch {
// 用户取消
} finally {
batchDeleting.value = false;
}
}
async function handleMoreClick(status: number) {
const ids = selectedIds.value;
if (!ids.length) {
ElMessage.warning("请先选择要操作的数据");
return;
}
try {
await confirmToggleStatus(status);
moreLoading.value = true;
await DeptAPI.batchDept({ ids, status });
await loadDeptData();
await userStore.getUserInfo();
} catch {
// 用户取消或操作失败
} finally {
moreLoading.value = false;
}
}
function toggleExpand() {
isExpanded.value = !isExpanded.value;
nextTick(() => {
const el = tableRef.value?.elTableRef;
if (!el || !tableData.value.length) return;
const walk = (rows: DeptTable[]) => {
rows.forEach((row) => {
if (row.children?.length) {
el.toggleRowExpansion(row, isExpanded.value);
walk(row.children);
}
});
};
walk(tableData.value);
});
}
onMounted(() => {
void loadDeptData();
});
</script>