如何为对象数组构建带复选框的MUI v5 TreeView(无父节点)
基于MUI v5 TreeView实现带复选框的树形菜单(数组数据源)
核心思路
你的数组数据源可以临时包装为一个虚拟根节点(仅用于适配TreeView的结构要求,不渲染到页面),同时自定义带复选框的TreeItem组件,并实现父子节点的勾选联动逻辑。
具体实现步骤
1. 临时包装数组为虚拟根结构
在渲染TreeView时,把你的数组数据包装成带children的虚拟根对象,TreeView直接渲染这个根的子节点,不显示根本身:
// 临时包装数组为虚拟根 const virtualRoot = { id: 'virtual-root', children: data // 你的原始数组数据 };
2. 自定义带复选框的TreeItem组件
用MUI的Checkbox和TreeItem组合,自定义一个带复选框的树形节点:
import { TreeItem, Checkbox } from '@mui/material'; const CheckboxTreeItem = ({ node, checkedIds, onToggle }) => { const isChecked = checkedIds.includes(node.id); // 判断是否半选中(子节点部分被勾选) const isIndeterminate = node.children?.some(child => checkedIds.includes(child.id)) && !isChecked; const handleToggle = () => { onToggle(node.id); }; return ( <TreeItem nodeId={node.id} label={ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <Checkbox checked={isChecked} indeterminate={isIndeterminate} onChange={handleToggle} size="small" /> {node.name} </div> } > {/* 递归渲染子节点 */} {node.children?.map(child => ( <CheckboxTreeItem key={child.id} node={child} checkedIds={checkedIds} onToggle={onToggle} /> ))} </TreeItem> ); };
3. 实现勾选联动逻辑
维护选中id的集合,处理单个节点勾选时的父子联动:
import { useState, useCallback } from 'react'; import { TreeView } from '@mui/x-tree-view'; const CheckboxTreeView = () => { const [checkedIds, setCheckedIds] = useState([]); // 递归获取所有子节点id const getAllChildIds = (node) => { let ids = [node.id]; if (node.children) { node.children.forEach(child => { ids = [...ids, ...getAllChildIds(child)]; }); } return ids; }; // 递归获取所有父节点id const getAllParentIds = (node, allNodes) => { let ids = []; const parent = allNodes.find(n => n.children?.some(c => c.id === node.id)); if (parent) { ids.push(parent.id); ids = [...ids, ...getAllParentIds(parent, allNodes)]; } return ids; }; // 处理节点勾选/取消 const handleToggle = useCallback((nodeId) => { // 扁平化所有节点,方便查找 const flattenNodes = (nodes) => { let result = []; nodes.forEach(node => { result.push(node); if (node.children) { result = [...result, ...flattenNodes(node.children)]; } }); return result; }; const allNodes = flattenNodes(data); const currentNode = allNodes.find(n => n.id === nodeId); let newCheckedIds; if (checkedIds.includes(nodeId)) { // 取消勾选:移除当前节点及所有子节点 const childIds = getAllChildIds(currentNode); newCheckedIds = checkedIds.filter(id => !childIds.includes(id)); } else { // 勾选:添加当前节点及所有子节点 const childIds = getAllChildIds(currentNode); newCheckedIds = [...new Set([...checkedIds, ...childIds])]; } // 更新父节点的勾选状态 const parentIds = getAllParentIds(currentNode, allNodes); parentIds.forEach(parentId => { const parentNode = allNodes.find(n => n.id === parentId); const allChildChecked = parentNode.children.every(child => newCheckedIds.includes(child.id)); if (allChildChecked) { if (!newCheckedIds.includes(parentId)) { newCheckedIds.push(parentId); } } else { newCheckedIds = newCheckedIds.filter(id => id !== parentId); } }); setCheckedIds(newCheckedIds); }, [checkedIds, data]); return ( <TreeView defaultExpanded={['3']} // 默认展开的节点id sx={{ height: 240, flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} > {/* 渲染虚拟根的子节点 */} {virtualRoot.children.map(node => ( <CheckboxTreeItem key={node.id} node={node} checkedIds={checkedIds} onToggle={handleToggle} /> ))} </TreeView> ); }; export default CheckboxTreeView;
4. 使用组件
直接在页面中引入并使用这个自定义组件即可,你的原始数组数据可以直接传入使用。
关键说明
- 虚拟根节点仅用于适配TreeView的结构要求,不会被渲染到页面,用户看到的就是你原始数组的一级节点。
- 联动逻辑包含了父节点勾选/取消时自动同步所有子节点,以及子节点全部勾选/取消时自动同步父节点的半选/全选状态。
- 可以根据需求调整
Checkbox的样式、TreeView的尺寸等。
内容的提问来源于stack exchange,提问作者coderX
相关产品推荐
相关产品推荐

