在企业级后台管理系统中,组织架构管理是最常见的功能之一。想象一下,一个大型企业的部门结构往往非常复杂,总公司下面有多个分公司,分公司下面又有各个部门,部门下面可能还有小组。这种层级关系如果用普通表格展示,用户需要来回切换页面查看不同层级的部门信息,操作体验非常糟糕。
树形表格正好解决了这个问题。它把树形结构的层级关系和表格的数据展示能力完美结合,用户可以通过点击展开/折叠按钮查看任意层级的部门信息,所有数据一目了然。我在去年参与的一个ERP系统开发中就深有体会,当客户看到我们实现的树形部门管理功能时,第一反应就是"这才是我想要的效果"。
Element-UI的el-table组件内置了对树形数据的支持,只要数据格式正确,几乎不需要额外代码就能实现树形展示。这比我们早期用jQuery手写树形表格要方便太多了,那时候光是处理各种展开收起逻辑就要写上百行代码。
首先确保你已经安装了Node.js(建议版本14+),然后通过Vue CLI创建一个新项目:
bash复制npm install -g @vue/cli
vue create tree-table-demo
cd tree-table-demo
选择默认的Vue 2模板即可,我们不需要特别复杂的配置。项目创建完成后,安装Element-UI:
bash复制npm install element-ui
在main.js中引入Element-UI:
javascript复制import Vue from 'vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
在src/views目录下新建DeptManagement.vue文件,我们先搭建一个基础的管理页面框架:
html复制<template>
<div class="app-container">
<!-- 搜索区域 -->
<el-form :inline="true" class="search-form">
<el-form-item label="部门名称">
<el-input placeholder="请输入部门名称"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary">搜索</el-button>
<el-button>重置</el-button>
</el-form-item>
</el-form>
<!-- 操作按钮区 -->
<div class="operation-buttons">
<el-button type="primary" icon="el-icon-plus">新增</el-button>
<el-button icon="el-icon-refresh">刷新</el-button>
</div>
<!-- 表格区域 -->
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="name" label="部门名称"></el-table-column>
<el-table-column prop="status" label="状态"></el-table-column>
<el-table-column prop="createTime" label="创建时间"></el-table-column>
<el-table-column label="操作" width="180">
<template>
<el-button size="mini">编辑</el-button>
<el-button size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
data() {
return {
tableData: []
}
}
}
</script>
<style scoped>
.app-container {
padding: 20px;
}
.search-form {
margin-bottom: 20px;
}
.operation-buttons {
margin-bottom: 20px;
}
</style>
这个基础框架包含了管理系统常见的几个部分:搜索区域、操作按钮区和数据表格。虽然现在还没有实现树形功能,但已经可以正常显示一个普通的部门列表。
要让el-table显示树形结构,数据必须包含层级关系。通常我们会使用包含children属性的对象数组来表示树形数据。一个典型的部门数据结构如下:
javascript复制{
id: '1',
name: '总公司',
status: '正常',
createTime: '2023-01-01',
children: [
{
id: '2',
name: '技术部',
status: '正常',
createTime: '2023-01-15',
children: [
{
id: '3',
name: '前端组',
status: '正常',
createTime: '2023-02-01'
}
]
}
]
}
在实际项目中,这种数据通常由后端API返回。开发阶段我们可以先使用模拟数据:
javascript复制data() {
return {
tableData: [
{
id: '1',
name: '总公司',
status: '正常',
createTime: '2023-01-01',
children: [
{
id: '2',
name: '技术部',
status: '正常',
createTime: '2023-01-15',
children: [
{
id: '3',
name: '前端组',
status: '正常',
createTime: '2023-02-01'
}
]
},
{
id: '4',
name: '市场部',
status: '正常',
createTime: '2023-01-20'
}
]
}
]
}
}
要让el-table识别并正确显示树形数据,需要配置两个关键属性:
修改表格代码如下:
html复制<el-table
:data="tableData"
row-key="id"
:tree-props="{children: 'children', hasChildren: 'hasChildren'}">
<!-- 列定义保持不变 -->
</el-table>
现在刷新页面,你应该能看到一个可以展开折叠的树形表格了。el-table会自动在可展开的行前面添加一个小箭头图标,点击即可展开或折叠子节点。
对于深层级的树形数据,提供一个"展开/折叠全部"的按钮会提升用户体验。实现这个功能需要操作el-table的两个属性:
首先在data中添加控制变量:
javascript复制data() {
return {
isExpandAll: true,
refreshTable: true,
// 其他数据...
}
}
然后添加展开/折叠按钮和方法:
html复制<el-button @click="toggleExpandAll">
{{ isExpandAll ? '折叠全部' : '展开全部' }}
</el-button>
javascript复制methods: {
toggleExpandAll() {
this.refreshTable = false
this.isExpandAll = !this.isExpandAll
this.$nextTick(() => {
this.refreshTable = true
})
}
}
最后修改el-table绑定:
html复制<el-table
v-if="refreshTable"
:data="tableData"
row-key="id"
:default-expand-all="isExpandAll"
:tree-props="{children: 'children', hasChildren: 'hasChildren'}">
<!-- 列定义 -->
</el-table>
现在点击按钮就可以切换全部展开或折叠状态了。这里的关键是通过改变refreshTable触发表格重新渲染,从而应用新的展开状态。
树形表格的新增操作需要考虑父级部门的问题。我们需要在点击"新增"按钮时,记录当前选中部门的ID作为父部门ID。
首先添加新增对话框和相关数据:
javascript复制data() {
return {
dialogVisible: false,
dialogTitle: '新增部门',
form: {
name: '',
parentId: '',
status: '1',
orderNum: 0
},
formRules: {
name: [{ required: true, message: '请输入部门名称', trigger: 'blur' }]
}
// 其他数据...
}
}
然后修改新增按钮和方法:
html复制<el-button type="primary" icon="el-icon-plus" @click="handleAdd">
新增
</el-button>
<!-- 新增/编辑对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible">
<el-form :model="form" :rules="formRules" label-width="100px">
<el-form-item label="部门名称" prop="name">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="状态">
<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-item label="排序">
<el-input-number v-model="form.orderNum" :min="0"></el-input-number>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</el-dialog>
javascript复制methods: {
handleAdd(row) {
this.dialogTitle = '新增部门'
this.form = {
name: '',
parentId: row ? row.id : '0', // 如果有row说明是添加子部门
status: '1',
orderNum: 0
}
this.dialogVisible = true
},
submitForm() {
this.$refs.form.validate(valid => {
if (valid) {
// 这里应该是调用API提交数据
// 模拟新增数据
const newDept = {
id: Date.now().toString(),
...this.form,
createTime: new Date().toISOString()
}
if (this.form.parentId === '0') {
// 添加到根节点
this.tableData.push(newDept)
} else {
// 添加到子节点
this.findAndAddChild(this.tableData, this.form.parentId, newDept)
}
this.dialogVisible = false
this.$message.success('新增成功')
}
})
},
findAndAddChild(data, parentId, newDept) {
for (let item of data) {
if (item.id === parentId) {
if (!item.children) {
this.$set(item, 'children', [])
}
item.children.push(newDept)
return true
}
if (item.children && this.findAndAddChild(item.children, parentId, newDept)) {
return true
}
}
return false
}
}
编辑功能与新增类似,主要区别在于需要先填充表单数据。我们继续完善代码:
修改操作列的编辑按钮:
html复制<el-table-column label="操作" width="180">
<template v-slot="{row}">
<el-button size="mini" @click="handleEdit(row)">编辑</el-button>
<el-button size="mini" type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
添加编辑方法:
javascript复制methods: {
handleEdit(row) {
this.dialogTitle = '编辑部门'
this.form = {
id: row.id,
name: row.name,
parentId: row.parentId || '0',
status: row.status,
orderNum: row.orderNum || 0
}
this.dialogVisible = true
},
submitForm() {
this.$refs.form.validate(valid => {
if (valid) {
if (this.form.id) {
// 编辑逻辑
this.findAndUpdate(this.tableData, this.form.id, this.form)
this.$message.success('修改成功')
} else {
// 新增逻辑
// ...之前的新增代码
}
this.dialogVisible = false
}
})
},
findAndUpdate(data, id, formData) {
for (let item of data) {
if (item.id === id) {
Object.assign(item, {
name: formData.name,
status: formData.status,
orderNum: formData.orderNum
})
return true
}
if (item.children && this.findAndUpdate(item.children, id, formData)) {
return true
}
}
return false
}
}
删除树形数据时需要特别注意:
添加删除方法:
javascript复制methods: {
handleDelete(row) {
this.$confirm('确定删除该部门吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
if (row.children && row.children.length > 0) {
this.$message.warning('该部门下有子部门,不能删除')
return
}
if (this.findAndRemove(this.tableData, row.id)) {
this.$message.success('删除成功')
} else {
this.$message.error('删除失败')
}
})
},
findAndRemove(data, id) {
for (let i = 0; i < data.length; i++) {
if (data[i].id === id) {
data.splice(i, 1)
return true
}
if (data[i].children && this.findAndRemove(data[i].children, id)) {
return true
}
}
return false
}
}
对于大型组织架构,一次性加载所有部门数据可能性能较差。Element-UI的树形表格支持懒加载模式,即只在展开节点时才加载子节点数据。
实现懒加载需要:
修改表格定义:
html复制<el-table
:data="tableData"
row-key="id"
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
:load="loadChildren"
lazy>
<!-- 列定义 -->
</el-table>
修改数据格式,添加hasChildren标记:
javascript复制data() {
return {
tableData: [
{
id: '1',
name: '总公司',
status: '正常',
createTime: '2023-01-01',
hasChildren: true // 标记该节点可能有子节点
}
]
}
}
实现loadChildren方法:
javascript复制methods: {
async loadChildren(tree, treeNode, resolve) {
// 模拟API请求
const children = await this.fetchDeptChildren(tree.id)
resolve(children)
},
fetchDeptChildren(parentId) {
return new Promise(resolve => {
// 模拟API延迟
setTimeout(() => {
if (parentId === '1') {
resolve([
{
id: '2',
name: '技术部',
status: '正常',
createTime: '2023-01-15',
hasChildren: true
},
{
id: '3',
name: '市场部',
status: '正常',
createTime: '2023-01-20',
hasChildren: false
}
])
} else if (parentId === '2') {
resolve([
{
id: '4',
name: '前端组',
status: '正常',
createTime: '2023-02-01'
}
])
} else {
resolve([])
}
}, 500)
})
}
}
Element-UI的树形表格还支持拖拽排序,这在调整部门顺序时非常有用。启用拖拽排序非常简单:
html复制<el-table
:data="tableData"
row-key="id"
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
draggable
:default-expand-all="isExpandAll"
@row-drop="handleDrop">
<!-- 列定义 -->
</el-table>
添加拖拽处理方法和样式:
javascript复制methods: {
handleDrop(draggingNode, dropNode, type) {
// type参数表示拖拽类型:'before'、'after'或'inner'
console.log('拖拽完成', draggingNode, dropNode, type)
// 这里需要实现实际的排序逻辑
// 注意:拖拽后需要手动更新数据源
}
}
css复制/* 添加拖拽手柄样式 */
.el-table .el-table__row .el-icon-rank {
cursor: move;
}
在实际项目中,当部门数据量很大时,可能会遇到性能问题。以下是几个优化建议:
使用虚拟滚动:Element-UI的表格本身已经做了优化,但对于特别大的数据集,可以考虑使用第三方虚拟滚动组件
分页加载:即使是树形数据,也可以实现分页加载,特别是对于平级节点较多的场景
减少响应式数据:对于不会变动的数据,可以使用Object.freeze()冻结对象,减少Vue的响应式开销
按需渲染:对于不可见的节点,可以暂时不渲染其DOM元素
后端过滤:搜索功能尽量在后端实现,减少前端数据处理压力
在实际项目中,我们需要与后端API交互。一个良好的树形部门API设计通常包括:
建议将API调用封装成单独的service模块:
javascript复制// src/api/dept.js
import request from '@/utils/request'
export function getDeptTree(params) {
return request({
url: '/dept/tree',
method: 'get',
params
})
}
export function addDept(data) {
return request({
url: '/dept',
method: 'post',
data
})
}
export function updateDept(id, data) {
return request({
url: `/dept/${id}`,
method: 'put',
data
})
}
export function deleteDept(id) {
return request({
url: `/dept/${id}`,
method: 'delete'
})
}
在实际项目中,我们需要处理各种异常情况:
建议使用try-catch包裹API调用,并提供友好的错误提示:
javascript复制async loadDeptData() {
this.loading = true
try {
const res = await getDeptTree(this.queryParams)
if (res.code === 0) {
this.tableData = res.data
} else {
this.$message.error(res.message || '获取部门数据失败')
}
} catch (error) {
console.error('获取部门数据出错:', error)
this.$message.error('网络错误,请稍后重试')
} finally {
this.loading = false
}
}
在多个企业级项目中实现树形表格后,我总结了一些宝贵的经验:
数据一致性:树形数据的新增、编辑和删除操作后,一定要确保前端数据与后端保持同步。我遇到过因为缓存导致数据显示不一致的问题,最后通过强制刷新或精细化的局部更新解决了。
权限控制:不同层级部门的操作权限可能不同。比如分公司管理员只能管理自己分公司的部门。我们在表格的操作列中加入了v-if条件渲染,根据权限动态显示按钮。
大数据量优化:有一次客户导入了几千个部门,导致页面卡死。最后我们实现了虚拟滚动和懒加载结合方案,并添加了搜索过滤功能,性能提升了10倍以上。
移动端适配:树形表格在移动端显示需要特别处理。我们最终实现了一个简化版,点击部门名称展开子部门,而不是用小小的箭头图标。
导入导出:客户经常需要批量导入部门结构。我们开发了Excel导入功能,并添加了数据校验和错误提示,大大减少了人工输入的错误。
操作日志:所有对部门结构的修改都应该记录操作日志。我们在每次调用修改API后,会自动记录一条操作日志,方便后续审计。
数据校验:特别是当允许用户拖拽排序时,一定要在后端校验数据的合法性,防止出现循环引用等错误数据结构。