前端开发:React + Antd 打造强大树形结构数据操作

前端开发的世界里,树结构数据展示是一个既强大又复杂的领域。当我们需要呈现具有层级关系的数据时,树结构无疑是一种极佳的选择。然而,随着数据量的增大,全量返回数据就显得不那么明智了,就像我们今天要探讨的这个案例一样。

想象一下,我们有一个庞大的树结构数据,为了提高性能和用户体验,我们采用了按需加载的方式,即只有当树节点展开时才加载当前展开的子节点。这种方式在很多应用场景中都非常实用,比如文件目录管理系统、组织架构图展示等。

但是,这也带来了一系列的挑战。树结构的展示往往伴随着许多交互功能,像同一节点内的上移、下移、置顶、置底、删除等操作。这些看似简单的操作,在这种展开加载的树形结构数据中,实现起来却没那么容易。

图片

每次进行这些操作时,仅仅请求树的根节点是无法准确还原操作之前具体展示和加载的树节点状态的。这就需要我们在前端进行巧妙的数据处理和视图更新。

在这里,我们以 React 和 Antd 中的 tree 结构组件为例,为大家详细剖析前端是如何应对这些挑战的。

首先,我们来看测试数据。我们创建了一个具有层级关系的树结构数据,它有不同层级的节点,每个节点都有自己的 id、pid(父节点 id)和名称,还有子节点列表。这就像是构建了一个小小的数据世界,每个节点都有它的位置和角色。

const treeData = [
  {
    id: 1,
    pid: -1,
    name: '节点1',
    children: [
      {
        id: 2,
        pid: 1,
        name: '节点2',
        children: [
          {
            id: 4,
            pid: 2,
            name: '节点4',
            children: [],
          },
          {
            id: 5,
            pid: 2,
            name: '节点5',
            children: [],
          },
          {
            id: 6,
            pid: 2,
            name: '节点6',
            children: [],
          },
        ],
      },
      {
        id: 3,
        pid: 1,
        name: '节点3',
        children: [
          {
            id: 7,
            pid: 3,
            name: '节点6',
            children: [],
          },
        ],
      },
    ],
  },
];

接着是我们的树组件。在这个组件中,有几个关键的部分。

我们使用 useState 来管理数据源,它就像是这个树结构的“大脑”,存储着所有的信息。当我们需要更新数据时,通过 updateDataSource 函数来更新这个“大脑”的记忆。

对于节点的移动操作,比如 handleMove 函数,这里面有着复杂而精妙的逻辑。我们要先找到节点的父节点,然后根据移动的方向(置顶、置底、上移、下移)来确定新的索引位置。这里需要考虑边界情况,比如当节点已经在顶部或者底部时,就不能再继续向上或者向下移动了。当新的索引位置确定后,我们要对数据进行重新排列,并且更新每个子节点的索引值,最后通过 updateDataSource 函数来刷新整个树的视图,让用户看到节点已经移动到了新的位置。

  const handleMove = (record, direction) => {
    const parent = findParent(record.id, dataSource);
    if (!parent) return;

    const children = [...parent.children];
    const index = children.findIndex((child) => child.id === record.id);
    const lastIndex = children.length - 1;

    let newIndex = index;

    if (direction === 'top' && index > 0) {
      newIndex = 0;
    } else if (direction === 'bottom' && index < lastIndex) {
      newIndex = lastIndex;
    } else if (direction === 'up' && index > 0) {
      newIndex = index - 1;
    } else if (direction === 'down' && index < lastIndex) {
      newIndex = index + 1;
    }

    if (newIndex !== index) {
      const [movedItem] = children.splice(index, 1);
      children.splice(newIndex, 0, movedItem);

      children.forEach((child, idx) => {
        child.index = idx;
      });

      parent.children = children;
      updateDataSource([...dataSource]);
    } else {
      message.warning('无法移动');
    }
  };

再说说删除操作,handleDelete 函数利用了 removeNodesByIdFromRoot 函数,它会从根节点开始遍历整个树结构,过滤掉要删除的节点及其所有子节点,然后更新数据源,让树视图中不再显示这些被删除的节点(删除过滤方法是传的 ids 数组,由于table组件可以多选,所以可以多个删除)。

  /** 根据 ids 删除 树形数据 */
  const removeNodesByIdFromRoot = (root: any[], ids: number[]) => {
    const data = root;
    const newTree = data.filter((x) => !ids.includes(x.id));
    newTree.forEach((x) => x.children && (x.children = removeNodesByIdFromRoot(x.children, ids)));
    return newTree;
  };

  const handleDelete = (record) => {
    const newData = removeNodesByIdFromRoot(dataSource, [record.id]);
    updateDataSource([...newData]);
  };

还有一个很重要的辅助函数 findParent,它用于在树结构中查找指定节点的父节点。这个函数通过递归的方式,深入到树的各个层级去寻找,就像一个精准的探测器,确保我们在进行各种操作时能准确找到相关的父节点信息。

  const findParent = (id, nodes) => {
    for (let node of nodes) {
      if (node.children) {
        if (node.children.find((child) => child.id === id)) {
          return node;
        }
        const parent = findParent(id, node.children);
        if (parent) return parent;
      }
    }
    return null;
  };

