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

如何为Material UI TreeView组件实现文件目录过滤功能

MUI TreeView 搜索过滤功能实现

核心逻辑是递归遍历树结构做匹配,保留匹配节点的所有父级路径,搜索时自动展开相关父节点,不会修改原始接口返回的数据,具体实现步骤如下:

1. 新增核心工具方法

两个递归方法,一个负责过滤节点,一个负责收集搜索时需要自动展开的节点ID:

// 递归过滤匹配的树节点:当前节点名匹配/任意子节点匹配则保留
const filterTreeNodes = (nodes, keyword) => {
  if (!keyword.trim()) return nodes;
  const lowerKeyword = keyword.toLowerCase();
  const isCurrentMatch = nodes.name.toLowerCase().includes(lowerKeyword);

  // 叶子节点(文件)直接返回匹配结果
  if (nodes.isFile) return isCurrentMatch ? nodes : null;

  // 文件夹节点先递归过滤子节点
  const matchedChildren = (nodes.children || [])
    .map(child => filterTreeNodes(child, keyword))
    .filter(Boolean);

  // 当前节点匹配/存在匹配的子节点,就保留该节点
  return isCurrentMatch || matchedChildren.length > 0
    ? { ...nodes, children: matchedChildren }
    : null;
};

// 递归收集所有需要自动展开的文件夹节点ID
const getAutoExpandedIds = (nodes, keyword) => {
  if (!keyword.trim()) return ["root"]; // 无搜索时保持默认展开根节点
  const lowerKeyword = keyword.toLowerCase();
  let expandedIds = [];

  if (!nodes.isFile) {
    // 检查是否存在匹配的子节点
    const hasMatchedChild = (nodes.children || []).some(child => {
      return child.isFile
        ? child.name.toLowerCase().includes(lowerKeyword)
        : getAutoExpandedIds(child, keyword).length > 0;
    });

    // 当前文件夹匹配/有匹配子节点,就加入展开列表,同时递归收集子文件夹的展开ID
    if (nodes.name.toLowerCase().includes(lowerKeyword) || hasMatchedChild) {
      expandedIds.push(nodes.path);
      (nodes.children || []).forEach(child => {
        expandedIds = expandedIds.concat(getAutoExpandedIds(child, keyword));
      });
    }
  }
  return expandedIds;
};

2. 修改组件逻辑

  • 新增搜索关键词状态
  • 用useMemo缓存过滤后的树数据、自动展开的节点ID,避免重复计算
  • 新增搜索输入框
  • 将TreeView的展开状态改为受控模式,传入过滤后的数据渲染
    修改后的完整组件代码:
import { useState, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { TreeView, TreeItem } from '@mui/lab';
import {
  ExpandMoreIcon,
  ChevronRightIcon,
  FolderOpenIcon,
  FolderIcon,
  ArticleIcon,
  CircularProgress,
  TextField,
  Typography
} from '@mui/material';

export default function RichObjectTreeView(props) {
  const dispatch = useDispatch();
  const [searchKeyword, setSearchKeyword] = useState("");

  const handleOnItemClick = (event, nodeIds) => {
    // 若点击的节点为文件,则将其展示到仪表板中
  };

  // 缓存过滤后的树数据
  const filteredTreeData = useMemo(() => {
    if (!props.treeViewApiData) return null;
    return filterTreeNodes(props.treeViewApiData, searchKeyword);
  }, [props.treeViewApiData, searchKeyword]);

  // 缓存需要展开的节点列表
  const expandedNodeIds = useMemo(() => {
    if (!props.treeViewApiData) return [];
    return getAutoExpandedIds(props.treeViewApiData, searchKeyword);
  }, [props.treeViewApiData, searchKeyword]);

  const renderTree = (nodes) => {
    if (!nodes || nodes.length === 0) return null;
    return (
      <TreeItem key={nodes.path} nodeId={nodes.path} label={nodes.name}>
        {Array.isArray(nodes.children)
          ? nodes.children.map((node) => renderTree(node))
          : null}
      </TreeItem>
    );
  };

  if (!props.treeViewApiData) {
    return <CircularProgress sx={{ marginLeft: "100px", marginTop: "100px" }} />;
  }

  return (
    <div>
      {/* 搜索输入框 */}
      <TextField
        size="small"
        fullWidth
        placeholder="搜索文件或文件夹"
        value={searchKeyword}
        onChange={(e) => setSearchKeyword(e.target.value)}
        sx={{ mb: 1, maxWidth: 400 }}
      />
      {
        filteredTreeData ? (
          <TreeView
            aria-label="rich object"
            defaultCollapseIcon={
              <>
                <ExpandMoreIcon />
                <FolderOpenIcon sx={{ marginRight: "12px" }} />
              </>
            }
            expanded={expandedNodeIds}
            defaultExpandIcon={
              <>
                <ChevronRightIcon />
                <FolderIcon sx={{ marginRight: "12px" }} />
              </>
            }
            defaultEndIcon={<ArticleIcon />}
            sx={{ height: 110, flexGrow: 1, maxWidth: 400, overflowY: "auto" }}
            onNodeFocus={handleOnItemClick}
          >
            {renderTree(filteredTreeData)}
          </TreeView>
        ) : (
          <Typography variant="body2" color="text.secondary" sx={{ py: 2, textAlign: 'center' }}>
            没有找到匹配的内容
          </Typography>
        )
      }
    </div>
  );
}

// 工具方法放组件外,避免每次渲染重新创建
const filterTreeNodes = (nodes, keyword) => {
  if (!keyword.trim()) return nodes;
  const lowerKeyword = keyword.toLowerCase();
  const isCurrentMatch = nodes.name.toLowerCase().includes(lowerKeyword);
  if (nodes.isFile) return isCurrentMatch ? nodes : null;
  const matchedChildren = (nodes.children || [])
    .map(child => filterTreeNodes(child, keyword))
    .filter(Boolean);
  return isCurrentMatch || matchedChildren.length > 0
    ? { ...nodes, children: matchedChildren }
    : null;
};

const getAutoExpandedIds = (nodes, keyword) => {
  if (!keyword.trim()) return ["root"];
  const lowerKeyword = keyword.toLowerCase();
  let expandedIds = [];
  if (!nodes.isFile) {
    const hasMatchedChild = (nodes.children || []).some(child => {
      return child.isFile
        ? child.name.toLowerCase().includes(lowerKeyword)
        : getAutoExpandedIds(child, keyword).length > 0;
    });
    if (nodes.name.toLowerCase().includes(lowerKeyword) || hasMatchedChild) {
      expandedIds.push(nodes.path);
      (nodes.children || []).forEach(child => {
        expandedIds = expandedIds.concat(getAutoExpandedIds(child, keyword));
      });
    }
  }
  return expandedIds;
};

效果说明

  • 搜索不区分大小写,支持同时匹配文件和文件夹名称
  • 只要子节点匹配,上层所有父文件夹都会自动保留并展开,不会出现匹配结果找不到路径的问题
  • 清空搜索框后自动恢复原始树结构和默认展开状态
  • 无匹配结果时会显示对应提示

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 10:36:18