【vue3】组织架构的部门的维护方案
做好维护部门信息的功能
上一篇介绍了角色与权限,里面涉及一个很重要的因素——部门。 部门和权限有着密切的联系,实现角色和权限,总是绕不开部门,所以部门维护也是一个重要环节。
相关文章
- 专栏 基于Vue3做一套低代码引擎
- 开篇
- 列表
- 表单
- 角色和权限
企业的部门信息结构
网上找了一张图,侵删:
企业的部门关系一般都是树形结构,由上级部门、下级部门的形式组成。有简单的也有复杂的,跨国集团的架构想必。。。
我们在保存部门信息的时候,不能只是单条的形式,还需要记录上级部门,下级部门等信息,而前后端的表现形式又不尽相同,我们来分别看一下。
后端的部门信息结构
后端一般会把部门信息存入关系型数据库,使用 parentId 字段体现上下级关系,使用 level、path、sort 等字段进行细节描述,比如这种结构:
部门表
字段名 | 标签 | 类型 | 说明 |
---|---|---|---|
ID | 部门编号 | int | 主键 |
deptName | 部门名称 | nvarchar | 部门名称 |
parentID | 上级部门编号 | int | 外键,自连接 |
level | 层数 | int | 第几级的部门 |
path | 父ID集合 | nvarchar | 父ID集合,“,”号分割。便于找到所有下级部门 |
sort | 排序 | int | 同级部门的先后顺序 |
sortAll | 总排序 | int | 所有部门一起排序 |
其他字段 |
设计接口
忘记class怎么写了,用TS的形式表达一下吧。
type idType = number | string
/**
* 后端的组织架构
*/
export interface IOrganization {
id: idType,
deptName: string, // 部门名称
parentId: idType, // 上级部门ID
level: number, // 层级
path: string, // 父ID集合,“,”号分割
sort: number, // 同一个父级里的先后顺序
sortAll: number, // 部门全排列
[id: idType]: any // 其他字段
}
(因为想在前端写转换函数)
示例
[
{
"id": 1,
"deptName": "总部门",
"level": 0,
"parentId": 0,
"path": "0,",
"sort": 0,
"sortAll": 10
},
{
"id": 2,
"deptName": "支持部门",
"level": 1,
"parentId": 1,
"path": "0,1,",
"sort": 10,
"sortAll": 20
},
{
"id": 4,
"deptName": "人事部门",
"level": 2,
"parentId": 2,
"path": "0,1,2,",
"sort": 10,
"sortAll": 30
},
{
"id": 7,
"deptName": "人事部二",
"level": 3,
"parentId": 4,
"path": "0,1,2,4,",
"sort": 10,
"sortAll": 40
},
{
"id": 6,
"deptName": "人事部一",
"level": 3,
"parentId": 4,
"path": "0,1,2,4,",
"sort": 20,
"sortAll": 50
},
{
"id": 3,
"deptName": "研发部门",
"level": 1,
"parentId": 1,
"path": "0,1,",
"sort": 20,
"sortAll": 60
},
{
"id": 5,
"deptName": "测试部门",
"level": 2,
"parentId": 3,
"path": "0,1,3,",
"sort": 10,
"sortAll": 70
}
]
前端的部门信息结构
前端一般采用树形结构,比如这样:
/**
* 前端的组织架构,照顾后端需要的数据
*/
export interface IOrganizationTree {
id: idType,
label: string, // 部门名称
children?: IOrganizationTree[] // 子部门
}
对比一下,我们会发现结构明显不同,一个是单层的,一个是多层的,一个使用parentID表示上下级,一个使用子属性的形式表示,所以需要写函数进行类型转换。
示例
[
{
"id": 1,
"label": "总部门",
"children": [
{
"id": 2,
"label": "支持部门",
"children": [
{
"id": 4,
"label": "人事部门",
"children": [
{
"id": 7,
"label": "人事部二",
"children": []
},
{
"id": 6,
"label": "人事部一",
"children": []
}
]
}
]
},
{
"id": 3,
"label": "研发部门",
"children": [
{
"id": 5,
"label": "测试部门",
"children": []
}
]
}
]
}
]
类型转换的函数
两种形式需要互相转换,所以写了两个函数。 并没有写成通用的形式,另外为了便于编码,用状态作为参数。
listToTree 后端列表转为树形结构
先写一个后端列表转 tree 的函数。这里利用了js的一个特性,不需要使用递归的方式,只需要遍历两次数组即可。
import type {
IOrganizationTree,
IOrganization,
IOrganizationState
} from '../types/10-organization'
/**
* 后端的结构转换为树状结构
* @param state 状态,orgTree:树状部门信息;orgList:后端列表;orgObject:key-value的
*/
export function listToTree(state: IOrganizationState) {
//从状态中获取部门信息
const { orgTree, orgList, orgObject } = state
const tmp = {} // 中转容器
// 第一次遍历,转换为 key 的形式
orgList.forEach((org: IOrganization) => {
orgObject[org.id] = org // 存入 key-value,非必要
tmp[org.id] = {
id: org.id,
label: org.deptName,
children: []
}
})
// 第二次遍历,寻找父节点
orgList.forEach((org: IOrganization) => {
// 判断父级
if (tmp[org.parentId]) {
// 有父级,加入 children
tmp[org.parentId].children.push(tmp[org.id])
} else {
// 顶级,加入 orgTree
orgTree.push(tmp[org.id])
}
})
}
treeToList 树形结构转后端列表
目前为止,没有找到不需要使用递归的方法。 好吧,我不太擅长用递归,所以总是先想办法规避。
/**
* tree 转换为转换为后端的形式
* @param state 状态,orgList:后端列表,目标。orgObject:key-value;currentOrg.sortAll 全排列
* @param parentId 父级ID,第一次传 0
* @param level 层数,第一次传 0
* @param path 路径,第一次传 ''
* @param treeNode 节点
* @param parentSort 同级里的序号,第一次传 1
*/
export function treeToList(
state: IOrganizationState, //
parentId: number | string,
level: number,
path: string,
treeNode: IOrganizationTree,
parentSort: number
) {
// 把 部门信息 存入 list
// 判断有无children
// 没有,结束递归
// 有,递归
// 设置 orgObject
const org = state.orgObject[treeNode.id]
org.level = level
org.parentId = parentId
org.path = path + parentId + ','
org.sort = parentSort * 10
org.sortAll = (state.currentOrg.sortAll++ ) * 10
// 把 部门信息 存入 list
state.orgList.push({
id: treeNode.id,
deptName: treeNode.label, // 部门名称
level: level, // 层级
parentId: parentId, // 上级部门ID
path: org.path, // 父ID集合,“,”号分割
sort: org.sort, // 同一个父级里的先后顺序
sortAll: org.sortAll // 部门全排列
})
// 判断有无children
if (treeNode.children && treeNode.children.length > 0) {
// 有,递归
let parent1Sort = 1
treeNode.children.forEach((node: IOrganizationTree, index: number) => {
treeToList(state, treeNode.id, level + 1, org.path, node, index + 1)
})
} else {
// 没有,退出递归
}
}
定义状态
为了更好的在各个组件和函数之间传递数据,所以使用了“状态”的方式。 看着是不是有点像 pinia,但是并不是 pinia,而是我直接封装的一个状态管理。
import type { InjectionKey } from 'vue'
// 状态
import { defineStore, useStoreLocal } from '@naturefw/nf-state'
import type { IState } from '@naturefw/nf-state/dist/type'
// 类型
import type {
IOrganizationTree,
IOrganizationState
} from '../types/10-organization'
// 转换函数
import { listToTree, treeToList } from '../controller/org'
// 状态的标识,避免命名冲突
const flag = Symbol('organization') as InjectionKey<string>
/**
* 注册局部状态
* * 部门信息管理用的状态 : IOrganizationState & IState
* @returns
*/
export const regOrganizationState = (): IOrganizationState & IState => {
// 定义 角色用的状态
const state = defineStore<IOrganizationState>(flag, {
state: (): IOrganizationState => {
return {
orgTree: [], // 部门信息,绑定 tree
orgList: [], // 部门信息,后端用的
orgObject: {},
currentOrg: { // 当前的部门的信息
id: 0, // 部门ID
num: 1, // 序号
sortAll: 1, // 类型转换时用的排序
info: { // 选中的节点
id: '',
label: ''
}
}
}
},
getters: {
},
actions: {
/**
* 后端列表 转为 tree,需要手动执行
*/
async listToTree () {
listToTree(this)
},
/**
* tree 转换为 后端列表
*/
async treeToList() {
// 先清空
this.orgList.length = 0
// 重置总排序
this.currentOrg.sortAll = 1
// 遍历 tree 的第一级节点
this.orgTree.forEach((org: IOrganizationTree, index: number) => {
// 状态, 父ID,层数,path,子节点,同级排序
treeToList(this, 0, 0, '', org, index)
})
}
}
},
{ isLocal: true }
)
return state
}
/**
* 子组件获取状态
*/
export const getOrganizationState = (): IOrganizationState & IState => {
return useStoreLocal<IOrganizationState & IState>(flag)
}
我喜欢使用“充血实体类”的方式使用状态,数据和方法放在一起方便使用。
所以状态里面不仅有部门信息,还有转换函数,还可以有部门初始化的方法。
实现维护部门的代码
基础知识介绍完毕,下面开始编码实现功能。
部门信息结构
部门信息可以使用 el-tree 来表示,因为比较方便。
- template
<el-tree
:allow-drop="allowDrop"
:allow-drag="allowDrag"
draggable
:data="state.orgTree"
:node-key="nodeKey"
:expand-on-click-node="false"
default-expand-all
highlight-current
empty-text="加载中"
@node-click="handleNodeClick"
@check-change="checkChange"
v-bind="$attrs"
>
<template #default="{ node, data }">
<span class="custom-tree-node">
<span>{{ data.label }}</span>
<span>
<org-button :node="node" :data="data"></org-button>
</span>
</span>
</template>
</el-tree>
设置相关的属性,使用 el-tree 的 slot 实现添加部门和删除部门的操作,具体代码由子组件实现。
使用 v-bind="$attrs"
接收外部设置的属性。
- ts
import { defineComponent } from 'vue'
import type Node from 'element-plus/es/components/tree/src/model/node'
import type { DropType } from 'element-plus/es/components/tree/src/tree.type'
// 类型
import type { IOrganizationTree } from './types/10-organization'
// 状态
import { getOrganizationState } from './state/state-organization'
// 组件
import orgButton from './comp/org-button.vue'
import orgForm from './comp/org-form.vue'
/**
* 组织架构的表单
*/
export default defineComponent({
name: 'nf-organization-form',
inheritAttrs: false,
components: {
orgButton,
orgForm
},
props: {
nodeKey: {
type: String,
default: 'id'
}
},
setup(props, context) {
// 获取状态
const state = getOrganizationState()
// 单击节点
const handleNodeClick = (data: IOrganizationTree) => {
state.currentOrg.id = data[props.nodeKey]
state.currentOrg.info = data
}
const mychange = (e) => {
// 防止事件冒泡
e.stopPropagation()
}
// 拖拽,
return {
// 状态
state,
// 事件
mychange,
handleNodeClick,
// 拖拽
allowDrop,
allowDrag
}
}
})
el-tree 实现了大部分功能,我们设置好属性即可,然后加上一点代码,实现其他功能。
操作按钮
做一个组件(org-button.vue)实现部门的添加、删除的功能,代码分散开,看着就不乱了。
- template
<a @click="append()"> 添加下级 </a>
<a style="margin-left: 8px" @click="remove()"> 删除 </a>
其实还想加一个“添加同级”的功能,不过想想,可以通过拖拽的方式调整顺序,所以添加的时候就不用纠结了。
- ts
// 添加部门
const append = () => {
if (data && node) {
const newChildTree = {
id: new Date().valueOf(),
label: '新部门' + state.currentOrg.num++,
children: [] // 子部门
}
const org = state.orgObject[data.id]
const newChild = {
id: newChildTree.id,
deptName: newChildTree.label,
parentId: data.id, // 上级部门ID
level: org.level + 1, // 层级
path: org.path + '' + data.id + ',' , // 父ID集合,“,”号分割
sort: node.data.children.length + 1, // 先设置,并不十分准确
sortAll: 1, // 这里不好判断
children: [] // 子部门
}
// key - value 结构
state.orgObject[newChildTree.id] = newChild
data.children.push(newChildTree)
}
}
// 删除部门
const remove = () => {
if (data && node) {
const parent = node.parent
const children: IOrganizationTree[] = parent.data.children || parent.data
const index = children.findIndex((d) => d.id === data.id)
children.splice(index, 1)
// dataSource.value = [...dataSource.value]
}
}
删除部门肯定没有这么简单,实际中不可能简单删除就完事了,需要做各种判断以及是否级联删除等。 不过这里就先不考虑这些复杂情况了,因为一两句也说不清。
修改部门的详细信息
这里部门信息分为两类:
- 系统结构需要,用户不能随意修改的,比如 parentID、sort、level等。
- 用户可以自己修改的,比如部门名称等。
本篇介绍的代码,其实都在维护第一种情况的字段,至于其他信息嘛,那就好办了,表单大家都会做。
为了方便维护,同时也是为了方便扩展,我们把修改部门信息的表单做成了组件(org-form.vue)。 这样,是不是说外部可以自己设置一个表单组件,替换掉内置的表单? 当然是可以的,这样就可以灵活的扩展部门的各种字段的维护了。
拖拽和排序
有些单位对于部门的先后顺序比较敏感,同级部门的先后必须不能错,所以我们增加了 sort 字段来表示同级部门的先后顺序,这样显示的时候就有了依据。同时可以使用拖拽的方式进行直观的调整。
好吧,拖拽的功能是el-tree 提供的,我们只是调用了一下。
// 拖拽,
// 总部门必须是根节点
const allowDrop = (draggingNode: Node, dropNode: Node, type: DropType) => {
if (dropNode.label === '总部门') {
return false
} else {
return true
}
}
// 总部门不能拖拽
const allowDrag = (draggingNode: Node) => {
return !draggingNode.label.includes('总部门')
}
缓存数据
前端做复杂的数据操作的时候,或则短时间内不会提交的时候,实现前端缓存就比较必要了。
我们可以把正在编辑的部门信息实时保存在 localStorage 里面,这样可以避免发生意外的时候丢失数据。
使用方式
建立组件,引入维护部门信息的组件,设置状态即可。
<org-manage
:expand-on-click-node="false" // 可以设置 el-tree 的各种属性
></org-manage>
import { orgManage, regOrganizationState } from '../../../lib-organization/main'
const state = regOrganizationState()
//通过状态设置部门的信息,可以设置 tree ,也可以设置list。
state.orgList.push(...list)
// 如果设置list的话,需要调用转换函数
setTimeout(() => {
state.treeToList()
}, 200)
sortAll 有什么用途?
不知道你有没有注意到 sortAll 这个字段,为了设置好这个字段,似乎还费了不少力气。 那么 sortAll,到底可以做什么呢?
普通 table
比如我们想用 el-table 显示部门信息,可以这样做:
<nf-grid-slot
v-bind="gridMeta"
:dataList="tableOrg"
size="small"
>
<template #deptName="scope">
<span :style="'margin-left: '+ (scope.row.level + 1) * 10 +'px'">
{{ scope.row.deptName }}
</span>
</template>
</nf-grid-slot>
nf-grid-slot 是啥?就是基于 el-table 封装的,可以依赖 json 渲染,同时还可以设置slot的列表控件。(这个不是重点)
我们先让数组按照 sortAll 进行排序,然后设置一个 slot ,依据 level 通过 css 的 margin-left 实现不同级别的部门的缩进效果。
这样一个部门信息就展示出来了。
普通 select
同理,我们可以使用 el-select 显示 部门信息
<el-select v-model="currOrg">
<el-option
v-for="item in state.orgList"
:key="item.id"
:label="item.deptName"
:value="item.id"
>
<span
:style="'margin-left: '+ (item.level + 1) * 10 +'px'"
>{{ item.deptName }}</span>
</el-option>
</el-select>
设置 el-option 的 slot,设置 css 即可。
path 有什么用途?
设置 path 的目的是为了方便找到所有的下级部门。因为不管是 tree 的结构,还是 后端的 parentID,想要找到所有的下级部门,都不太方便实现。但是有了path就不同了。
sql
数据库查询,可以使用 like 的方式:
select * from tbl_Dept where path like 'xxxx,%'
js
前端可以通过数组的 filter 和字符串的 indexOf 进行查询:
watch(() => currOrg.value, (id) => {
const org = state.orgObject[id]
const arr = state.orgList.filter((item) =>
item.path.item.path.indexOf(org.path + org.id + ',') === 0)
tableOrg.length = 0
tableOrg.push(...arr)
})
源码
转载自:https://juejin.cn/post/7123049639246299166