<template>
|
<div>
|
<div class="toolbar">
|
<el-button v-permission="'sys:depts:create'" type="primary" @click="openAddRoot">新增一级部门</el-button>
|
</div>
|
|
<el-table
|
:data="treeData"
|
row-key="id"
|
:tree-props="{ children: 'children' }"
|
:expand-row-keys="expandedRowKeys"
|
v-loading="loading"
|
border
|
stripe
|
@expand-change="handleExpandChange"
|
>
|
<el-table-column prop="deptName" label="部门名称" min-width="280">
|
<template #default="{ row }">
|
<span>{{ row.deptName }}</span>
|
<el-tag v-if="row.status === 0" type="info" size="small" class="st-tag">禁用</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="260" fixed="right">
|
<template #default="{ row }">
|
<div class="table-ops">
|
<el-button
|
v-permission="'sys:depts:create'"
|
link
|
type="primary"
|
size="small"
|
:disabled="row.level >= 5"
|
@click="openAddChild(row)"
|
>
|
添加下级
|
</el-button>
|
<el-button v-permission="'sys:depts:update'" link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
|
<el-button v-permission="'sys:depts:delete'" link type="primary" size="small" @click="confirmDelete(row)">删除</el-button>
|
</div>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<el-dialog v-model="dlg" :title="dlgTitle" width="480px" destroy-on-close @closed="resetDlg">
|
<el-form label-width="100px">
|
<el-form-item v-if="showParentReadonly" label="上级部门">
|
<el-input :model-value="parentDeptName" disabled />
|
</el-form-item>
|
<el-form-item v-if="showParentSelect" label="上级部门" required>
|
<el-tree-select
|
v-model="form.parentId"
|
:data="parentTreeSelectData"
|
check-strictly
|
:render-after-expand="false"
|
placeholder="请选择上级部门"
|
clearable
|
class="tree-sel"
|
/>
|
</el-form-item>
|
<el-form-item label="部门名称" required>
|
<el-input v-model="form.deptName" maxlength="128" placeholder="请输入" />
|
</el-form-item>
|
<el-form-item label="部门状态" required>
|
<el-radio-group v-model="form.status">
|
<el-radio :label="1">启用</el-radio>
|
<el-radio :label="0">禁用</el-radio>
|
</el-radio-group>
|
</el-form-item>
|
</el-form>
|
<template #footer>
|
<el-button @click="dlg = false">取消</el-button>
|
<el-button
|
v-permission="mode === 'edit' ? 'sys:depts:update' : 'sys:depts:create'"
|
type="primary"
|
:loading="saving"
|
@click="submitDlg"
|
>
|
保存
|
</el-button>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup lang="ts">
|
import { computed, ref } from "vue";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
import http from "../api/http";
|
|
type DeptNode = {
|
id: number;
|
uuid: string;
|
parentId?: number;
|
deptName: string;
|
sortOrder?: number;
|
status: number;
|
level: number;
|
children?: DeptNode[];
|
};
|
|
type TreeSelNode = {
|
value: number;
|
label: string;
|
disabled?: boolean;
|
children?: TreeSelNode[];
|
};
|
|
const loading = ref(false);
|
const saving = ref(false);
|
const treeData = ref<DeptNode[]>([]);
|
const expandedRowKeys = ref<string[]>([]);
|
const dlg = ref(false);
|
type DlgMode = "root" | "child" | "edit";
|
const mode = ref<DlgMode>("root");
|
const editingId = ref<number | null>(null);
|
const editingIsRoot = ref(false);
|
const parentDeptName = ref("");
|
const childParentId = ref<number | null>(null);
|
const pendingExpandRowKey = ref<string | null>(null);
|
|
const form = ref({
|
uuid: "",
|
deptName: "",
|
status: 1,
|
parentId: undefined as number | undefined,
|
});
|
|
const dlgTitle = computed(() => {
|
if (mode.value === "root") return "新增一级部门";
|
if (mode.value === "child") return "添加下级部门";
|
return "编辑部门";
|
});
|
|
const showParentReadonly = computed(() => mode.value === "child");
|
const showParentSelect = computed(() => mode.value === "edit" && !editingIsRoot.value);
|
|
const parentTreeSelectData = ref<TreeSelNode[]>([]);
|
|
function collectDescendantIds(n: DeptNode): number[] {
|
const r: number[] = [n.id];
|
for (const c of n.children || []) {
|
r.push(...collectDescendantIds(c));
|
}
|
return r;
|
}
|
|
function toTreeSelectData(nodes: DeptNode[], disabledIds: Set<number>): TreeSelNode[] {
|
return nodes.map((n) => ({
|
value: n.id,
|
label: n.deptName + (n.status === 0 ? "(禁用)" : ""),
|
disabled: disabledIds.has(n.id),
|
children: n.children?.length ? toTreeSelectData(n.children, disabledIds) : undefined,
|
}));
|
}
|
|
function toRowKey(id?: number | string | null): string | null {
|
return id == null ? null : String(id);
|
}
|
|
function collectTreeIds(nodes: DeptNode[]): string[] {
|
const ids: string[] = [];
|
for (const node of nodes) {
|
ids.push(String(node.id));
|
if (node.children?.length) {
|
ids.push(...collectTreeIds(node.children));
|
}
|
}
|
return ids;
|
}
|
|
function collectDescendantRowKeys(node: DeptNode): string[] {
|
const keys: string[] = [];
|
for (const child of node.children || []) {
|
keys.push(String(child.id));
|
keys.push(...collectDescendantRowKeys(child));
|
}
|
return keys;
|
}
|
|
function ensureExpandedRow(id?: number | string | null) {
|
const rowKey = toRowKey(id);
|
if (!rowKey || expandedRowKeys.value.includes(rowKey)) return;
|
expandedRowKeys.value = [...expandedRowKeys.value, rowKey];
|
}
|
|
async function loadTree() {
|
loading.value = true;
|
try {
|
const { data } = await http.get("/sys/dept-manage/tree");
|
treeData.value = (data.result || []) as DeptNode[];
|
const validIds = new Set(collectTreeIds(treeData.value));
|
expandedRowKeys.value = expandedRowKeys.value.filter(id => validIds.has(id));
|
if (pendingExpandRowKey.value != null && validIds.has(pendingExpandRowKey.value)) {
|
ensureExpandedRow(pendingExpandRowKey.value);
|
}
|
pendingExpandRowKey.value = null;
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "加载失败");
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
function handleExpandChange(row: DeptNode, expanded: boolean) {
|
const rowKey = String(row.id);
|
if (expanded) {
|
ensureExpandedRow(rowKey);
|
return;
|
}
|
const collapsedKeys = new Set([rowKey, ...collectDescendantRowKeys(row)]);
|
expandedRowKeys.value = expandedRowKeys.value.filter(id => !collapsedKeys.has(id));
|
}
|
|
function openAddRoot() {
|
mode.value = "root";
|
editingId.value = null;
|
editingIsRoot.value = true;
|
parentDeptName.value = "";
|
childParentId.value = null;
|
form.value = { uuid: "", deptName: "", status: 1, parentId: undefined };
|
dlg.value = true;
|
}
|
|
function openAddChild(row: DeptNode) {
|
if (row.level >= 5) return;
|
mode.value = "child";
|
editingId.value = null;
|
editingIsRoot.value = false;
|
parentDeptName.value = row.deptName;
|
childParentId.value = row.id;
|
form.value = { uuid: "", deptName: "", status: 1, parentId: row.id };
|
dlg.value = true;
|
}
|
|
function openEdit(row: DeptNode) {
|
mode.value = "edit";
|
editingId.value = row.id;
|
const root = !row.parentId || row.parentId === 0;
|
editingIsRoot.value = root;
|
parentDeptName.value = "";
|
childParentId.value = null;
|
form.value = {
|
uuid: row.uuid,
|
deptName: row.deptName,
|
status: row.status,
|
parentId: root ? undefined : row.parentId,
|
};
|
if (!root) {
|
const disabled = new Set(collectDescendantIds(row));
|
parentTreeSelectData.value = toTreeSelectData(treeData.value, disabled);
|
} else {
|
parentTreeSelectData.value = [];
|
}
|
dlg.value = true;
|
}
|
|
function resetDlg() {
|
editingId.value = null;
|
parentTreeSelectData.value = [];
|
}
|
|
async function submitDlg() {
|
if (!form.value.deptName.trim()) {
|
ElMessage.warning("请填写部门名称");
|
return;
|
}
|
saving.value = true;
|
try {
|
if (mode.value === "root") {
|
await http.post("/sys/dept-manage", {
|
parentId: 0,
|
deptName: form.value.deptName.trim(),
|
status: form.value.status,
|
});
|
ElMessage.success("已创建");
|
} else if (mode.value === "child") {
|
if (childParentId.value == null) {
|
ElMessage.warning("缺少上级部门");
|
return;
|
}
|
pendingExpandRowKey.value = toRowKey(childParentId.value);
|
await http.post("/sys/dept-manage", {
|
parentId: childParentId.value,
|
deptName: form.value.deptName.trim(),
|
status: form.value.status,
|
});
|
ElMessage.success("已创建");
|
} else if (editingId.value != null) {
|
const body: Record<string, unknown> = {
|
uuid: form.value.uuid,
|
deptName: form.value.deptName.trim(),
|
status: form.value.status,
|
};
|
if (!editingIsRoot.value) {
|
if (form.value.parentId == null) {
|
ElMessage.warning("请选择上级部门");
|
return;
|
}
|
body.parentId = form.value.parentId;
|
}
|
await http.put(`/sys/dept-manage/${editingId.value}`, body);
|
ElMessage.success("已保存");
|
}
|
dlg.value = false;
|
loadTree();
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "保存失败");
|
} finally {
|
saving.value = false;
|
}
|
}
|
|
function confirmDelete(row: DeptNode) {
|
ElMessageBox.confirm("删除为逻辑删除,是否继续?", "确认删除", {
|
type: "warning",
|
confirmButtonText: "删除",
|
cancelButtonText: "取消",
|
})
|
.then(async () => {
|
try {
|
await http.delete(`/sys/dept-manage/${row.id}`, { params: { uuid: row.uuid } });
|
ElMessage.success("已删除");
|
loadTree();
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "删除失败");
|
}
|
})
|
.catch(() => {});
|
}
|
|
loadTree();
|
</script>
|
|
<style scoped>
|
.toolbar {
|
display: flex;
|
justify-content: flex-end;
|
margin-bottom: 12px;
|
}
|
.st-tag {
|
margin-left: 8px;
|
vertical-align: middle;
|
}
|
.tree-sel {
|
width: 100%;
|
}
|
</style>
|