树节点key不唯一的勾选、展开状态的处理思路

前言

之前公司的业务涉及树节点key不唯一的树,之前写过一篇文章记录 # 多维度子树、树节点key不唯一的Tree组件封装

这个业务的树结构在前端处理后如下:

我们可以看到,A树下面有个节点C,H树下也有个节点C,这两个C原本的key就是一样的。所以前端在处理数据结构时,每个节点都会拼上父节点的key作为新的key来避免key不唯一的情况

然后为了快速读取节点,在处理数据结构时,维护了一个map,结构如下:

其中,originData是每个节点原本的数据。children则是保存了A节点下的所有子节点

在了解了数据结构的情况下,之前文章的交互是勾选、取消勾选是节点各管各的。但本文的交互是:

  • 如果勾选、取消勾选时,勾的是父节点,父节点不做选中态处理,而它下面的所有子节点,包括其他父节点下相同key的子节点都要处理选中态
  • 如果勾选、取消勾选时,勾的是子节点,则把所有相同key的子节点(不同父节点下可能存在相同子节点)都处理选中态

然后我在处理逻辑的过程中感觉存在挺多小问题,于是本文就是记录下思路。因为代码实在太长了

选中时

先看下面的截图,我描述下交互流程

图一,当我点击【我的名字超级长】父节点时,只处理子节点的选中态,也就是【张飞】、【测试】,又因为【张飞】这个节点也存在于【咨询部】这个父节点下,所以也要勾选上。

然后我们来说实现思路

首先,我们每次选中时,只处理最新选中的节点。之前选中的节点不再处理。

这种情况又分两种大情况

  • 勾选的是父节点。
    • 如果当前父节点的所有子节点在上一次已经被选中了,则本次就应该取消选中所有子节点
    • 如果当前父节点的某些子节点在上一次被选中了,相当于选了但没全选,则本次就应该全选所有子节点
    • 如果当前父节点的所有子节点在上一次没有被选中,则本次就应该全选所有子节点
  • 勾选的是子节点。
    • 把所有相同key的子节点都选上

看得出来,父节点勾选时比较复杂。所以我们重点理一下父节点的这三种情况

勾选的是父节点

如果当前父节点的所有子节点在上一次已经被选中了

这种情况下,勾选父节点后,所有相关的子节点就取消勾选。

思路是:

  • 当前父节点下的所有子节点取消勾选。
  • 然后再去查找其他父节点下有没有相同key的子节点。如果有的话一并取消勾选

下面代码的入口函数是 onCancelPrevParentChildChecked

tsx 复制代码
// currentParentKey是当前我们勾选的父节点的key
onCancelPrevParentChildChecked(currentParentKey: string) {
    let currentSameNodesKeys: string[] = [];
    let result: TTXTreeCascaderNode[] = [];
    // 取消选中当前父节点下的所有子节点
    const {
      newCheckedNodes,
      sameNodesKeys,
    } = this.onCancelCurrentParentsChildCheckedStatus(currentParentKey)
    result = newCheckedNodes;
    currentSameNodesKeys = sameNodesKeys;
    // 如果其他父节点下也存在相同key的子节点,一并取消勾选
    if (sameNodesKeys.length) {
      result = result.reduce((prev: TTXTreeCascaderNode[], node) => {
        if (!currentSameNodesKeys.includes(node.key as string)) {
          return [
            ...prev,
            node
          ]
        }
        return prev
      }, [])
    }
    return result
  }
  
  onCancelCurrentParentsChildCheckedStatus(currentParentKey: string) {
    const newCheckedNodes: TTXTreeCascaderNode[] = [];
    const sameNodesKeys: string[] = []
    this.checkedNodes.forEach((node) => {
      // 如果当前节点不是当前父节点的子节点, 记录下来
      if (!(node.key as string).includes(currentParentKey)) {
        newCheckedNodes.push(node)
      } else {
        // 否则, 再判断其他父节点下是否有相同key的子节点.如果有,则记录相同节点key
        const hasSameKeyNodes = this.onFindAllSameKeyNode([node])
        if (hasSameKeyNodes.length) {
          hasSameKeyNodes.forEach((node) => {
            sameNodesKeys.push(node.key as string)
          })
        }
      }
    })
    return {
      newCheckedNodes,
      sameNodesKeys
    }
  }
  
  onFindAllSameKeyNode(nodes: TTXTreeCascaderNode[]) {
    let sameKeyNodes: TTXTreeCascaderNode[] = []
    nodes.forEach((n) => {
      const key = (n.key as string).split('_')[n.level ?? 1]
      const sameNodes = this.onGetNodeWithTxTreeHeleper(key) ?? [];
      sameKeyNodes = [...new Set([...sameKeyNodes, ...sameNodes])];
    })
    return sameKeyNodes;
  }

上面代码中,onCancelCurrentParentsChildCheckedStatus思路如下

  • 遍历之前选择的节点列表,判断当前节点是不是我们勾选的这个父节点的子节点
    • 如果不是,则记录它的key(用于后续去查找其他父节点下有没有相同key的节点)
    • 如果是,则过滤掉它

这一步后,我们就得到了应该保留勾选状态的节点列表 newCheckedNodes,以及需要去判断是否存在于其他父节点下的子节点key列表sameNodesKeys

然后继续下一步,遍历 sameNodesKeys 判断其他父节点下有没有相同key的子节点,有的话,也要过滤掉。

而这一步我们直接从前文我们说过的 Map 去拿,也就是代码中的

tsx 复制代码
const key = (n.key as string).split('_')[n.level ?? 1]
const sameNodes = this.onGetNodeWithTxTreeHeleper(key) ?? [];

