如何为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
相关产品推荐
相关产品推荐

