使用 React 和 Ant Design 实现树形结构数据的搜索功能

399 阅读9分钟

前言

在前端开发中,处理树形结构的数据是一个常见的需求。无论是展示组织结构、文件目录,还是实现搜索树,都需要将扁平数据转换为树形数据,或者将树形数据转换为扁平数据。本文将介绍如何进行这些转换,并结合 antd 和 React 实现一个丝滑的搜索树。

Snipaste_2025-03-04_18-57-29.png

扁平数据转 tree 形数据

首先,我们来看如何将扁平数据转换为树形数据。假设我们有以下扁平数据结构:

const data = [
  { name: "张三", id: 1 },
  { name: "后端技术专家", id: 3, parentId: 2 },
  { name: "首席架构师", id: 2, parentId: 1 },
  { name: "后端架构师", id: 4, parentId: 3 },
  { name: "后端工程师", id: 5, parentId: 4 },
  { name: "后端菜鸟", id: 6, parentId: 5 },
  { name: "后端小白", id: 7, parentId: 6 },
  { name: "李四", id: 8 },
  { name: "支付宝", id: 9, parentId: 8 },
  { name: "淘宝", id: 10, parentId: 8 },
  { name: "天猫", id: 11, parentId: 8 },
  { name: "钉钉", id: 12, parentId: 8 },
  { name: "花呗", id: 13, parentId: 9 },
  { name: "余额宝", id: 14, parentId: 9 },
  { name: "蚂蚁森林", id: 15, parentId: 9 },
  { name: "企业版", id: 16, parentId: 12 },
  { name: "直播", id: 17, parentId: 10 },
  { name: "店铺", id: 18, parentId: 10 },
  { name: "优惠券", id: 19, parentId: 10 },
  { name: "双十一", id: 20, parentId: 11 },
  { name: "双十二", id: 21, parentId: 11 },
  { name: "年货节", id: 22, parentId: 11 },
];

转换函数

我们可以使用递归的方法将扁平数据转换为树形数据:

function arrayToTree(items: any[], parentId: number | null = null): any[] {
  return items
    // 筛选出当前层级的节点(根据 parentId 匹配)
    .filter(item => item.parentId === parentId)
    // 遍历筛选出的节点,为每个节点添加子节点
    .map(item => ({
      ...item,
      children: arrayToTree(items, item.id)
    }));
}

const treeData = arrayToTree(data);
console.log(JSON.stringify(treeData, null, 2));

输出结果

[
  {
    "name": "张三",
    "id": 1,
    "children": [
      {
        "name": "首席架构师",
        "id": 2,
        "parentId": 1,
        "children": [
          {
            "name": "后端技术专家",
            "id": 3,
            "parentId": 2,
            "children": [
              {
                "name": "后端架构师",
                "id": 4,
                "parentId": 3,
                "children": [
                  {
                    "name": "后端工程师",
                    "id": 5,
                    "parentId": 4,
                    "children": [
                      {
                        "name": "后端菜鸟",
                        "id": 6,
                        "parentId": 5,
                        "children": [
                          {
                            "name": "后端小白",
                            "id": 7,
                            "parentId": 6,
                            "children": []
                          }
                        ]
                      }
                    ]
                  }
                ]
              }
            ]
          }
        ]
      }
    ]
  },
  {
    "name": "李四",
    "id": 8,
    "children": [
      {
        "name": "支付宝",
        "id": 9,
        "parentId": 8,
        "children": [
          {
            "name": "花呗",
            "id": 13,
            "parentId": 9,
            "children": []
          },
          {
            "name": "余额宝",
            "id": 14,
            "parentId": 9,
            "children": []
          },
          {
            "name": "蚂蚁森林",
            "id": 15,
            "parentId": 9,
            "children": []
          }
        ]
      },
      {
        "name": "淘宝",
        "id": 10,
        "parentId": 8,
        "children": [
          {
            "name": "直播",
            "id": 17,
            "parentId": 10,
            "children": []
          },
          {
            "name": "店铺",
            "id": 18,
            "parentId": 10,
            "children": []
          },
          {
            "name": "优惠券",
            "id": 19,
            "parentId": 10,
            "children": []
          }
        ]
      },
      {
        "name": "天猫",
        "id": 11,
        "parentId": 8,
        "children": [
          {
            "name": "双十一",
            "id": 20,
            "parentId": 11,
            "children": []
          },
          {
            "name": "双十二",
            "id": 21,
            "parentId": 11,
            "children": []
          },
          {
            "name": "年货节",
            "id": 22,
            "parentId": 11,
            "children": []
          }
        ]
      },
      {
        "name": "钉钉",
        "id": 12,
        "parentId": 8,
        "children": [
          {
            "name": "企业版",
            "id": 16,
            "parentId": 12,
            "children": []
          }
        ]
      }
    ]
  }
]