然后,再去滤掉上一步我们上一步得到的 newCheckedNodes

tsx 复制代码
if (sameNodesKeys.length) {
      result = result.reduce((prev: TTXTreeCascaderNode[], node) => {
        if (!currentSameNodesKeys.includes(node.key as string)) {
          return [
            ...prev,
            node
          ]
        }
        return prev
      }, [])
    }

这样 result 就是当前最新的计算了勾选状态的节点集合。

如果当前父节点的某些子节点在上一次被选中了

这种情况下,相当于选了但没全选,则本次就应该全选所有子节点

tsx 复制代码
onSetCheckedStatusWhenCheckChildNode(truthKeys: string[]) {
    const checkedNodes: TTXTreeCascaderNode[] = []
    truthKeys.filter(Boolean).forEach((key) => {
      const currentParentNode = this.onGetNodeWithTxTreeHeleper(key) ?? []
      currentParentNode.forEach(root => generateAllChildNode(root, checkedNodes));
    })
    return checkedNodes
}

同样,先从 Map 里面拿到我们这个父节点,然后递归去查找它的所有叶子节点 checkedNodes

然后,我们还要去判断checkedNodes 里面有没有节点同时存在于其他父节点下,有的话一并勾上

tsx 复制代码
const newSameNodes = this.onFindAllSameKeyNode(checkedNodes);
const result = [...newSameNodes, ...this.checkedNodes]
this.checkedNodes = result;
this.checkedKeys = result.map((node) => node.key as string);
this.expandKeys = [...new Set(this.generateExpandKeysWithCheckedNodes())]

generateExpandKeysWithCheckedNodes(nodes?: TTXTreeCascaderNode[]) {
    return (nodes ?? this.checkedNodes)
      .map((node) => node.parentId)
      .reduce(
        (prev: string[], parentId) => [
          ...prev,
          ...splitUnderscoreString(parentId),
        ],
        []
      );
  }

找到其他父节点下相同key的子节点,得到 result,然后再去计算下 expandKeys

如果当前父节点的所有子节点在上一次没有被选中

其实这种情况下,代码逻辑直接复用 如果当前父节点的某些子节点在上一次被选中了 的情况即可。因为他们都是要把这个父节点下的所有子节点选中,然后同时把其他父节点下的相同key子节点也选中

勾选的是子节点

这个简单,直接根据 子节点 key 从 Map 里面就可以拿到处理后的子节点。然后同步到 checkedNodes 即可。

用的都是上面提及到的代码

ini 复制代码
onSetCheckedStatusWhenCheckChildNode(truthKeys: string[]) {
    const checkedNodes: TTXTreeCascaderNode[] = []
    truthKeys.filter(Boolean).forEach((key) => {
      const currentParentNode = this.onGetNodeWithTxTreeHeleper(key) ?? []
      currentParentNode.forEach(root => generateAllChildNode(root, checkedNodes));
    })
    return checkedNodes
}
  
checkedNodes = [...checkedNodes, ...this.onSetCheckedStatusWhenCheckChildNode(childTruthKey)];
const newSameNodes = this.onFindAllSameKeyNode(checkedNodes);

const result = [...newSameNodes, ...this.checkedNodes]
this.checkedNodes = result;
this.checkedKeys = result.map((node) => node.key as string);
this.expandKeys = [...new Set(this.generateExpandKeysWithCheckedNodes())];

取消选中

取消选中时,由于业务不记录父节点的选中态。所以取消勾选时,如果勾选的是父节点,走的逻辑和勾选时是一样的。不再赘述

如果取消勾选时勾的是子节点,直接遍历 checkedNodes,过滤掉当前选中的节点key以及相同key的节点即可

tsx 复制代码
onCanlceChecked(currentKey: string) {
    const key = currentKey.split('_')[currentKey.split('_').length - 1];
    const checkedNodes: TTXTreeCascaderNode[] = this.checkedNodes.reduce((prev: TTXTreeCascaderNode[], node) => {
      const nodeKey = (node.key as string).split('_')[node.level ?? 1];
      return nodeKey !== key ? [
        ...prev,
        node
      ] : prev
    }, [])
    const checkedKeys: string[] = checkedNodes.map((node) => node.key as string);
    this.checkedNodes = checkedNodes;
    this.checkedKeys = checkedKeys;
  }

结尾

总的来说,思路理清楚了,也不算很难,但确实比较复杂。实际逻辑处理的代码量也400多行

后续看还能不能再优化一下。希望能给有类似业务的小伙伴一点思路

相关推荐
我今晚不熬夜22 分钟前
使用单调栈解决力扣第42题--接雨水
java·数据结构·算法·leetcode
拾光拾趣录38 分钟前
基础 | 🔥6种声明方式全解⚠️
前端·面试
flashlight_hi1 小时前
LeetCode 分类刷题:209. 长度最小的子数组
javascript·算法·leetcode
展信佳_daydayup2 小时前
0-1 深度学习基础——文件读取
算法
朱程2 小时前
AI 编程时代手工匠人代码打造 React 项目实战(四):使用路由参数 & mock 接口数据
前端
高斯林.神犇2 小时前
冒泡排序实现以及优化
数据结构·算法·排序算法
PineappleCoder2 小时前
深入浅出React状态提升:告别组件间的"鸡同鸭讲"!
前端·react.js
Github项目推荐2 小时前
跨平台Web服务开发的新选择(5802)
算法·架构
wycode2 小时前
Vue2源码笔记(1)编译时-模板代码如何生效之生成AST树
前端·vue.js
程序员嘉逸2 小时前
LESS 预处理器
前端