基于Material-UI Next实现左侧多级嵌套菜单的技术咨询
Got it, let's walk through building that multi-level nested sidebar menu that matches Material-UI Next's style closely. Since you already have a way to convert your flat JSON data into nested objects, here's a practical, step-by-step guide to implement this:
First, we'll use Material-UI's core components that align with the official site's sidebar structure: Drawer for the sidebar container, List/ListItemButton for menu items, Collapse for expandable submenus, and icons like ExpandLess/ExpandMore for toggle indicators.
Since your data is now nested, a recursive component is the cleanest way to render multi-level menus. Here's a reusable implementation:
import { List, ListItemButton, ListItemText, Collapse, Typography } from '@mui/material'; import ExpandLess from '@mui/icons-material/ExpandLess'; import ExpandMore from '@mui/icons-material/ExpandMore'; import { useState, memo } from 'react'; import { useLocation } from 'react-router-dom'; // If using React Router for routing // Memoize to avoid unnecessary re-renders for large menus const NestedMenuItem = memo(({ item, depth = 0 }) => { const [isSubmenuOpen, setIsSubmenuOpen] = useState(false); const location = useLocation(); const isActive = location.pathname === item.path; // Mark active menu item const toggleSubmenu = () => { setIsSubmenuOpen(!isSubmenuOpen); }; // Handle indentation for nested items (matches official site's spacing) const indentStyles = { paddingLeft: `${depth * 32}px` }; return ( <> <ListItemButton onClick={item.children ? toggleSubmenu : undefined} style={indentStyles} selected={isActive} sx={{ '&.Mui-selected': { backgroundColor: '#e0e0e0', // Match official site's active item background '&:hover': { backgroundColor: '#bdbdbd' } } }} > <ListItemText primary={ <Typography variant="body1" fontWeight={item.children ? 'normal' : 'medium'} // Bold parent items > {item.label} </Typography> } /> {/* Show toggle icon only if item has children */} {item.children && (isSubmenuOpen ? <ExpandLess /> : <ExpandMore />)} </ListItemButton> {/* Render submenu with collapse animation */} {item.children && ( <Collapse in={isSubmenuOpen} timeout="auto" unmountOnExit> <List component="div" disablePadding> {item.children.map(child => ( <NestedMenuItem key={child.id} item={child} depth={depth + 1} /> ))} </List> </Collapse> )} </> ); });
Now wrap the recursive component in a Drawer to match the official site's layout:
import { Drawer, List, Box } from '@mui/material'; const SidebarMenu = ({ nestedMenuData }) => { return ( <Drawer variant="permanent" sx={{ width: 240, flexShrink: 0, '& .MuiDrawer-paper': { width: 240, boxSizing: 'border-box', backgroundColor: '#f5f5f5', // Match official site's sidebar background paddingTop: '64px' // Offset for top AppBar (adjust based on your layout) } }} > <Box sx={{ overflow: 'auto' }}> <List> {nestedMenuData.map(item => ( <NestedMenuItem key={item.id} item={item} /> ))} </List> </Box> </Drawer> ); };
- Icon Support: If your JSON includes icon references, add a
ListItemIconcomponent insideListItemButton:// Example: Assume item.icon is an imported icon component like Home import { Home, Settings } from '@mui/icons-material'; // Inside ListItemButton <ListItemIcon> {item.icon === 'Home' ? <Home /> : <Settings />} </ListItemIcon> - Persistent Expansion: If you want submenus to stay open on page refresh, save
isSubmenuOpenstate tolocalStorageinstead of usinguseState.
If you ever need to adjust your data conversion function, here's a reliable flat-to-nested utility:
const convertFlatToNested = (flatData) => { const itemMap = {}; const nestedData = []; // First map all items by their ID flatData.forEach(item => { itemMap[item.id] = { ...item, children: [] }; }); // Build nested structure by linking children to parents flatData.forEach(item => { if (item.parentId) { itemMap[item.parentId].children.push(itemMap[item.id]); } else { nestedData.push(itemMap[item.id]); } }); return nestedData; };
内容的提问来源于stack exchange,提问作者Gregor Sotošek

