Ant Design Tree树形控件,异步加载、动态操作节点的解决方案

2,144 阅读3分钟

背景:

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

效果图

image.png

解决思路

  • 引入 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> 
);