今日目标
✔ 掌握 el-tree 组件的使用。
✔ 完成组织架构的增删改查功能。
组织架构树形结构布局
目标
使用 ElementUI 组件布局组织架构的基本布局。
认识组织架构

一个企业的组织架构是该企业的灵魂,组织架构通常采用树形金字塔式结构,本章节,我们布局出页面的基本结构。

树形组件认识
接下来,实现树形的结构,采用 Element UI 的 tree 组件,相关属性如下。
参数 |
说明 |
类型 |
可选值 |
默认值 |
default-expand-all |
是否默认展开所有节点 |
boolean |
— |
— |
data |
展示数据 |
array |
— |
— |
node-key |
每个树节点用来作为唯一标识的属性,在整棵树中应该是唯一的 |
String |
— |
— |
props |
配置选项,具体看下表 |
object |
— |
— |
props 对象的配置信息如下。
参数 |
说明 |
类型 |
可选值 |
默认值 |
label |
指定节点标签为节点对象的某个属性值 |
string, function(data, node) |
— |
— |
children |
指定子树为节点对象的某个属性值 |
string |
— |
— |
disabled |
指定节点选择框是否禁用为节点对象的某个属性值 |
boolean, function(data, node) |
— |
— |
data 属性是组成树形数据的关键,演示它的使用,官网。
1 2 3 4 5 6
| <template> <el-card> <el-row></el-row> <el-tree :data="departs"></el-tree> </el-card> </template>
|
改造树形组件的数据和结构
📝 目标:根据固定数据,完成下面的效果,掌握 tree props 属性和作用域插槽的使用。
views/department/index.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| <template> <div class="container"> <div class="app-container"> <el-card> <el-tree :data="departs" :props="defaultProps" :default-expand-all="true"> <template #default="{ data }"> <el-row type="flex" align="middle" style="height: 40px; width: 100%"> <el-col> <span>{{ data.name }}</span> </el-col> <el-col :span="4"> <el-row type="flex"> <el-col>{{ data.managerName }}</el-col> <el-col> <el-dropdown> <span>操作<i class="el-icon-arrow-down" /> </span> <el-dropdown-menu slot="dropdown"> <el-dropdown-item>添加子部门</el-dropdown-item> <el-dropdown-item>编辑部门</el-dropdown-item> <el-dropdown-item>删除部门</el-dropdown-item> </el-dropdown-menu> </el-dropdown> </el-col> </el-row> </el-col> </el-row> </template> </el-tree> </el-card> </div> </div> </template> <script> export default { name: 'Department', data() { return { departs: [{ name: '总裁办', managerName: 'zs', children: [{ name: '董事会', managerName: 'ls' }] }, { name: '行政部', managerName: 'ww' }, { name: '人事部', managerName: 'zl' } ], defaultProps: { label: 'name' } } } } </script>
|
任务
完成树形结构的显示。
获取加工组织架构数据
目标
获取真实的组织架构数据,并将其转化成树形数据显示在页面上。
封装 API 接口
- 封装获取组织架构的请求,
api/departments.js
。
1 2 3 4 5 6 7
| import request from '@/utils/request'
export function getDepartments() { return request({ url: '/company/department', }) }
|
- 在钩子函数中调用接口,
views/departments/index.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import { getDepartments } from '@/api/departments' export default { name: 'Department', data() { return { departs: [], defaultProps: { label: 'name' } } }, created() { this.getDepartments() }, methods: { async getDepartments() { const result = await getDepartments() this.departs = result } } }
|
将数组数据转化成树形结构