tree 形数据转扁平数据

接下来,我们来看如何将树形数据转换为扁平数据。假设我们有以下树形数据结构:

const treeData = [
  {
    name: "张三",
    id: 1,
    children: [
      {
        name: "首席架构师",
        id: 2,
        parentId: 1,
        children: [
          {
            name: "后端技术专家",
            id: 3,
            parentId: 2,
            children: [
              {
                name: "后端架构师",
                id: 4,
                parentId: 3,
                children: [
                  {
                    name: "后端工程师",
                    id: 5,
                    parentId: 4,
                    children: [
                      {
                        name: "后端菜鸟",
                        id: 6,
                        parentId: 5,
                        children: [
                          {
                            name: "后端小白",
                            id: 7,
                            parentId: 6,
                            children: []
                          }
                        ]
                      }
                    ]
                  }
                ]
              }
            ]
          }
        ]
      }
    ]
  },
  {
    name: "李四",
    id: 8,
    children: [
      {
        name: "支付宝",
        id: 9,
        parentId: 8,
        children: [
          {
            name: "花呗",
            id: 13,
            parentId: 9,
            children: []
          },
          {
            name: "余额宝",
            id: 14,
            parentId: 9,
            children: []
          },
          {
            name: "蚂蚁森林",
            id: 15,
            parentId: 9,
            children: []
          }
        ]
      },
      {
        name: "淘宝",
        id: 10,
        parentId: 8,
        children: [
          {
            name: "直播",
            id: 17,
            parentId: 10,
            children: []
          },
          {
            name: "店铺",
            id: 18,
            parentId: 10,
            children: []
          },
          {
            name: "优惠券",
            id: 19,
            parentId: 10,
            children: []
          }
        ]
      },
      {
        name: "天猫",
        id: 11,
        parentId: 8,
        children: [
          {
            name: "双十一",
            id: 20,
            parentId: 11,
            children: []
          },
          {
            name: "双十二",
            id: 21,
            parentId: 11,
            children: []
          },
          {
            name: "年货节",
            id: 22,
            parentId: 11,
            children: []
          }
        ]
      },
      {
        name: "钉钉",
        id: 12,
        parentId: 8,
        children: [
          {
            name: "企业版",
            id: 16,
            parentId: 12,
            children: []
          }
        ]
      }
    ]
  }
];

转换函数

我们可以使用递归的方法将树形数据转换为扁平数据:

function treeToArray(tree: any[]): any[] {
  let result: any[] = [];
  tree.forEach(node => {
    // 解构出当前节点的 children 属性,其余属性保留
    const { children, ...rest } = node;
    // 将当前节点(去掉 children 属性)添加到结果数组中
    result.push(rest);
    // 如果存在子节点,递归处理子节点并将结果合并到结果数组中
    if (children) {
      result = result.concat(treeToArray(children));
    }
  });
  return result;
}

const flatData = treeToArray(treeData);
console.log(JSON.stringify(flatData, null, 2));

输出结果

