【2022】项目中树形组件的一些应用

188 阅读7分钟

一、前言

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源数据}]})。
  • 还有一个重点突破,就是实现需求的功能点进行拆分,形成一个一个的方法,里面含的逻辑点和行数都要控制,代码规范。

当然,各位看官若是有见解,请留言。