- 封装一个工具方法,
utils/index.js
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export function transListToTreeData(list, rootValue) { const arr = [] list.forEach((item) => { if (item.pid === rootValue) { const children = transListToTreeData(list, item.id) if (children.length) { item.children = children } arr.push(item) } }) return arr }
|
- 调用转化方法,转化树形结构,
src/views/department/index.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
import { transListToTreeData } from '@/utils' export default { methods: { async getDepartments() { this.departs = transListToTreeData(result, 0) } } }
|
array to tree
需求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| const list = [{ id: 'a', pid: '', name: '总裁办' }, { id: 'b', pid: '', name: '行政部' }, { id: 'c', pid: '', name: '财务部' }, { id: 'd', pid: 'c', name: '财务核算部' }, { id: 'e', pid: 'c', name: '税务管理部' }, { id: 'f', pid: 'e', name: '税务管理 A 部' }, { id: 'g', pid: 'e', name: '税务管理 B 部' }, ]
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| const list = [{ id: 'a', pid: '', name: '总裁办' }, { id: 'b', pid: '', name: '行政部' }, { id: 'c', pid: '', name: '财务部', children: [{ id: 'd', pid: 'c', name: '财务核算部' }, { id: 'e', pid: 'c', name: '税务管理部', children: [{ id: 'f', pid: 'e', name: '税务管理 A 部' }, { id: 'g', pid: 'e', name: '税务管理 B 部' }, ], }, ], }, ]
|
确定 ROOT_ID
🍜 先确定 ROOT_ID 为 ''
,把 pid 等于 ROOT_ID 的添加到新数组的第一层。
🤔 思路:封装方法 fn1,从 list 中查找,【谁】(数组的某一项 item)的 pid 等于根 id 的(根 id 就是 ROOT_ID),就把这个【谁】添加到数组中。
1 2 3 4 5 6 7 8 9 10 11
| const ROOT_ID = ''
function fn1(list) { const arr = [] list.forEach(item => { if (item.pid === ROOT_ID) { arr.push(item) } }) return arr; }
|
查找条件可能会变化,为了方便后续的处理,改造上面代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const ROOT_ID = ''
function fn1(list, id = ROOT_ID) { const arr = [] list.forEach((item) => { if (item.pid === id) { arr.push(item) } }) return arr } const result = fn1(list) console.log(result)
|
找 item 的 children
🤔 添加进去的 item 项可能还有 children,所以添加之前先找添加进去的这个 item 有没有 children,有 children 的话就把这个 children 挂到 item 上之后再进行添加。
🤔 那么怎么找 children 呢?
😀 思路:封装方法 fn2,从 list 中查找,【谁】的 pid 等于 item 的 id 的,就把这个【谁】添加到数组中,最后把整个数组作为添加进去的 item 的 children。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const ROOT_ID = ''
function fn1(list, id = ROOT_ID) { const arr = [] list.forEach((item) => { if (item.pid === id) { const children = fn2(list, item.id) if (children.length) { item.children = children } arr.push(item) } }) return arr }
function fn2(list, id) { const arr = [] list.forEach((item) => { if (item.pid === id) { arr.push(item) } }) return arr }
const result = fn1(list, '') console.log(result)
|
找 item 的 children
添加进去的 item 项可能还有 children,所以添加之前先找有没有 children,怎么找 children 呢?
思路:封装方法 fn3,从 list 中查找,【谁】的 pid 等于 item 的 id 的,就把这个【谁】添加到数组中,最后把整个数组作为添加进去的 item 的 children。
递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const ROOT_ID = ''
function fn(list, id = ROOT_ID) { const arr = [] list.forEach(item => { if (item.pid === id) { const children = fn(list, item.id) if (children.length) { item.children = children } arr.push(item) } }) return arr; } const arr = fn(list) console.log(arr)
|
1 2 3 4 5 6 7
| import { arrayToTree } from 'performant-array-to-tree' arrayToTree(arr, { parentId: 'pid', dataField: null })
|
组织架构删除部门功能
目标
实现操作功能的删除功能。
封装删除部门接口
封装删除功能模块, api/departments.js
。
1 2 3 4 5 6
| export function delDepartments(id) { return request({ url: `/company/department/${id}`, method: 'delete', }) }
|
注册下拉菜单事件
监听下拉菜单的点击事件并传递相关参数, views/departments/index.vue
。
1 2 3 4 5 6 7 8 9 10
| <el-dropdown @command="operateDepts($event, data.id)"> <span> 操作<i class="el-icon-arrow-down" /> </span> <el-dropdown-menu slot="dropdown"> <el-dropdown-item command="add">添加子部门</el-dropdown-item> <el-dropdown-item command="edit">编辑部门</el-dropdown-item> <el-dropdown-item command="del">删除部门</el-dropdown-item> </el-dropdown-menu> </el-dropdown>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| export default { name: 'Department', methods: { operateDepts(type, id) { if (type === 'add') { } else if (type === 'edit') { } else { } } } }
|
调用删除部门接口
删除之前,提示用户是否删除,然后调用删除接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import { delDepartments } from '@/api/departments' export default { name: 'Department', methods: { operateDepts(type) { if (type === 'add') {} else if (type === 'edit') {} else { this.$confirm('确定要删除该部门吗').then(() => { return delDepartments(id) }).then(() => { this.getDepartments() }).catch(err => { console.log(err) }) } } } }
|
组织架构新增部门弹框
目标
实现新增部门功能的组件创建。
封装新增接口
封装新增部门的 api 模块, api/departments.js
。
1 2 3 4 5 6 7
| export function addDepartments(data) { return request({ url: '/company/department', method: 'post', data, }) }
|
新建新增弹层
- 构建一个新增部门的窗体组件,
views/department/components/add-dept.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <template> <el-dialog title="新增部门" visible> <el-form label-width="120px"> <el-form-item label="部门名称"> <el-input style="width:80%" placeholder="1-50个字符" /> </el-form-item> <el-form-item label="部门编码"> <el-input style="width:80%" placeholder="1-50个字符" /> </el-form-item> <el-form-item label="部门负责人"> </el-form-item> <el-form-item label="部门介绍"> <el-input style="width:80%" placeholder="1-300个字符" type="textarea" :rows="3" /> </el-form-item> </el-form> <el-row slot="footer" type="flex" justify="center"> <el-col :span="6"> <el-button type="primary" size="small">确定</el-button> <el-button size="small">取消</el-button> </el-col> </el-row> </el-dialog> </template>
|
- 引入新增弹框组件,
departments/index.vue
。
1 2 3 4 5 6
| import AddDept from './components/add-dept' export default { components: { AddDept }, }
|
1 2 3 4
| <div class="container"> <div class="app-container"></div> <add-dept /> </div>
|
点击显示弹层
📝 目标:点击新增子部门时显示弹层组件。
- 用外界传递过来的 showDialog 属性控制组件的显示或者隐藏,
views/departments/components/add-dept.vue
。
1 2 3 4 5 6 7 8 9 10
| export default { name: 'AddDept', props: { showDialog: { type: Boolean, default: false } } }
|
把 showDialog 变量和 el-dialog 组件的 visible 属性进行绑定。
1
| <el-dialog title="新增部门" :visible="showDialog"></el-dialog>
|
- 父组件定义并传递控制窗体显示的变量
showDialog
,views/departments/index.vue
。
1
| <add-dept :show-dialog="showDialog" />
|
1 2 3 4 5 6 7 8 9 10
| export default { name: 'Department', data() { return { showDialog: false } }, }
|
- 当点击新增部门时,弹出组件,
views/departments/tree-tools.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| export default { name: 'Department', methods: { operateDepts(type, id) { if (type === 'add') { this.showDialog = true } else if (type === 'edit') { } else { } } } }
|
- 关闭弹框。
给 el-dialog 组件绑定 @close 事件, src/views/department/components/add-dept.vue
1
| <el-dialog title="新增部门" :visible="showDialog" @close="btnCancel"></el-dialog>
|
1 2 3 4 5 6 7 8 9
| export default { name: 'AddDept', methods: { btnCancel() { this.$emit('update:showDialog', false) } } }
|
父组件添加 .sync
修饰符(可以简化事件监听和赋值的操作), src/views/department/index.vue
1
| <add-dept :show-dialog.sync="showDialog" />
|
组织架构新增部门校验
目标
完成新增部门功能的规则校验和数据提交部分。
数据收集和基础校验
- 准备变量和规则,
views/departments/components/add-dept.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| export default { name: 'AddDept', data() { return { formData: { name: '', code: '', managerId: '', introduce: '', pid: '' }, rules: { name: [{ required: true, message: '部门名称不能为空', trigger: 'blur' }, { min: 1, max: 50, message: '部门名称要求1-50个字符', trigger: 'blur' } ], code: [{ required: true, message: '部门编码不能为空', trigger: 'blur' }, { min: 1, max: 50, message: '部门编码要求1-50个字符', trigger: 'blur' } ], managerId: [{ required: true, message: '部门负责人不能为空', trigger: 'change' }], introduce: [{ required: true, message: '部门介绍不能为空', trigger: 'blur' }, { trigger: 'blur', min: 1, max: 300, message: '部门介绍要求1-50个字符' } ] } } }, }
|
- 把上面的数据和规则与视图进行绑定。
给 el-form
组件配置 model 和 rules 属性。
给 el-form-item
组件配置 prop 属性。
给表单组件 el-input
等进行 v-model 双向绑定。
部门名称和部门编码
📝 目标:完成部门名称和部门编码的自定义校验,部门名称不能和 同级别
的重复。
🤔 思考:添加时,部门名称的规则是什么?部门编码的规则是什么?
- 获取最新的部门数据,判断是否重复。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import { getDepartments } from '@/api/departments' export default { name: 'AddDept', data() { const checkNameRepeat = async (rule, value, callback) => { const result = await getDepartments() const isRepeat = result.some((item) => item.name === value) isRepeat ? callback(new Error(`当前已经有${value}的部门了`)) : callback() } return { rules: { name: [ { trigger: 'blur', validator: checkNameRepeat } ], } } }, }
|
- 检查部门编码的过程同理。
获取部门负责人的数据
目标
获取新增表单中的部门负责人下拉数据。
内容
- 封装获取部门负责人的接口,
api/departments.js
。
1 2 3 4 5
| export function getManagerList() { return request({ url: '/sys/user/simple', }) }
|
- 组件初始化完毕后调用获取员工列表的方法,
views/departments/add-dept.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import { getManagerList } from '@/api/departments' export default { name: 'AddDept', data() { return { managerList: [], } }, created() { this.getManagerList() }, methods: { async getManagerList() { this.managerList = await getManagerList() }, }, }
|
- 渲染数据。
1 2 3 4
| <el-select v-model="formData.managerId" style="width:80%" placeholder="请选择"> <el-option v-for="item in managerList" :key="item.id" :label="item.username" :value="item.id" /> </el-select>
|
记录当前点击的数据项
- 点击添加子部门按钮时进行记录,
src/views/department/index.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| export default { name: 'Department', data() { return { currentNodeId: null } }, methods: { operateDepts(type, id) { if (type === 'add') { this.currentNodeId = id this.showDialog = true } else if (type === 'edit') { } else { } } } }
|
- 传递 currentNodeId 到新增的子组件,
src/views/department/index.vue
。
1
| <add-dept :show-dialog.sync="showDialog" :current-node-id="currentNodeId" />
|
- 子组件进行接收数据,
src/views/department/components/add-dept.vue
。
1 2 3 4 5 6 7 8 9 10 11
| export default { name: 'AddDept', props: { currentNodeId: { type: Number, default: null } } }
|
组织架构新增部门功能
- 给 el-form 定义一个 ref 属性,
views/departments/components/add-dept.vue
。
1
| <el-form ref="deptForm" :model="formData" :rules="rules" label-width="120px"></el-form>
|
1 2 3 4 5 6 7 8
| btnOK() { this.$refs.deptForm.validate(isOK => { if (isOK) { } }) }
|
- 校验通过时,调用新增接口,因为是添加子部门,所以子部门的 pid 应该等于当前点击部门的 id。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import { addDepartments } from '@/api/departments'
export default { name: 'AddDept', methods: { btnOK() { this.$refs.deptForm.validate(async isOK => { if (isOK) { await addDepartments({ ...this.formData, pid: this.currentNodeId }) this.$emit('updateDepartment') this.$message.success('新增部门成功') this.btnCancel() } }) }, } }
|
- 父组件监听 updateDepartment,
views/departments/index.vue
。
1
| <add-dept :show-dialog.sync="showDialog" :current-node-id="currentNodeId" @updateDepartment="getDepartments" />
|
- 关闭弹框时,重置表单和校验规则。
1 2 3 4 5 6 7 8 9 10 11 12 13
| export default { name: 'AddDept', methods: { btnCancel() { this.$refs.deptForm.resetFields() this.$emit('update:showDialog', false) } } }
|
编辑部门功能数据回写
目标
实现编辑部门的功能。
填充数据
📝 目标:点击编辑按钮时,父组件调用子组件获取详情方法来填充数据。
- 封装获取部门信息的接口,
api/departments.js
。
1 2 3 4 5 6
| export function getDepartDetail(id) { return request({ url: `/company/department/${id}`, }) }
|
- 添加部门的组件中定义获取详情的方法,并把结果赋值给 formData,
views/departments/components/add-dept.vue
。
1 2 3 4 5 6 7
| import { getDepartDetail } from '@/api/departments'
async getDepartDetail() { this.formData = await getDepartDetail(this.currentNodeId) }
|
- 父组件中,点击编辑部门按钮时,调用子组件的方法,
views/departments/index.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| export default { name: 'Department', methods: { operateDepts(type, id) { if (type === 'add') { } else if (type === 'edit') { this.currentNodeId = id this.showDialog = true this.$refs.addDept.getDepartDetail() } else { } } } }
|
- 处理 props 传递数据异步的问题。
1
| this.$nextTick(this.$refs.addDept.getDepartDetail)
|
根据计算属性显示控制标题
- 根据有无 ID,利用计算属性,区分编辑和添加的弹框标题,
views/departments/components/add-dept.vue
。
1 2 3 4 5
| computed: { showTitle() { return this.formData.id ? '编辑部门' : '新增子部门' } },
|
- 绑定。
1
| <el-dialog :title="showTitle" :visible="showDialog" @close="btnCancel"></el-dialog>
|
- el-form 中的 resetFields 不能重置非表单中的数据,导致 formData.id 残留,所以在取消的时候手动重置数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| btnCancel() { this.formData = { name: '', code: '', managerId: '', introduce: '', pid: '' } this.$refs.deptForm.resetFields() this.$emit('update:showDialog', false) }
|
封装并调用编辑的接口
🤔 思考:点击确定按钮时,如何区分是新增和编辑?
- 封装编辑部门接口,
api/departments.js
。
1 2 3 4 5 6 7 8
| export function updateDepartments(data) { return request({ url: `/company/department/${data.id}`, method: 'put', data, }) }
|
- 点击确定时,进行场景区分,
views/departments/components/add-dept.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| export default { name: 'AddDept', methods: { btnOK() { this.$refs.deptForm.validate(async isOK => { if (isOK) { if (this.formData.id) { await updateDepartments(this.formData) } else { await addDepartments({ ...this.formData, pid: this.currentNodeId }) } this.$emit('updateDepartment') this.$message.success(`${this.formData.id ? '编辑' : '新增'}部门成功`) this.btnCancel() } }) } } }
|
校验规则支持编辑场景
发现原来的校验规则和编辑部门有些冲突,所以需要进一步处理,编辑时 name 和 code 的校验规则应该是怎样的?
views/departments/components/add-dept.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const checkNameRepeat = async (rule, value, callback) => { let result = await getDepartments() if (this.formData.id) { result = result.filter(item => item.id !== this.formData.id) } const isRepeat = result.some((item) => item.name === value) isRepeat ? callback(new Error(`当前已经有${value}的部门了`)) : callback() } const checkCodeRepeat = async (rule, value, callback) => { let result = await getDepartments() if (this.formData.id) { result = result.filter(item => item.id !== this.formData.id) } const isRepeat = result.some((item) => item.code === value) isRepeat ? callback(new Error(`当前已经有${value}的编码了`)) : callback() }
|
至此,整个组织架构,我们完成了读取 / 新增部门 / 删除部门 / 编辑部门。
给数据获取添加进度条
目标
由于获取数据的延迟性,为了更好的体验,可以利用 Element UI 提供的 v-loading 指令给页面加一个进度提示。
内容
- 定义 loading 变量,
views/departments/index.vue
。
1 2 3 4 5
| data() { return { loading: false } }
|
- 获取数据前后设置变量。
1 2 3 4 5 6
| async getDepartments() { this.loading = true const result = await getDepartments() this.departs = transListToTreeData(result.depts, 0) this.loading = false }
|
- 赋值变量给 v-loading 指令。
1
| <el-card v-loading="loading"></el-card>
|