背景:
- 一次性渲染整个树结构,节点层级过多、数据量过大时,接口效率低
- 动态增加子节点、删除节点、修改节点
- React 框架
"antd": "^5.13.2"
效果图

解决思路
- 引入 Tree 组件,异步加载数据: 展开节点时,模拟接口调用返回该节点的下一级子节点
- 右键点击节点名称,展示操作栏: 通过节点位置,计算操作栏位置
- 执行具体操作,成功后渲染树状结构: 模拟接口调用,操作完成后重新渲染树状结构,只渲染改动部分节点
详细实现
1、引入Tree组件,异步加载数据
// 初始化数据
const initTreeData = [
{
title: '展开时加载',
key: '0',
},
{
title: '展开时加载',
key: '1',
},
{
title: '叶子节点',
key: '2',
isLeaf: true,
},
];
//异步加载全部子节点
const updateTreeData = (list, key, children) =>
list.map((node) => {
if (node.key === key) {
return {
...node,
children,
};
}
if (node.children) {
return {
...node,
children: updateTreeData(node.children, key, children),
};
}
return node;
});
//引入Tree组件
import { Tree } from 'antd';
const Demo = () => {
const [treeData, setTreeData] = useState(initTreeData);、
const onLoadData = ({ key, children }) =>
new Promise((resolve) => {
if (children) {
resolve();
return;
}
setTimeout(() => {
//模拟接口返回数据
let mock = [
{
title: '子节点',
key: `${key}-0`,
},
{
title: '叶子节点',
key: `${key}-1`,
isLeaf: true,
},
];
setTreeData((origin) =>
updateTreeData(origin, key, mock),
);
resolve();
}, 1000);
});
return (
<PageContainer>
<Tree loadData={onLoadData} treeData={treeData} />
</PageContainer>
);
};
export default Demo;
2、右键点击节点名称,展示操作栏
//记录当前操作节点位置信息
const [currentNode, setCurrentNode] = useState(null);
//右键点击节点时,保存节点key(增加子节点、删除节点、修改节点时使用)及位置(动态生成操作栏)等信息
const onRightClick = ({ event, node }) => {
var x = event.currentTarget.offsetLeft + event.currentTarget.clientWidth + 40;
//具体位置根据Tree组件在页面的位置进行调整
var y = event.currentTarget.offsetParent.offsetTop + 75;
setCurrentNode({
pageX: x,
pageY: y,
key: node.key,
title: node.title,
})
}
//根据当前操作节点位置,计算操作栏位置,动态生成操作栏
const showActionBar = () => {
const { pageX, pageY } = { ...currentNode };
const tmpStyle = {
position: 'absolute',
maxHeight: 40,
textAlign: 'center',
left: `${pageX}px`,
top: `${pageY}px`,
display: 'flex',
flexDirection: 'row',
};
return <div style={tmpStyle}>
<div style={{ alignSelf: 'center', marginLeft: 10 }} onClick={addNode}>
<Tooltip placement="bottom" title="添加子节点">
<PlusSquareOutlined />
</Tooltip>
</div>
<div style={{ alignSelf: 'center', marginLeft: 10 }} onClick={updateNode}>
<Tooltip placement="bottom" title="修改节点名称">
<FormOutlined />
</Tooltip>
</div>
<div style={{ alignSelf: 'center', marginLeft: 10 }} onClick={deleteNode}>
<Tooltip placement="bottom" title="删除节点">
<MinusSquareOutlined />
</Tooltip>
</div>
</div>
}
return (
<PageContainer>
<Tree loadData={onLoadData} treeData={treeData} onRightClick={onRightClick} />
{/* 右键点击节点时,展示操作栏 */}
{currentNode ? showActionBar() : null}
</PageContainer>
);
3、执行具体操作,成功后渲染树状结构
//添加单个子节点
const addNode = () => {
const loop = (list, key, node) =>
list.forEach((item, index, array) => {
if (item.key === key) {
array[index].children ? array[index].children.push(node) : array[index].children = [node];
delete array[index].isLeaf;
} else if (item.children) {
loop(item.children, key, node);
}
});
let newTreeData = [...treeData];
//模拟数据,可以是单节点/多节点/叶子节点/非叶子节点
let mock = {
title: '我是新节点',
key: new Date().getTime(),
};
loop(newTreeData, currentNode.key, mock);
setTreeData(newTreeData);
setCurrentNode(null);
}
//修改节点名称
const updateNode = () => {
const loop = (list, key, newTitle) =>
list.forEach((item, index, array) => {
if (item.key === key) {
array[index].title = newTitle;
} else if (item.children) {
loop(item.children, key, newTitle);
}
});
let newTreeData = [...treeData];
loop(newTreeData, currentNode.key, '节点新名称');
setTreeData(newTreeData);
setCurrentNode(null);
}
//删除节点
const deleteNode = () => {
const loop = (list, key) =>
list.forEach((item, index, array) => {
if (item.key === key) {
array.splice(index, 1);
} else if (item.children) {
loop(item.children, key);
}
});
let newTreeData = [...treeData];
loop(newTreeData, currentNode.key);
setTreeData(newTreeData);
setCurrentNode(null);
}
4、优化
// 切换节点时,隐藏操作栏
const onExpand = () => {
setCurrentNode(null);
}
return (
<PageContainer>
<Tree loadData={onLoadData} treeData={treeData} onExpand={onExpand} onRightClick={onRightClick} />
{/* 右键点击节点时,展示操作栏 */}
{currentNode ? showActionBar() : null}
</PageContainer>
);