[
  { "name": "张三", "id": 1 },
  { "name": "首席架构师", "id": 2, "parentId": 1 },
  { "name": "后端技术专家", "id": 3, "parentId": 2 },
  { "name": "后端架构师", "id": 4, "parentId": 3 },
  { "name": "后端工程师", "id": 5, "parentId": 4 },
  { "name": "后端菜鸟", "id": 6, "parentId": 5 },
  { "name": "后端小白", "id": 7, "parentId": 6 },
  { "name": "李四", "id": 8 },
  { "name": "支付宝", "id": 9, "parentId": 8 },
  { "name": "花呗", "id": 13, "parentId": 9 },
  { "name": "余额宝", "id": 14, "parentId": 9 },
  { "name": "蚂蚁森林", "id": 15, "parentId": 9 },
  { "name": "淘宝", "id": 10, "parentId": 8 },
  { "name": "直播", "id": 17, "parentId": 10 },
  { "name": "店铺", "id": 18, "parentId": 10 },
  { "name": "优惠券", "id": 19, "parentId": 10 },
  { "name": "天猫", "id": 11, "parentId": 8 },
  { "name": "双十一", "id": 20, "parentId": 11 },
  { "name": "双十二", "id": 21, "parentId": 11 },
  { "name": "年货节", "id": 22, "parentId": 11 },
  { "name": "钉钉", "id": 12, "parentId": 8 },
  { "name": "企业版", "id": 16, "parentId": 12 }
]

场景

在实际开发中,树形结构的数据广泛应用于各种场景,例如:

  1. 文件目录:展示文件系统中的目录结构。
  2. 菜单导航:展示网站或应用中的多级菜单。
  3. 权限管理:展示用户权限的层级关系。

结合 antd 和 React 实现丝滑的 search Tree(搜索树)

接下来,我们将结合 antd 和 React 实现一个丝滑的搜索树。首先,我们需要安装 antd:

npm install antd

示例代码

以下是一个完整的示例代码,展示如何使用 antd 和 React 实现搜索树:

import React, { useState } from 'react';
import { Tree, Input } from 'antd';
import type { DataNode } from 'antd/es/tree';

const { Search } = Input;

// 原始数据
const data = [
  { name: "张三", id: 1 },
  { name: "后端技术专家", id: 3, parentId: 2 },
  { name: "首席架构师", id: 2, parentId: 1 },
  { name: "后端架构师", id: 4, parentId: 3 },
  { name: "后端工程师", id: 5, parentId: 4 },
  { name: "后端菜鸟", id: 6, parentId: 5 },
  { name: "后端小白", id: 7, parentId: 6 },
  { name: "李四", id: 8 },
  { name: "支付宝", id: 9, parentId: 8 },
  { name: "淘宝", id: 10, parentId: 8 },
  { name: "天猫", id: 11, parentId: 8 },
  { name: "钉钉", id: 12, parentId: 8 },
  { name: "花呗", id: 13, parentId: 9 },
  { name: "余额宝", id: 14, parentId: 9 },
  { name: "蚂蚁森林", id: 15, parentId: 9 },
  { name: "企业版", id: 16, parentId: 12 },
  { name: "直播", id: 17, parentId: 10 },
  { name: "店铺", id: 18, parentId: 10 },
  { name: "优惠券", id: 19, parentId: 10 },
  { name: "双十一", id: 20, parentId: 11 },
  { name: "双十二", id: 21, parentId: 11 },
  { name: "年货节", id: 22, parentId: 11 },
];

// 将扁平数组转换为树形结构
function arrayToTree(items: any[], parentId: number | null = null): DataNode[] {
  return items
    .filter(item => item.parentId === parentId) // 筛选出当前层级的节点
    .map(item => ({
      title: item.name, // 节点显示的名称
      key: item.id, // 节点的唯一标识
      children: arrayToTree(items, item.id), // 递归生成子节点
    }));
}

// 转换后的树形数据
const treeData = arrayToTree(data);