在操作按钮的渲染部分,我们通过 canMove 函数来判断每个节点是否可以进行相应的操作。根据节点在父节点的子节点列表中的位置,来确定置顶、置底、上移、下移按钮是否可用。

  const canMove = (record) => {
    const parent = findParent(record.id, dataSource);
    const children = parent ? parent.children : [];
    const index = children.findIndex((child) => child.id === record.id);

    return {
      canTop: index > 0,
      canBottom: index < children.length - 1,
      canUp: index > 0,
      canDown: index < children.length - 1,
    };
  };

最后,我们将这个精心打造的树组件在 Table 中进行展示,通过配置 columns 和其他属性,让它以一个美观且易用的形式呈现给用户。

在前端开发中,处理这种树形结构数据的操作就像是一场精细的手术,每一个步骤都需要精确无误。希望通过我们对这个 React 和 Antd 树结构组件操作的详细讲解,能更好地应对类似的挑战,打造出更优秀的用户界面和交互体验。

图片

完整代码(根据实际数据情况进行微调)

import { Button, Space, Table, message } from 'antd';
import React, { useState } from 'react';

const treeData = [
  {
    id: 1,
    pid: -1,
    name: '节点1',
    children: [
      {
        id: 2,
        pid: 1,
        name: '节点2',
        children: [
          {
            id: 4,
            pid: 2,
            name: '节点4',
            children: [],
          },
          {
            id: 5,
            pid: 2,
            name: '节点5',
            children: [],
          },
          {
            id: 6,
            pid: 2,
            name: '节点6',
            children: [],
          },
        ],
      },
      {
        id: 3,
        pid: 1,
        name: '节点3',
        children: [
          {
            id: 7,
            pid: 3,
            name: '节点6',
            children: [],
          },
        ],
      },
    ],
  },
];

const TreeTable = () => {
  const [dataSource, setDataSource] = useState(treeData);

  const updateDataSource = (newData) => {
    setDataSource([...newData]);
  };

  const handleMove = (record, direction) => {
    const parent = findParent(record.id, dataSource);
    if (!parent) return;

    const children = [...parent.children];
    const index = children.findIndex((child) => child.id === record.id);
    const lastIndex = children.length - 1;

    let newIndex = index;

    if (direction === 'top' && index > 0) {
      newIndex = 0;
    } else if (direction === 'bottom' && index < lastIndex) {
      newIndex = lastIndex;
    } else if (direction === 'up' && index > 0) {
      newIndex = index - 1;
    } else if (direction === 'down' && index < lastIndex) {
      newIndex = index + 1;
    }

    if (newIndex !== index) {
      const [movedItem] = children.splice(index, 1);
      children.splice(newIndex, 0, movedItem);

      children.forEach((child, idx) => {
        child.index = idx;
      });

      parent.children = children;
      updateDataSource([...dataSource]);
    } else {
      message.warning('无法移动');
    }
  };

  /** 根据 ids 删除 树形数据 */
  const removeNodesByIdFromRoot = (root: any[], ids: number[]) => {
    const data = root;
    const newTree = data.filter((x) => !ids.includes(x.id));
    newTree.forEach((x) => x.children && (x.children = removeNodesByIdFromRoot(x.children, ids)));
    return newTree;
  };

  const handleDelete = (record) => {
    const newData = removeNodesByIdFromRoot(dataSource, [record.id]);
    updateDataSource([...newData]);
  };

  const findParent = (id, nodes) => {
    for (let node of nodes) {
      if (node.children) {
        if (node.children.find((child) => child.id === id)) {
          return node;
        }
        const parent = findParent(id, node.children);
        if (parent) return parent;
      }
    }
    return null;
  };

  const canMove = (record) => {
    const parent = findParent(record.id, dataSource);
    const children = parent ? parent.children : [];
    const index = children.findIndex((child) => child.id === record.id);

    return {
      canTop: index > 0,
      canBottom: index < children.length - 1,
      canUp: index > 0,
      canDown: index < children.length - 1,
    };
  };

  const columns = [
    {
      title: '名称',
      width: 350,
      dataIndex: 'name',
    },
    {
      title: '操作',
      dataIndex: 'oper',
      align: 'center',
      width: 200,
      render(_, record) {
        const { canTop, canBottom, canUp, canDown } = canMove(record);
        return (
          <Space>
            <Button onClick={() => handleMove(record, 'top')} disabled={!canTop}>
              置顶
            </Button>
            <Button onClick={() => handleMove(record, 'bottom')} disabled={!canBottom}>
              置底
            </Button>
            <Button onClick={() => handleMove(record, 'up')} disabled={!canUp}>
              上移
            </Button>
            <Button onClick={() => handleMove(record, 'down')} disabled={!canDown}>
              下移
            </Button>
            <Button onClick={() => handleDelete(record)}>删除</Button>
          </Space>
        );
      },
    },
  ];

  return (
    <Table
      rowKey="id"
      columns={columns}
      dataSource={dataSource}
      rowSelection={{}}
      pagination={false}
    />
  );
};

export default TreeTable;

来源: 程序员Rain web前端智汇堂

THE END