You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

实现树形组件搜索时展开匹配节点且保留完整搜索功能

树形组件搜索筛选与展开状态控制实现方案

需求梳理

  • 搜索输入时,所有匹配的节点需保持展开状态
  • 清空搜索内容后,树形组件恢复初始折叠/展开状态
  • 全树搜索逻辑:
    • 若父节点匹配搜索关键词,仅展开至该父节点层级,其子节点可维持折叠状态
    • 若子节点匹配,需自动展开所有祖先节点路径,确保匹配的子节点可见

完整实现代码

主组件 TreeList.js

import React, { useEffect, useState } from "react";
import Tree from "./Tree";
import { data } from "./data";
import "./styles.css";

const TreeList = () => {
  const [search, setSearch] = useState("");
  // 存储初始展开状态,用于清空搜索时恢复
  const [initialExpandedKeys, setInitialExpandedKeys] = useState([]);

  useEffect(() => {
    // 初始化时记录初始展开状态(可根据业务自定义初始展开节点)
    setInitialExpandedKeys([]);
  }, []);

  return (
    <>
      <input
        type="text"
        value={search}
        placeholder="搜索节点"
        onChange={(e) => setSearch(e.target.value.trim())}
      />
      <Tree 
        data={data} 
        searchKeyword={search} 
        initialExpandedKeys={initialExpandedKeys}
      />
    </>
  );
};

export default TreeList;

树形节点组件 Tree.js

import React, { useState, useMemo } from "react";

const Tree = ({ data, searchKeyword, initialExpandedKeys }) => {
  const [expandedKeys, setExpandedKeys] = useState(initialExpandedKeys);

  // 清空搜索时恢复初始状态
  useState(() => {
    if (!searchKeyword) {
      setExpandedKeys(initialExpandedKeys);
    }
  }, [searchKeyword, initialExpandedKeys]);

  // 递归判断节点是否匹配搜索关键词,或后代节点是否匹配
  const checkMatch = (node, keyword) => {
    if (!keyword) return false;
    // 当前节点匹配
    if (node.name.toLowerCase().includes(keyword.toLowerCase())) {
      return true;
    }
    // 递归检查子节点
    if (node.children && node.children.length > 0) {
      return node.children.some(child => checkMatch(child, keyword));
    }
    return false;
  };

  // 递归获取需要展开的节点key集合
  const getExpandedKeys = (nodes, keyword, parentKeys = []) => {
    let keys = [];
    nodes.forEach(node => {
      const currentPath = [...parentKeys, node.id];
      // 当前节点匹配,仅加入当前节点key
      if (node.name.toLowerCase().includes(keyword.toLowerCase())) {
        keys.push(node.id);
      } else if (node.children && node.children.length > 0) {
        // 子节点匹配,加入所有祖先节点key
        const childMatches = getExpandedKeys(node.children, keyword, currentPath);
        if (childMatches.length > 0) {
          keys = [...keys, ...currentPath];
        }
      }
    });
    // 去重并返回
    return [...new Set(keys)];
  };

  // 根据搜索关键词计算需要展开的节点
  const computedExpandedKeys = useMemo(() => {
    if (!searchKeyword) return initialExpandedKeys;
    return getExpandedKeys(data, searchKeyword);
  }, [searchKeyword, data, initialExpandedKeys]);

  // 切换节点展开状态(非搜索模式下的手动操作)
  const toggleExpand = (nodeId) => {
    if (searchKeyword) return; // 搜索模式下禁止手动修改展开状态
    setExpandedKeys(prev => 
      prev.includes(nodeId) ? prev.filter(id => id !== nodeId) : [...prev, nodeId]
    );
  };

  // 递归渲染树形节点
  const renderTreeNodes = (nodes) => {
    return nodes.map(node => {
      const isMatch = checkMatch(node, searchKeyword);
      const isExpanded = computedExpandedKeys.includes(node.id);
      const hasChildren = node.children && node.children.length > 0;

      return (
        <div key={node.id} className="tree-node">
          <div 
            className={`node-header ${isMatch ? 'matched' : ''}`}
            onClick={() => toggleExpand(node.id)}
          >
            {hasChildren && (
              <span className="expand-icon">{isExpanded ? '▼' : '▶'}</span>
            )}
            {node.name}
          </div>
          {isExpanded && hasChildren && (
            <div className="node-children">
              {renderTreeNodes(node.children)}
            </div>
          )}
        </div>
      );
    });
  };

  return <div className="tree-container">{renderTreeNodes(data)}</div>;
};

export default Tree;

示例数据 data.js

export const data = [
  {
    id: 1,
    name: "父节点1",
    children: [
      { id: 11, name: "子节点1-1" },
      { id: 12, name: "子节点1-2", children: [{ id: 121, name: "孙节点1-2-1" }] }
    ]
  },
  {
    id: 2,
    name: "父节点2",
    children: [
      { id: 21, name: "子节点2-1" },
      { id: 22, name: "子节点2-2" }
    ]
  }
];

样式文件 styles.css

.tree-container {
  margin-top: 16px;
  padding-left: 8px;
}

.tree-node {
  margin: 4px 0;
}

.node-header {
  display: flex;
  align-items: center;
  cursor: pointer;
  padding: 4px;
}

.expand-icon {
  margin-right: 4px;
  font-size: 12px;
}

.node-children {
  padding-left: 20px;
}

.matched {
  background-color: #f0f8ff;
  font-weight: bold;
}

input {
  padding: 8px;
  width: 300px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

实现说明

  1. 搜索匹配与展开控制:
    • 通过checkMatch递归判断节点自身或后代是否匹配关键词
    • getExpandedKeys根据匹配规则生成需要展开的节点ID集合:父节点匹配仅加入自身ID,子节点匹配则加入所有祖先节点ID
  2. 状态恢复:
    • 组件初始化时记录初始展开状态,清空搜索时自动重置expandedKeys为初始值
  3. 交互限制:
    • 搜索模式下禁止手动修改展开状态,避免与搜索自动展开逻辑冲突
  4. 性能优化:
    • 使用useMemo缓存计算后的展开节点集合,避免每次渲染重复计算

内容的提问来源于stack exchange,提问作者user16860065

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 14:25:40