const SearchTree: React.FC = () => {
  // 状态:存储展开的节点 key
  const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
  // 状态:存储搜索框的值
  const [searchValue, setSearchValue] = useState('');
  // 状态:是否自动展开父节点
  const [autoExpandParent, setAutoExpandParent] = useState(true);

  // 处理节点展开事件
  const onExpand = (expandedKeys: React.Key[]) => {
    setExpandedKeys(expandedKeys); // 更新展开的节点 key
    setAutoExpandParent(false); // 手动展开时不自动展开父节点
  };

  // 处理搜索框内容变化事件
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target; // 获取输入框的值
    const expandedKeys = treeData
      .map(item => {
        // 如果节点的标题包含搜索值,则获取其父节点的 key
        if (item.title.indexOf(value) > -1) {
          return getParentKey(item.key, treeData);
        }
        return null;
      })
      // 去重并过滤掉 null 值
      .filter((item, i, self) => item && self.indexOf(item) === i);
    setExpandedKeys(expandedKeys as React.Key[]); // 更新展开的节点 key
    setSearchValue(value); // 更新搜索值
    setAutoExpandParent(true); // 搜索时自动展开父节点
  };

  // 获取父节点的 key
  const getParentKey = (key: React.Key, tree: DataNode[]): React.Key | null => {
    let parentKey: React.Key | null = null;
    for (const node of tree) {
      if (node.children) {
        // 如果当前节点的子节点中包含目标 key,则当前节点是目标的父节点
        if (node.children.some(child => child.key === key)) {
          parentKey = node.key;
        } else {
          // 递归查找子节点
          const foundKey = getParentKey(key, node.children);
          if (foundKey) {
            parentKey = foundKey;
          }
        }
      }
    }
    return parentKey;
  };

  // 遍历树形数据,生成带有高亮搜索值的节点
  const loop = (data: DataNode[]): DataNode[] =>
    data.map(item => {
      const index = item.title.indexOf(searchValue); // 搜索值在标题中的位置
      const beforeStr = item.title.substring(0, index); // 搜索值之前的部分
      const afterStr = item.title.substring(index + searchValue.length); // 搜索值之后的部分
      const title =
        index > -1 ? (
          // 如果标题中包含搜索值,则高亮显示搜索值
          <span>
            {beforeStr}
            <span className="site-tree-search-value">{searchValue}</span>
            {afterStr}
          </span>
        ) : (
          // 否则正常显示标题
          <span>{item.title}</span>
        );
      if (item.children) {
        // 如果有子节点,递归处理子节点
        return { ...item, title, children: loop(item.children) };
      }
      return { ...item, title }; // 返回处理后的节点
    });

  return (
    <div>
      {/* 搜索框 */}
      <Search style={{ marginBottom: 8 }} placeholder="Search" onChange={onChange} />
      {/* 树形控件 */}
      <Tree
        onExpand={onExpand} // 节点展开事件
        expandedKeys={expandedKeys} // 当前展开的节点 key
        autoExpandParent={autoExpandParent} // 是否自动展开父节点
        treeData={loop(treeData)} // 树形数据带有高亮搜索值
      />
    </div>
  );
};

export default SearchTree;

代码解释

  1. 导入依赖

    • React 和 useState:用于创建 React 组件和管理状态。
    • Tree 和 Input:从 antd 库中导入的组件,用于展示树形结构和搜索框。
    • DataNodeantd 库中定义的树节点类型。
  2. 定义扁平数据

    • data 数组包含了多个对象,每个对象代表一个节点,包含 nameid 和 parentId 属性。
  3. 将扁平数据转换为树形数据

    • arrayToTree 函数递归地将扁平数据转换为树形数据结构。
    • treeData 变量存储转换后的树形数据。
  4. 定义 SearchTree 组件

    • 使用 useState 定义三个状态变量:expandedKeyssearchValue 和 autoExpandParent
    • onExpand 函数处理节点展开事件,更新 expandedKeys 和 autoExpandParent 状态。
    • onChange 函数处理搜索框内容变化事件,更新 expandedKeys 和 searchValue 状态。
    • getParentKey 函数递归地获取父节点的 key
    • loop 函数遍历树形数据,生成带有高亮搜索值的节点。
    • 返回包含 Search 和 Tree 组件的 JSX 结构。

性能优化

为了提高性能,可以在数据量较大时使用虚拟滚动技术,减少 DOM 节点的渲染数量。此外,可以使用 useMemo 和 useCallback 来缓存计算结果和函数,避免不必要的重新渲染。

常见问题及解决方案

  1. 数据量大时渲染缓慢

    • 解决方案:使用虚拟滚动技术,如 react-virtualized 或 react-window
  2. 搜索时高亮显示不准确

    • 解决方案:确保在搜索时正确处理大小写和特殊字符。

结语

通过本文的介绍,我们了解了如何将扁平数据转换为树形数据,反之亦然,并结合 antd 和 React 实现了一个丝滑的搜索树组件。希望这些内容对你有所帮助,在实际项目中能够灵活运用这些技巧。