代码提交

This commit is contained in:
lixiaobang 2026-01-14 13:16:05 +08:00
parent 16350b70db
commit f04d63451d
2 changed files with 612 additions and 67 deletions

View File

@ -0,0 +1,52 @@
import request from '@/utils/request'
// 查询部门列表
export function listDept(query) {
return request({
url: '/system/dept/list',
method: 'get',
params: query
})
}
// 查询部门列表(排除节点)
export function listDeptExcludeChild(deptId) {
return request({
url: '/system/dept/list/exclude/' + deptId,
method: 'get'
})
}
// 查询部门详细
export function getDept(deptId) {
return request({
url: '/system/dept/' + deptId,
method: 'get'
})
}
// 新增部门
export function addDept(data) {
return request({
url: '/system/dept',
method: 'post',
data: data
})
}
// 修改部门
export function updateDept(data) {
return request({
url: '/system/dept',
method: 'put',
data: data
})
}
// 删除部门
export function delDept(deptId) {
return request({
url: '/system/dept/' + deptId,
method: 'delete'
})
}

View File

@ -4,11 +4,20 @@
<div class="MainBox"> <div class="MainBox">
<!-- 顶部操作栏 --> <!-- 顶部操作栏 -->
<div class="top-bar"> <div class="top-bar">
<el-button type="primary" icon="Plus">新增层级节点</el-button> <el-button type="primary" icon="Plus" @click="handleAdd">新增层级节点</el-button>
<el-button icon="Upload">批量导入层级</el-button> <el-button icon="Upload">批量导入层级</el-button>
<el-button icon="Download">导出层级结构</el-button> <el-button icon="Download">导出层级结构</el-button>
<el-button icon="Setting">配置层级规则</el-button> <el-button icon="Setting">配置层级规则</el-button>
<el-input placeholder="按地市公司名称搜索..." prefix-icon="Search" class="search-input" /> <el-input
v-model="queryParams.deptName"
placeholder="按地市公司名称搜索..."
prefix-icon="Search"
class="search-input"
clearable
@keyup.enter="handleQuery"
/>
<el-button type="primary" icon="Search" @click="handleQuery">查询</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</div> </div>
<!-- 树形结构卡片 --> <!-- 树形结构卡片 -->
@ -16,7 +25,7 @@
<template #header> <template #header>
<span>单位 / 楼宇树形层级管理</span> <span>单位 / 楼宇树形层级管理</span>
</template> </template>
<el-tree :data="treeData" :props="treeProps" default-expand-all show-line> <el-tree ref="treeRef" v-loading="loading" :data="treeData" :props="treeProps" default-expand-all show-line>
<template #default="{ node, data }"> <template #default="{ node, data }">
<div class="tree-node-content"> <div class="tree-node-content">
<span class="node-icon"> <span class="node-icon">
@ -33,69 +42,99 @@
{{ data.status }} {{ data.status }}
</el-tag> </el-tag>
<span class="node-projects">关联项目: {{ data.projects }}</span> <span class="node-projects">关联项目: {{ data.projects }}</span>
<span class="node-actions" v-if="data.originalData" @click.stop>
<el-button link type="primary" icon="Edit" @click.stop="handleUpdate(data.originalData)" v-hasPermi="['system:dept:edit']">修改</el-button>
<el-button link type="primary" icon="Plus" @click.stop="handleAdd(data.originalData)" v-hasPermi="['system:dept:add']">新增</el-button>
<el-button v-if="data.originalData.parentId != 0" link type="primary" icon="Delete" @click.stop="handleDelete(data.originalData)" v-hasPermi="['system:dept:remove']">删除</el-button>
</span>
</div> </div>
</template> </template>
</el-tree> </el-tree>
</el-card> </el-card>
<!-- 添加或修改层级节点对话框 -->
<el-dialog :title="title" v-model="open" width="600px" append-to-body>
<el-form ref="deptRef" :model="form" :rules="rules" label-width="80px">
<el-row>
<el-col :span="24" v-if="form.parentId !== 0">
<el-form-item label="上级部门" prop="parentId">
<el-tree-select
v-model="form.parentId"
:data="deptOptions"
:props="{ value: 'deptId', label: 'deptName', children: 'children' }"
value-key="deptId"
placeholder="选择上级部门"
check-strictly
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="部门名称" prop="deptName">
<el-input v-model="form.deptName" placeholder="请输入部门名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="显示排序" prop="orderNum">
<el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="负责人" prop="leader">
<el-input v-model="form.leader" placeholder="请输入负责人" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="phone">
<el-input v-model="form.phone" placeholder="请输入联系电话" maxlength="11" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="部门状态">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in sys_normal_disable"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref, onMounted, getCurrentInstance, reactive, toRefs, nextTick } from 'vue'
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from '@/api/treeRatingManagement'
const { proxy } = getCurrentInstance()
const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
//
const treeRef = ref(null)
//
const loading = ref(false)
//
const queryParams = ref({
deptName: undefined
})
// //
const treeData = ref([ const treeData = ref([])
{
label: '南京市供电公司',
type: '单位',
status: '已生效',
projects: 12,
children: [
{
label: '城东片区',
type: '区域',
status: '已生效',
projects: 5,
children: [
{
label: 'A 办公楼',
type: '楼宇',
status: '待审核',
projects: 2
},
{
label: 'B 产业园',
type: '楼宇',
status: '已生效',
projects: 3
}
]
},
{
label: '城西片区',
type: '区域',
status: '已生效',
projects: 7,
children: [
{
label: 'C 写字楼',
type: '楼宇',
status: '已禁用',
projects: 0
}
]
}
]
},
{
label: '苏州市供电公司',
type: '单位',
status: '已生效',
projects: 8
}
])
// //
const treeProps = { const treeProps = {
@ -103,6 +142,25 @@ const treeProps = {
label: 'label' label: 'label'
} }
//
const open = ref(false)
const title = ref("")
const deptOptions = ref([])
//
const data = reactive({
form: {},
rules: {
parentId: [{ required: true, message: "上级部门不能为空", trigger: "blur" }],
deptName: [{ required: true, message: "部门名称不能为空", trigger: "blur" }],
orderNum: [{ required: true, message: "显示排序不能为空", trigger: "blur" }],
email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
phone: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }]
},
})
const { form, rules } = toRefs(data)
// //
const getStatusTagType = (status) => { const getStatusTagType = (status) => {
if (status === '已生效') return 'success' if (status === '已生效') return 'success'
@ -110,58 +168,493 @@ const getStatusTagType = (status) => {
if (status === '已禁用') return 'danger' if (status === '已禁用') return 'danger'
return 'info' return 'info'
} }
/** 查询树形层级列表 */
function getList() {
loading.value = true
listDept(queryParams.value).then(response => {
//
let rawData = []
if (response.data && Array.isArray(response.data)) {
rawData = response.data
} else if (Array.isArray(response)) {
rawData = response
}
// parentId使 handleTree
// children使
let treeStructure = []
if (rawData.length > 0 && rawData[0].parentId !== undefined) {
//
treeStructure = proxy.handleTree(rawData, "deptId", "parentId", "children")
} else {
//
treeStructure = rawData
}
//
treeData.value = convertToTreeData(treeStructure)
loading.value = false
//
nextTick(() => {
expandAllNodes()
})
}).catch(() => {
loading.value = false
})
}
/** 展开所有节点 */
function expandAllNodes() {
if (treeRef.value) {
const nodes = treeRef.value.store.nodesMap
for (let key in nodes) {
nodes[key].expanded = true
}
}
}
/** 将接口数据转换为树形结构格式 */
function convertToTreeData(data) {
if (!Array.isArray(data)) return []
return data.map(item => ({
label: item.deptName || item.name || item.label,
type: item.type || '单位',
status: item.status === '0' ? '已生效' : item.status === '1' ? '已禁用' : (item.status || '已生效'),
projects: item.projects || 0,
originalData: item, //
children: item.children && item.children.length > 0 ? convertToTreeData(item.children) : undefined
}))
}
/** 取消按钮 */
function cancel() {
open.value = false
reset()
}
/** 表单重置 */
function reset() {
form.value = {
deptId: undefined,
parentId: undefined,
deptName: undefined,
orderNum: 0,
leader: undefined,
phone: undefined,
email: undefined,
status: "0"
}
proxy.resetForm("deptRef")
}
/** 新增按钮操作 */
function handleAdd(row) {
reset()
listDept().then(response => {
deptOptions.value = proxy.handleTree(response.data, "deptId")
})
if (row != undefined) {
form.value.parentId = row.deptId
}
open.value = true
title.value = "添加层级节点"
}
/** 修改按钮操作 */
function handleUpdate(row) {
if (!row || !row.deptId) {
proxy.$modal.msgError("数据错误,无法编辑")
return
}
reset()
listDeptExcludeChild(row.deptId).then(response => {
deptOptions.value = proxy.handleTree(response.data, "deptId")
})
getDept(row.deptId).then(response => {
form.value = response.data
open.value = true
title.value = "修改层级节点"
})
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["deptRef"].validate(valid => {
if (valid) {
if (form.value.deptId != undefined) {
updateDept(form.value).then(response => {
proxy.$modal.msgSuccess("修改成功")
open.value = false
getList()
})
} else {
addDept(form.value).then(response => {
proxy.$modal.msgSuccess("新增成功")
open.value = false
getList()
})
}
}
})
}
/** 删除按钮操作 */
function handleDelete(row) {
if (!row || !row.deptId) {
proxy.$modal.msgError("数据错误,无法删除")
return
}
proxy.$modal.confirm('是否确认删除名称为"' + row.deptName + '"的数据项?').then(function() {
return delDept(row.deptId)
}).then(() => {
proxy.$modal.msgSuccess("删除成功")
getList()
}).catch(() => {})
}
/** 搜索按钮操作 */
function handleQuery() {
getList()
}
/** 重置按钮操作 */
function resetQuery() {
queryParams.value = {
deptName: undefined
}
handleQuery()
}
//
onMounted(() => {
getList()
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.MainBox { .MainBox {
padding: 20px; padding: 24px;
height: 100%; height: 100%;
background: #F9FAFB; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: calc(100vh - 84px);
} }
.top-bar { .top-bar {
display: flex; display: flex;
gap: 10px; gap: 12px;
margin-bottom: 20px; margin-bottom: 24px;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
padding: 16px 20px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.search-input { .search-input {
margin-left: auto; margin-left: auto;
width: 300px; width: 320px;
border-radius: 6px;
transition: all 0.3s ease;
:deep(.el-input__wrapper) {
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
&:hover {
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
}
}
}
.el-button {
border-radius: 6px;
font-weight: 500;
transition: all 0.3s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(64, 158, 255, 0.2);
}
} }
} }
.tree-card { .tree-card {
height: calc(100vh - 200px); height: calc(100vh - 240px);
overflow-y: auto; overflow-y: auto;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.3s ease;
background: #ffffff;
&:hover {
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
}
:deep(.el-card__header) {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #ffffff;
font-weight: 600;
font-size: 16px;
padding: 16px 20px;
border-radius: 12px 12px 0 0;
}
:deep(.el-card__body) {
padding: 20px;
}
//
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
// background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 4px;
}
} }
.tree-node-content { .tree-node-content {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 12px;
padding: 8px 0; padding: 10px 12px;
border-radius: 6px;
transition: all 0.3s ease;
flex: 1;
min-width: 0;
&:hover {
background: linear-gradient(90deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.05) 100%);
transform: translateX(4px);
}
.node-icon { .node-icon {
color: #409eff; color: #667eea;
font-size: 16px; font-size: 18px;
flex-shrink: 0;
transition: all 0.3s ease;
.el-icon {
filter: drop-shadow(0 2px 4px rgba(102, 126, 234, 0.3));
}
} }
.node-label { .node-label {
font-weight: 500; font-weight: 600;
min-width: 150px; min-width: 150px;
color: #303133;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.node-type { .node-type {
color: #666; color: #909399;
font-size: 12px; font-size: 12px;
background: #f0f2f5;
padding: 2px 8px;
border-radius: 4px;
flex-shrink: 0;
}
:deep(.el-tag) {
border-radius: 4px;
font-weight: 500;
padding: 2px 10px;
font-size: 12px;
border: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.node-projects { .node-projects {
color: #999; color: #909399;
font-size: 12px; font-size: 12px;
margin-left: auto; margin-left: auto;
background: #f5f7fa;
padding: 4px 10px;
border-radius: 4px;
flex-shrink: 0;
white-space: nowrap;
}
.node-actions {
margin-left: 12px;
display: flex;
gap: 4px;
flex-shrink: 0;
opacity: 0.7;
transition: opacity 0.3s ease;
.tree-node-content:hover & {
opacity: 1;
}
.el-button {
padding: 4px 8px;
font-size: 12px;
border-radius: 4px;
transition: all 0.3s ease;
&:hover {
background: rgba(64, 158, 255, 0.1);
transform: scale(1.05);
}
}
}
}
//
:deep(.el-tree) {
.el-tree-node {
.el-tree-node__content {
height: auto;
min-height: 40px;
padding: 4px 0;
transition: all 0.3s ease;
&:hover {
background: transparent;
}
}
.el-tree-node__expand-icon {
color: #667eea;
font-size: 16px;
transition: all 0.3s ease;
&:hover {
color: #764ba2;
transform: scale(1.1);
}
}
&.is-current > .el-tree-node__content {
background: linear-gradient(90deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.1) 100%);
border-radius: 6px;
}
}
.el-tree-node__children {
padding-left: 20px;
}
}
//
:deep(.el-dialog) {
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
.el-dialog__header {
//background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #ffffff;
padding: 20px 24px;
border-radius: 12px 12px 0 0;
margin-right: 0;
.el-dialog__title {
font-weight: 600;
font-size: 18px;
}
.el-dialog__headerbtn {
.el-dialog__close {
color: #ffffff;
font-size: 20px;
&:hover {
color: rgba(255, 255, 255, 0.8);
}
}
}
}
.el-dialog__body {
padding: 24px;
}
.el-form-item__label {
font-weight: 500;
color: #606266;
}
.el-input, .el-input-number, .el-tree-select {
border-radius: 6px;
:deep(.el-input__wrapper) {
border-radius: 6px;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
}
}
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding-top: 20px;
.el-button {
border-radius: 6px;
padding: 10px 24px;
font-weight: 500;
transition: all 0.3s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
}
}
//
@media (max-width: 768px) {
.MainBox {
padding: 16px;
}
.top-bar {
flex-direction: column;
align-items: stretch;
.search-input {
margin-left: 0;
width: 100%;
}
}
.tree-card {
height: calc(100vh - 300px);
}
.tree-node-content {
flex-wrap: wrap;
gap: 8px;
.node-label {
min-width: 100%;
}
.node-projects {
margin-left: 0;
}
} }
} }
</style> </style>