一、前言
1.1 背景
在 2022 年的项目开发中,有一个需求:流程设计器,其实现格式类似钉钉,有四种节点类型:审批人节点、办理人节点、抄送人节点和条件节点。其节点设置中有几个树形选择弹框:
- 选择角色/组织,规则:
- a. 只能选择叶子节点;
- b. 支持限制选择个数;
- c. 左侧是树形, 右侧是左侧选中数据的列表且支持删除;
- 选择组织下的人员,规则:
- a. 只能选择人员,组织不可选
- b. 同一人员可能在不同的组织下(需要支持同步选中和取消选中);
- c. 支持限制选择个数;
- d. 左侧是树形, 右侧是左侧选中数据的列表且支持删除;
- 选择人员或角色,规则:
- a. 支持选择人员或选择角色切换;
- b. 选择人员规则同 选择组织下的人员 规则;
- c. 选择角色规则同 选择角色/组织 规则;
1.2 树形组件
项目用的前端组件库是 element-plus,用到的基础组件是 el-tree。
关于 el-tree 有几点规则:
- 每个节点 Node 自身会有一个唯一的 id 即 node.id,在实例化 Node 时自动生成(
this.id = nodeIdSeed++
); - 每次树形数据更新会重新生成子树;
- 在 tree-store 里会根据树形数据同步生成一个 map 即 nodesMap,其键值对的键是由用户指定的标识树节点的唯一性的 nodeKey 在节点数据中对应的值或者是 node.id 对应节点的 id 值 ,值是对应 Node;
注意一点:根据选择组织下的人员需求,如果用户指定了 nodeKey,且有多个节点对应的数据信息是一样的,这种情况下,在 nodesMap 里存储的是最后一个,因为前面的会被后面的覆盖。于是,产生一个问题:同一个人在不同组织下的选中或取消选中的同步问题。
首先 选择角色/组织组件,没有重复的数据节点,但要支持许纳泽角色和选择组织,其传入的数据的 props 格式和目标格式都可能不同。而 选择组织下的人员组件 可能存在重复数据的节点且需要判断节点是组织还是人员,故写成两个组件。
下面开始根据 el-tree 写组件~
二、选择角色/组织组件
2.1 需求分析与组件设计
为满足需求的组件开发做准备~
- a. 布局:左右两个框,左侧是树,右侧是左侧选中的数据列表且支持删除;
- b. 几个变量:
- checkBaseList:响应式变量,存储左侧选中的数据列表;
- c. 操作规则:
- c1. 勾选左侧树,若是叶子节点则加入,否则不做处理;
- c2. 取消勾选,若存在 checkBaseList 则删除,否则不做处理;;
- c3. 点击右侧删除,取消勾选;
- cd. 初值回显;
2.2 关键代码
三、选择组织下的人员组件
3.1 需求分析与组件设计
为满足需求的组件开发做准备~
- a. 布局:左右两个框,左侧是树,右侧是左侧选中的数据列表(checkBaseList)且支持删除;
- b. 需要两个字段,一个字段标识树节点的唯一性,一个字段标识人员的唯一性(能判断不同组织下是否是同一个人);
- c. 几个变量:
- nodesMap:树形源数据形成的键值对,键是能标识人员唯一性的字段值,值是键对应的源数据列表(源数据指给 el-tree 传入的 data,结果数据是根据选中节点的列表以用户想要的格式形成的列表),作用:方便拿到某人员对应的数据列表;
- checkBaseList:响应式变量,存储左侧选中的数据列表;
- d. 操作规则:
- d1. 勾选左侧人员节点,通过方法 getLeftExistNodes 拿到对应的人员列表,循环判断:若未加入 checkedBaseList 则加入且选中;
- d2. 取消勾选左侧人员节点,通过方法 getLeftExistNodes 拿到对应的人员列表,循环调用 addCheckedBase 判断:若存在于 checkedBaseList 则删除且取消选中;
- d3. 点击右侧删除,通过方法 getLeftExistNodes 拿到对应的人员列表,循环调用 removeCheckedBase 判断:若存在于 checkedBaseList 则删除且取消选中;
- d4. 回显的三个时机:初值回显;树的源数据改变后回显;搜索后回显;
3.2 关键代码
// check-change 当树节点的复选框被点击时触发
const nodeCheckChange = (data, bool)=> {
// curOptPropObj 记录当前入参
const resultId = curOptPropObj.value.resultId
// getLeftExistNodes 根据 nodesMap 得到该人员对应的节点数据列表
const nodeDataArr = getLeftExistNodes(data, resultId)
// 将数据处理成目标格式
const handleData = getFormatData(data)
nodeDataArr.forEach(nodeData=> {
// hasExistCurCheckedNodes 判断数据对应的节点是否已被选中
if (bool && !hasExistCurCheckedNodes(nodeData)) {
leftTreeRef.value.setChecked(nodeData, true)
}
if (!bool) {
leftTreeRef.value.setChecked(nodeData, false)
}
})
bool
? addCheckedBase(handleData)
: removeCheckedBase(handleData)
}
四、选择人员或角色
4.1 需求分析与组件设计
- a. 与 选择组织下的人员 组件不同的是,在左侧树组件上面有分类的切换(选择人员/选择角色),设置一个响应式变量 curTreeType 记录当前所在类别;
- b. 接收两个处理数据的参数:一个是解读树的源数据参数 options,一个是结果参数 resultProps(用户想要的数据格式);
- c. 变量
- d. 规则如下:
- d1. 选择人员的规则同 选择组织人员组件 规则;
- d2. 选择角色的规则同 选择角色组件 规则;
4.2 关键代码
4.2.1 nodeCheckChange
当树节点的复选框被点击时触发
const curTreeType = ref("person")
const hasPerson = computed(()=> curTreeType.value == "person")
const nodeCheckChange = (data, bool)=> {
if (hasPerson.value) {
orgPersonNodeCheckChange(data, bool)
} else {
roleNodeCheckChange(data, bool)
}
}
因为组织人员树和角色树的规则不同,则分开处理:
- orgPersonNodeCheckChange:选择人员时触发 check-change
- roleNodeCheckChange:选择角色时触发 check-change
4.2.2 orgPersonNodeCheckChange
入参 data,是树节点对应的源数据,所以,如果需要处理结果数据则通过方法 getFormatData 。
三个逻辑点:(1)拿到不同部门下的所有该人员节点源数据(getLeftExistNodes);(2)遍历得到的人员源数据列表,列表项 nodeData,若要选中则判断 nodeData 若未选中则执行选中操作;(3)最后,根据 bool 执行入库或出库操作;
const orgPersonNodeCheckChange = (data, bool)=> {
const resultId = curOptPropObj.value.resultId
// 处理成目标格式
const handleData = getFormatData(data)
getLeftExistNodes(data, resultId).forEach(nodeData=> {
if (bool && !hasExistCurCheckedNodes(nodeData)) {
leftTreeRef.value.setChecked(nodeData, true)
}
if (!bool) {
leftTreeRef.value.setChecked(nodeData, false)
}
})
bool
? addCheckedBase(handleData)
: removeCheckedBase(handleData)
}
4.2.3 roleNodeCheckChange
两个逻辑点:(1)判断对应节点是否是叶子节点;(2)若是叶子节点则根据 bool 执行入库(addCheckedBase)或出库(removeCheckedBase)操作。
const roleNodeCheckChange = (data, bool)=> {
const node = leftTreeRef.value.getNode(data)
if (node) {
// 处理成目标格式
const handleData = getFormatData(data)
node.isLeaf
? bool ? addCheckedBase(handleData) : removeCheckedBase(handleData)
: null
}
}
4.2.4 addCheckedBase 入库
响应式变量 checkBaseList 记录了当前选中数据列表(不同组织下的同一人只存一个),且展示在右侧已选。若勾选了则执行入库操作。
const addCheckedBase = (data)=> {
// indexExistBase 获取data在checkBaseList的位置,判断是否已存在
const index = indexExistBase(data, curResultPropObj.value.resultId)
if (indexExistBase(data, curResultPropObj.value.resultId) < 0) {
checkedBaseList.value.push(data)
}
}
4.2.5 removeCheckedBase 出库
const removeCheckedBase = (data)=> {
const index = indexExistBase(data, curResultPropObj.value.resultId)
if (index >= 0) {
checkedBaseList.value.splice(index, 1)
}
}
4.2.6 indexExistBase
const indexExistBase = (data, key)=> {
key = key || curOptPropObj.value.resultId
return checkedBaseList.value.findIndex(item=> item[key] == data[key])
}
4.2.7 右侧已选删除
// 获取右侧选中人员在左侧树中存在的所有节点
const getLeftExistNodes = (data, key, sourceArr)=>{
if (!key) {
key = curResultPropObj.value.resultId || originIdProp
}
const originId = data[key]
if (!sourceArr) {
// personAllNodeObject 记录了人员组织节点的数据源 map,以人员唯一id为键,注意其生成规则
sourceArr = personAllNodeObject.value
}
return sourceArr[originId]
}
const rightRemoveCheckedBase = (data)=> {
const resultId = curResultPropObj.value.resultId
getLeftExistNodes(
data,
resultId,
data.type == 2 ? roleAllNodeObject.value : null
).forEach(nodeData=> {
// 取消右侧已选项在左侧的勾选并在出库
leftTreeRef.value.setChecked(nodeData, false)
removeCheckedBase(getFormatData(nodeData))
})
}
4.2.8 回显及回显的时机
const setInitChoosed = async (checkedIdList=[])=> {
await nextTick
checkedIdList.forEach(item=> {
const resultId = curResultPropObj.value.resultId
// type 为 1 是人员,为 2 是角色
if (hasPerson.value && item.type == 1) {
const cur = getLeftExistNodes(item, resultId)
leftTreeRef.value.setChecked(cur[0], true)
}
if (!hasPerson.value && item.type == 2) {
const cur = getLeftExistNodes(item, resultId, roleAllNodeObject.value)
leftTreeRef.value.setChecked(cur[0], true)
}
})
}
五、总结
本篇笔记记录了2022年项目中使用到的三个树形选择组件的开发思路
- 其难点在于组织下人员组件中同一个人可能在不同的组织下,这种情况下,需要两个id(一个id能唯一标识数据节点的唯一性,一个id能标识人员的唯一性,这两个是是不同的)和一个map(以标识人员唯一性的id为键值对的键,以人员源数据列表为值,例如:{123:[{人员1源数据}, {人员2源数据}]})。
- 还有一个重点突破,就是实现需求的功能点进行拆分,形成一个一个的方法,里面含的逻辑点和行数都要控制,代码规范。
当然,各位看官若是有见解,请留言。