前言
之前公司的业务涉及树节点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多行

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