Tree数据处理
Tree组件的数据处理往往需要使用递归,本文归纳一下常见的数据处理情景,持续更新;
·
文章目录
Tree组件的数据处理往往需要使用递归,本文归纳一下常见的数据处理情景,持续更新;
一、Tree数据重置
- 递归的标志就是寻找子元素的集合字段,一般为children,将所有节点依次过滤,
- 遍历过程类似于先序遍历,递归得到的返回值重新组成新的children数据,这个过程类似于后序遍历
- 遍历过程主要是增加必要的属性比如value、key,还可以根据节点数据动态设置icon
- 注意展开项expandedKeys的收集,根据数据自主控制
import { SmileOutlined } from "@ant-design/icons";
import { Button, Form, Tree } from "antd";
import React, { useMemo } from "react";
const treeDataTest = [
{ name: "0", id: "0", children: [{ name: "1", id: "0-0" }] },
{
name: "1",
id: "1",
sub: [
{
name: "1-0",
id: "1-0",
children: [
{ name: "1-0-0", id: "1-0-0" },
{ name: "1-0-1", id: "1-0-1" }
]
},
{
name: "2-0",
id: "2-0",
sub: [
{
name: "2-0-0",
id: "2-0-0",
sub: [{ name: "2-0-0-0", id: "2-0-0-0", children: [{ name: "2-0-0-0-0", id: "2-0-0-0-0" }] }]
}
]
}
]
}
];
export default function TreePage() {
const [form] = Form.useForm();
const [expandedKeys, setExpandedKeys] = React.useState([]);
const labelWarpBtn = {
offset: 6,
span: 14
};
const onValuesChange = (changedValues, allValues) => {
console.log("changedValues: ", changedValues);
console.log("allValues: ", allValues);
};
// 转换每一个节点,只区分children、sub属性、icon属性
const transformData = (data, expandedKeys) => {
data.forEach((item) => {
item.title = item.name;
item.key = item.id;
if (item.children) {
expandedKeys.push(item.key);
item.children = transformData(item.children, expandedKeys);
} else if (item.sub) {
item.children = transformData(item.sub, expandedKeys);
} else {
item.icon = <SmileOutlined />;
}
});
return data;
};
// 单纯过滤数据添加属性
const treeData = useMemo(() => {
const expandedKeys = [];
const data = transformData(treeDataTest, expandedKeys);
setExpandedKeys(expandedKeys);
return data;
}, [treeDataTest]);
return (
<div>
<Form form={form} labelCol={{ span: 6 }} wrapperCol={{ span: 14 }} onValuesChange={onValuesChange}>
<Form.Item name="tree" label="tree">
{/* fieldNames={{ title: "name", key: "id", children: "children" }} */}
{/* 这里只是初步定义,只能扩充三个字段,想要更灵活的属性,在数据层修改就好了 */}
{/* 一般处理数据后还要关注expandedKeys等属性 */}
<Tree treeData={treeData} expandedKeys={expandedKeys} onExpand={setExpandedKeys} showIcon></Tree>
</Form.Item>
<Form.Item wrapperCol={labelWarpBtn}>
<Button type="primary" htmlType="submit" onClick={console.log(form.getFieldsValue())}>
Submit
</Button>
</Form.Item>
</Form>
</div>
);
}
二、Tree拆分成二级数据
1、过滤数据
- 过滤不存在有效数据的节点,假设num表示该节点下级存在的有效数据的数量,通过num可以进行空数据过滤
- 递归中遇到报错可以使用debugger查看问题,调用次数太多使用console也无法定位
const treeDataTest = [
{ name: "0", id: "0", children: [{ name: "1", id: "0-0" }] },
{
name: "1",
id: "1",
sub: [
{
name: "1-0",
id: "1-0",
children: [
{ name: "1-0-0", id: "1-0-0" },
{ name: "1-0-1", id: "1-0-1" }
]
},
{
name: "2-0",
id: "2-0",
sub: [
{
name: "2-0-0",
id: "2-0-0",
sub: [{ name: "2-0-0-0", id: "2-0-0-0", children: [{ name: "2-0-0-0-0", id: "2-0-0-0-0" }] }]
}
]
}
]
}
];
// 过滤数据,只展示存在有效数据的节点
const filterData = (data) => {
const filter = (arr) => {
return arr.filter((item) => {
if (item.num > 0) {
// debugger;
if (item.sub?.length > 0) {
item.sub = filter(item.sub);
}
return true;
}
return false;
});
};
return filter(JSON.parse(JSON.stringify(data)));
};
2、二级数据
- 当需要拆分成两级时,需要把中间层级省略,保留末端children数据(假设有效数据都保存在children中)
- 设定目标数据的层级为两级,就可以遍历最外层,而内层递归,逐个往数组里添加末端children数据
import { SmileOutlined } from "@ant-design/icons";
import { Button, Form, Tree } from "antd";
import React, { useCallback, useMemo } from "react";
const treeDataTest = [
{ name: "0", id: "0", num: 1, children: [{ name: "1", id: "0-0" }], sub: [] },
{
name: "1",
id: "1",
num: 3,
sub: [
{
name: "1-0",
id: "1-0",
num: 2,
children: [
{ name: "1-0-0", id: "1-0-0" },
{ name: "1-0-1", id: "1-0-1" }
]
},
{
name: "2-0",
id: "2-0",
num: 1,
sub: [
{
name: "2-0-0",
id: "2-0-0",
num: 1,
sub: [{ name: "2-0-0-0", id: "2-0-0-0", num: 1, children: [{ name: "2-0-0-0-0", id: "2-0-0-0-0" }] }]
}
]
}
]
},
{
name: "2",
id: "2",
num: 0,
sub: [{ name: "2-0", id: "2-0", num: 0, sub: [{ name: "2-0-0", id: "2-0-0", num: 0 }] }]
}
];
export default function TreePage() {
const [form] = Form.useForm();
const [expandedKeys, setExpandedKeys] = React.useState([]);
const labelWarpBtn = {
offset: 6,
span: 14
};
const onValuesChange = (changedValues, allValues) => {
console.log("changedValues: ", changedValues);
console.log("allValues: ", allValues);
};
// 转换为二级树结构,方便展示数据
const transformChildrenOnly = (data) => {
data.forEach((item) => {
item.title = item.name;
item.key = item.id;
item.icon = <SmileOutlined />;
});
return data;
};
const transformChildren = (data, arr) => {
data.forEach((item) => {
// 这里递归的条件仅限于sub,因为他是叶子节点,不被需要
// 如果有level层级,可以采取更灵活的条件去拆分数据
if (item.children) {
arr.push(...transformChildrenOnly(item.children));
} else if (item.sub) {
transformChildren(item.sub, arr);
}
});
return data;
};
const transformDataToSecondTree = useCallback((data) => {
const newData = [];
const expandedKeys = [];
data.forEach((item) => {
const arr = [];
item.title = item.name;
item.key = item.id;
expandedKeys.push(item.key);
if (item.children) {
// 如果第二层就是children
arr.push(...transformChildrenOnly(item.children));
} else if (item.sub) {
// 如果第二层是sub属性,sub代表他是叶子节点,不是最终节点
transformChildren(item.sub, arr);
}
newData.push({ ...item, children: arr });
});
setExpandedKeys(expandedKeys);
return newData;
}, []);
// 过滤数据,只展示存在有效数据的节点
const filterData = (data) => {
const filter = (arr) => {
return arr.filter((item) => {
if (item.num > 0) {
// debugger;
if (item.sub?.length > 0) {
item.sub = filter(item.sub);
}
return true;
}
return false;
});
};
return filter(JSON.parse(JSON.stringify(data)));
};
// 转换为二级树结构
const treeData = useMemo(() => transformDataToSecondTree(filterData(treeDataTest)), [treeDataTest]);
return (
<div>
<Form form={form} labelCol={{ span: 6 }} wrapperCol={{ span: 14 }} onValuesChange={onValuesChange}>
<Form.Item name="tree" label="tree">
{/* fieldNames={{ title: "name", key: "id", children: "children" }} */}
{/* 这里只是初步定义,只能扩充三个字段,想要更灵活的属性,在数据层修改就好了 */}
{/* 一般处理数据后还要关注expandedKeys等属性 */}
<Tree treeData={treeData} expandedKeys={expandedKeys} onExpand={setExpandedKeys} showIcon></Tree>
</Form.Item>
<Form.Item wrapperCol={labelWarpBtn}>
<Button type="primary" htmlType="submit" onClick={console.log(form.getFieldsValue())}>
Submit
</Button>
</Form.Item>
</Form>
</div>
);
}
三、搜索
1、所有节点
所有节点都进行属性搜索,回调函数进行判断
// 搜索
const searchTree = (data, value) => {
const filter = (arr, callback) => {
return arr
.map((item) => ({ ...item }))
.filter((item) => {
// 假设item.children默认为空数组[]
item.children = item.children && filter(item.children, callback);
// 1、children存在元素,意味着他的子节点满足搜索条件,返回true
// 2、callback(item)为true,意味着该节点自己满足条件,返回true
return callback(item) || item.children?.length > 0;
});
};
const searchFunc = (item) => {
return item?.title?.includes(value);
};
return filter(JSON.parse(JSON.stringify(data)), searchFunc);
};
2、最末一级保留子节点
- 如果最后一级才有实际作用(比如城市地址选择最后一级才有用),那么当最后一级的父级满足条件,展示父级下所有的子节点
- 其余情况依旧是筛选所有节点
const searchTreeSaveLast = (data, value) => {
const filter = (arr, callback) => {
return arr
.map((item) => ({ ...item }))
.filter((item) => {
// 保存children
const children = item.children;
// 假设item.children默认为空数组[]
item.children = item.children && filter(item.children, callback);
// 如果最末一级的父元素,满足条件,保留所有子节点
// islastparent代表是否是最后一级父元素
if (callback(item) && item.islastparent) {
item.children = children;
return true;
}
// 1、children存在元素,意味着他的子节点满足搜索条件,返回true
// 2、callback(item)为true,意味着该节点自己满足条件,返回true
return callback(item) || item.children?.length > 0;
});
};
const searchFunc = (item) => {
return item?.title?.includes(value);
};
return filter(JSON.parse(JSON.stringify(data)), searchFunc);
};
3、找到满足条件的第一个节点
const findNodeIndex = (tree: any[], func: (obj: any) => boolean): { index: number; node: any } => {
for (let i = 0; i < tree.length; i++) {
if (func(tree[i]))
return {
index: i,
node: tree[i],
};
if (tree[i].children) {
const res = findNodeIndex(tree[i].children, func);
if (res) return res;
}
}
return null;
};
四、节点遍历
1、遍历、筛选
// 遍历
// 应该是没有中序遍历的
const mapTree = (arr) => {
arr.forEach((item) => {
// 前序遍历的操作位置
item.type = 1;
if (item.children) {
mapTree(item.children);
}
// 后序遍历的操作位置
item.count = item.children.length;
});
};
// filter 有返回值,筛选子节点
const filterTree = (arr) => {
return arr.filter((item) => {
// 前序遍历的操作位置
item.type = 1;
if (item.children) {
item.children = filterTree(item.children);
}
// 后序遍历的操作位置
item.count = item.children.length;
return item.children?.length;
});
};
// 层序遍历,对于同一层的节点进行处理
const sequenceTree = (arr) => {
if (arr.length <= 0) return;
const queue = [...arr];
while (queue.length) {
const item = queue.shift();
console.log("item: ", item);
if (item?.children) {
queue.push(...item.children);
}
}
};
五、常见问题
1、默认展开问题
- autoExpandParent默认为false,比如1-11-111三级,你的必须设置展开项为[1,11,111]这样才可以展开所有
- 如果不需要默认,那你无需设置expandedKey、autoExpandParent
- 如果需要默认,一种是你按照规范传值,一种是改为autoExpandParent=true,设置展开项[111],也能展开各级
- 这里获取所有末级节点展开,autoExpandParent=true,注意在onExpand时再设置为false,这样点击展开图标也会正常,expandedKey数据依旧是包含末级节点的;
- 这个展开项有点绕,相比selectedKeys和checkedKeys对于单个元素的确定性,expandedKey具有展示的区别
// 递归获取所有节点的 key 叶子节点
const allKeys = useCallback((data: IPushTreeItem[]): string[] => {
return data.reduce((acc: string[], item: IPushTreeItem) => {
if (item.isLeaf) {
acc.push(item.key);
}
if (item.children && item.children.length) {
acc.push(...allKeys(item.children));
}
return acc;
}, []);
}, []);

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐
所有评论(0)