实际项目中比较常遇到树相关的处理,所以记录一下。
主要是下面几个功能
- 数据源转换成指定树结构 (目前数据源都是数组树)
- 模糊查询树 (根据节点名称)
- 查找指定key节点
- 查找指定key节点的父节点路径
- 查找指定key节点的所有子节点
ts
import { TreeDataNode } from "antd";
export type TRecord = {
[key: string]: any;
};
/**@param 转换后节点类型/
export type TNode = {
/**@param 数据源节点/
data: TRecord;
/**@param 转换后节点的子节点/
children: TNode[];
/**@param 是否高亮,用于模糊查询UI渲染/
highLight: boolean;
} & TreeDataNode;
/**@param 模糊查询树结果/
export interface IFilterTreeResult {
/**@param 模糊查询后的树/
filterTree: TNode[];
/**@param 模糊查询后的树节点key集合/
filterKeys: string[];
}
/**@param 节点与数据源节点字段映射关系/
export interface ITreeConfig {
/**@param 节点label对应的字段名 */
labelKey: string;
/**@param 节点value对应的字段名 */
valueKey: string;
/**@param 节点key对应的字段名 */
keyKey: string;
/**@param 节点children对应的字段名 */
childrenKey: string;
}
export interface ITreeProcessor {
/**@function 转换指定树结构 */
processTree(tree: TRecord[], nameKey: string, childrenName: string): TNode[];
/**@function 模糊查询树,返回过滤后的树和满足条件的节点路径 */
filterTree(predicate: (node: TNode) => boolean): IFilterTreeResult;
/**@function 获取节点key与节点本身的映射 */
getNodeMap(): Map<string, TNode>;
/**@function 获取节点key与上级父节点key的映射 */
getParentMap(): Map<string, string | null>;
/**@function 向外暴露转换后的树 */
getProcessTree(): TNode[];
/**@function 清除节点映射 */
clearNodeMap(): void;
/**@function 清除过滤匹配映射 */
clearFilterMatchMap(): void;
}
class TreeProcessor implements ITreeProcessor {
/**@param 树 */
private tree: TNode[];
/**@param */
private treeConfig: ITreeConfig;
/**@param 节点key与节点本身的映射 */
private nodeMap: Map<string, TNode>;
/**@param 节点key与父节点key的映射 */
private parentMap: Map<string, string | null>;
/**@param 匹配过滤条件节点的key集合 */
private matchedIds: Set<string>;
/**@param 匹配过滤条件节点的所有子节点key集合 */
private matchedChildrenIds: Set<string>;
constructor(config: Partial<ITreeConfig> = {}) {
this.tree = [];
this.nodeMap = new Map();
this.parentMap = new Map();
this.matchedIds = new Set<string>();
this.matchedChildrenIds = new Set<string>();
this.treeConfig = {
labelKey: config.labelKey || 'label',
valueKey: config.valueKey || 'value',
keyKey: config.keyKey || 'id',
childrenKey: config.childrenKey || 'children',
}
}
processTree(tree: TRecord[]): TNode[] {
this.clearNodeMap();
const processNode = (
nodes: TRecord[],
parentId: string | null
): TNode[] => {
return nodes.map((node) => {
const beforeNode = {
...node,
};
delete beforeNode[this.treeConfig.childrenKey];
const newNode = {
key: node[this.treeConfig.keyKey],
title: node[this.treeConfig.labelKey] ?? "",
data: beforeNode,
highLight: false,
children: [] as TNode[],
};
this.nodeMap.set(node[this.treeConfig.keyKey], newNode);
this.parentMap.set(node[this.treeConfig.keyKey], parentId);
if (Array.isArray(node[this.treeConfig.childrenKey])) {
newNode.children = processNode(node[this.treeConfig.childrenKey], node[this.treeConfig.keyKey]);
}
return newNode;
});
};
const result = processNode(tree, null);
this.tree = result;
return result;
}
filterTree(predicate: (node: TNode) => boolean) {
// 计算路径
Array.from(this.nodeMap.values())
.filter(predicate)
.forEach((node) => {
this.matchedIds.add(node.key as string);
this.markAncestors(node.key as string);
this.markChildrenAncestors(node.key as string);
});
const filterTree = this.buildFilterTree(predicate);
const filterKeys = Array.from(
new Set([...this.matchedIds, ...this.matchedChildrenIds])
);
this.clearFilterMatchMap();
return {
filterTree,
filterKeys,
};
}
getNodeMap() {
return this.nodeMap;
}
getParentMap() {
return this.parentMap;
}
getProcessTree() {
return this.tree;
}
clearNodeMap() {
this.nodeMap.clear();
this.parentMap.clear();
}
clearFilterMatchMap() {
this.matchedIds.clear();
this.matchedChildrenIds.clear();
}
/**@function 构建过滤匹配后的树 */
private buildFilterTree(predicate: (node: TNode) => boolean) {
const loop = (nodes: TNode[]): TNode[] => {
return nodes
.filter(
(node) =>
this.matchedIds.has(node.key as string) ||
this.matchedChildrenIds.has(node.key as string)
)
.map((node) => ({
...node,
highLight: predicate(node),
children: node.children ? loop(node.children as TNode[]) : [],
}));
};
return loop(this.tree);
}
/**@function 获取指定key节点的所有父节点key */
private markAncestors(nodeId: string) {
let parentId = this.parentMap.get(nodeId);
while (parentId) {
this.matchedIds.add(parentId);
parentId = this.parentMap.get(parentId);
}
}
/**@function 获取指定key节点的所有子节点key */
private markChildrenAncestors(nodeId: string) {
const node = this.nodeMap.get(nodeId);
if (!node) return;
this.matchedChildrenIds.add(nodeId);
node.children.forEach((child) =>
this.markChildrenAncestors(child.key as string)
);
}
}
export default TreeProcessor;
思路是:
- 转换树结构时,根据 treeConfig 的配置信息,构造指定的树节点,并记录相关的map信息
- 模糊查询时,过滤出匹配的节点,然后通过map去查找匹配节点的父节点key路径。然后这里因为项目需要,也去找了子节点key路径。然后再根据把匹配的节点高亮