ant-design-vue中实现a-tree树形控件父子关联选中过滤的算法

在使用ant-design-vue的框架时,a-tree是比较常用的组件,比较适合处理树形结构的数据。

但是在与后台数据进行授权交互时,就不友好了。

在原生官方文档的例子中,若子项被勾选,则父级节点会被关联勾选,但这勾选并不一定是选中的意思。有可能是半选中,通过方框样式选中,也就是说父级节点的值不会出现在checkedKeys的数组中。

html 复制代码
<a-tree
  v-model:checkedKeys="checkedKeys"
  checkable
  :tree-data="treeData"
>
</a-tree>

这对后端数据授权处理是不友好的。因为一般授权子节点时,父级节点必须是也授权的,否则应用无法到达子节点的功能。也就是说,关联被授权的父子节点的ID都要传给后端。

提交的时候,到还好说。将所有选中的列表和半选中的列表合并起来提交就可以了。

js 复制代码
const handleCheck = (checkedKeys, e) => {
  const {halfCheckedKeys } = e;
  const allCheckedKeys = [...checkedKeys, ...halfCheckedKeys];
};

但是数据回写的时候就难受了,数据库里的授权列表回写到a-tree组件时,由于父节点被回传过来,那么当父节点被选中时,所有子节点都会被选中,这与实际不符。

所以需要将服务传过来的数据allCheckedKeys处理,再还原成选中checkedKeys和半选中halfCheckedKeys的两部分。

这个业务逻辑比较复杂,需要比较授权列表和树形结构数据。

我在网上找了很久都没找到关于此业务的算法逻辑。但是找到了另外的两种解决办法:

一是更改数据库结构,添加授权列表的字段分为选中和半选中的状态,传给后端服务时ID分类传输。

二是父子节点不再关联,即添加a-tree的属性为checkStrictly="true",分别独立处理单个节点的逻辑。

第一种方法缺点就是数据表设计不规范,数据库存储了适应前端页面的数据。优点是避免了js编写筛出未全部选中的父级id的工作。

第二种方法缺点是勾选父级节点时,子节点不会被关联勾选,层级很多时,操作不方便。同时子节点勾选时,可能遗漏父节点的勾选。当然可以再写代码,补上往上和往下关联的逻辑。但是还是有缺点,父节点被勾选时,是通过"√"表示,若不展开下级列表,会不知道子节点是否被全选上。

最后思来想去,感觉两种方法都有缺点。我还是觉得自己将这个业务逻辑的算法写出来吧。。。

代码如下:

ts 复制代码
/**
 * 提供两个方法:
 * 一是转换自定义树形对象数据为a-tree识别的树形对象列表
 * 二是将数据库存储的已分配id列表重新转化为checkedList
 * 
 * @param {string} idKey - 数据项 ID 的键名,默认为 'id'
 * @param {string} nameKey - 数据项名称的键名,默认为 'name'
 * @param {string} childrenKey - 子节点列表的键名,默认为 'children'
 */
export const useTreeConverter = (
  idKey: string = 'id',
  nameKey: string = 'name',
  childrenKey: string = 'children'
) => {
  /**
   * 转换对象
   * @param data 树形结构数据
   * @returns 返回UI组件认可的包含key、title、children属性的树形结构数据
   */
  const convertTree = (data: any[]): any[] => {
    return data.map((item) => ({
      key: item[idKey],
      title: item[nameKey],
      children:
        item[childrenKey] && item[childrenKey].length > 0 ? convertTree(item[childrenKey]) : []
    }))
  }

  /**
   *
   * @param savedKeys 授权已分配的ID列表
   * @param treeData 框架规定的treeData
   * @returns
   */
  const loadCheckState = (savedKeys: number[] = [], treeData: any[]) => {
    //选中数组
    const checkedKeysTemp: number[] = []
    //半选中数组
    const halfCheckedKeysTemp: number[] = []

    const checkNodeStatus = (node) => {
      //若本节点为叶子节点且ID列表包含节点的key值,则加入到选中数组中
      if (node.children.length === 0 && savedKeys.includes(node.key)) {
        checkedKeysTemp.push(node.key)
      }
      //若本节点为非叶子节点
      if (node.children.length > 0) {
        const isAllLeaf = node.children.every((child) => child.children.length === 0)
        //子节点都为叶子节点
        if (isAllLeaf) {
          //若叶子节点被选中,则加入到选中数组中
          for (let item of node.children) {
            if (savedKeys.includes(item.key)) {
              checkedKeysTemp.push(item.key)
            }
          }
          //若子节点都被选中,则该节点为被选中
          const allChildrenChecked = node.children.every((child) => savedKeys.includes(child.key))
          if (allChildrenChecked) {
            checkedKeysTemp.push(node.key)
            console.log(checkedKeysTemp)
          } else {
            //若子节点部分被选中,则该节点为半选中
            const someChildrenChecked = node.children.some((child) => savedKeys.includes(child.key))
            if (someChildrenChecked) {
              halfCheckedKeysTemp.push(node.key)
            }
          }
        } else {
          //若子节点不是都为叶子节点
          for (let item of node.children) {
            //子节点进行迭代
            if (item.children.length > 0) {
              item.children.forEach(checkNodeStatus)
            } else {
              checkNodeStatus(item)
            }
          }
          //迭代完子节点,继续判断该节点是否被选中
          const allChildrenChecked = node.children.every((child) =>
            checkedKeysTemp.includes(child.key)
          )
          //若子节点都被选中且不是半选中,则该节点为被选中
          if (allChildrenChecked) {
            checkedKeysTemp.push(node.key)
          } else {
            //若子节点部分被选中,则该节点为半选中
            const someChildrenChecked = node.children.some((child) => savedKeys.includes(child.key))
            if (someChildrenChecked) {
              halfCheckedKeysTemp.push(node.key)
            }
          }
        }
      }
    }

    // treeData 是你的树形结构的数据
    treeData.forEach(checkNodeStatus)

    return checkedKeysTemp
  }

  return {
    convertTree,
    loadCheckState
  }
}

由于a-tree的组件还是经常使用的,我将此算法封装为组合式函数useTreeConverter.ts中了,后面其他代码,也能复用该算法逻辑。

它包含两个逻辑:

一是将自定义的树形结构转换为组件识别的形如{children:'children', title:'title', key:'key' }的树形结构TreeData

二是给定形如[1,3,5,7,11]的授权ID列表和转换后的树形结构TreeData通过递归迭代的方式还原出哪些是checkedKeys的值。

该组合式函数的代码写一次就够了,后面其他使用a-tree的组件都用得上。提供2个服务端返回的授权列表和树形结构的参数输入,再调用提供的convertTreeloadCheckState的2个函数就可以得出结果了。

相关推荐
网易独家音乐人Mike Zhou1 分钟前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
活宝小娜1 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点1 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow1 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o1 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
开心工作室_kaic2 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
刚刚好ā2 小时前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue
沉默璇年3 小时前
react中useMemo的使用场景
前端·react.js·前端框架
yqcoder4 小时前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript
2401_882727574 小时前
BY组态-低代码web可视化组件
前端·后端·物联网·低代码·数学建模·前端框架