| | |
| | | import React, { useState, useEffect, useMemo, Suspense } from "react"; |
| | | import { |
| | | useTranslate, |
| | | DashboardMenuItem, |
| | | MenuItemLink, |
| | | Menu, |
| | | useSidebarState, |
| | | usePermissions, |
| | | } from 'react-admin'; |
| | | import { useLocation } from 'react-router-dom'; |
| | | import { Box } from '@mui/material'; |
| | | import SubMenu from './SubMenu'; |
| | | import SettingsIcon from '@mui/icons-material/Settings'; |
| | | import DashboardIcon from '@mui/icons-material/Dashboard'; |
| | | import HorizontalRuleIcon from '@mui/icons-material/HorizontalRule'; |
| | | import PersonIcon from '@mui/icons-material/Person'; |
| | | import * as Icons from '@mui/icons-material'; |
| | | useTranslate, |
| | | DashboardMenuItem, |
| | | MenuItemLink, |
| | | Menu, |
| | | useSidebarState, |
| | | usePermissions, |
| | | } from "react-admin"; |
| | | import { useLocation } from "react-router-dom"; |
| | | import { Box } from "@mui/material"; |
| | | import SubMenu from "./SubMenu"; |
| | | import SettingsIcon from "@mui/icons-material/Settings"; |
| | | import DashboardIcon from "@mui/icons-material/Dashboard"; |
| | | import HorizontalRuleIcon from "@mui/icons-material/HorizontalRule"; |
| | | import PersonIcon from "@mui/icons-material/Person"; |
| | | import * as Icons from "@mui/icons-material"; |
| | | |
| | | const getIconComponent = (iconStr) => { |
| | | return Icons[iconStr] || HorizontalRuleIcon; |
| | | return Icons[iconStr] || HorizontalRuleIcon; |
| | | }; |
| | | |
| | | export const MyMenu = ({ dense = false }) => { |
| | | const [state, setState] = useState({}); |
| | | const translate = useTranslate(); |
| | | const location = useLocation(); |
| | | const [sidebarIsOpen] = useSidebarState(); |
| | | const { isPending, permissions } = usePermissions(); |
| | | const [state, setState] = useState({}); |
| | | const translate = useTranslate(); |
| | | const location = useLocation(); |
| | | const [sidebarIsOpen] = useSidebarState(); |
| | | const { isPending, permissions } = usePermissions(); |
| | | |
| | | useEffect(() => { |
| | | // default open sub menu |
| | | const defaultExpandMenu = ["menu.system", "menu.dispatcher", "menu.equipment"]; |
| | | permissions?.forEach(item => { |
| | | if (defaultExpandMenu.includes(item.name)) { |
| | | setState(state => ({ ...state, [item.route]: true })); |
| | | } |
| | | }); |
| | | }, [permissions]); |
| | | useEffect(() => { |
| | | // default open sub menu |
| | | const defaultExpandMenu = [ |
| | | "menu.system", |
| | | "menu.dispatcher", |
| | | "menu.equipment", |
| | | ]; |
| | | permissions?.forEach((item) => { |
| | | if (defaultExpandMenu.includes(item.name)) { |
| | | setState((state) => ({ ...state, [item.route]: true })); |
| | | } |
| | | }); |
| | | }, [permissions]); |
| | | |
| | | useEffect(() => { |
| | | // expand this parent menu |
| | | const currentPath = location.pathname; |
| | | const parentRoutes = findParentRoutes(currentPath, permissions) |
| | | for (const parentRoute of parentRoutes) { |
| | | setState(state => ({ ...state, [parentRoute]: true })); |
| | | } |
| | | useEffect(() => { |
| | | // expand this parent menu |
| | | const currentPath = location.pathname; |
| | | const parentRoutes = findParentRoutes(currentPath, permissions); |
| | | for (const parentRoute of parentRoutes) { |
| | | setState((state) => ({ ...state, [parentRoute]: true })); |
| | | } |
| | | }, [location.pathname]); |
| | | |
| | | }, [location.pathname]); |
| | | const handleToggle = (menu) => { |
| | | setState((state) => ({ ...state, [menu]: !state[menu] })); |
| | | }; |
| | | |
| | | const handleToggle = (menu) => { |
| | | setState(state => ({ ...state, [menu]: !state[menu] })); |
| | | }; |
| | | const getIcon = (iconStr) => { |
| | | const IconComponent = getIconComponent(iconStr); |
| | | if (IconComponent) { |
| | | return <IconComponent />; |
| | | } else { |
| | | return <KeyboardArrowDownIcon />; |
| | | } |
| | | }; |
| | | |
| | | const getIcon = (iconStr) => { |
| | | const IconComponent = getIconComponent(iconStr); |
| | | if (IconComponent) { |
| | | return <IconComponent />; |
| | | } |
| | | }; |
| | | // 检查菜单是否被选中 |
| | | const isSelected = (component) => { |
| | | if (!component) return false; |
| | | const currentPath = location.pathname.replace("/", ""); |
| | | return currentPath === component; |
| | | }; |
| | | |
| | | const generateMenu = (permissions) => { |
| | | return permissions.map((node) => { |
| | | if (node.children) { |
| | | // 检查父级菜单是否有子菜单被选中 |
| | | const hasSelectedChild = (node) => { |
| | | if (!node.children) return false; |
| | | return node.children.some(child => { |
| | | if (child.children) { |
| | | return hasSelectedChild(child); |
| | | } |
| | | return isSelected(child.component); |
| | | }); |
| | | }; |
| | | |
| | | // 在 MyMenu 组件的 generateMenu 函数中,确保 MenuItemLink 也左对齐 |
| | | const generateMenu = (permissions) => { |
| | | return permissions.map((node) => { |
| | | if (node.children) { |
| | | const selected = isSelected(node.component) || hasSelectedChild(node); |
| | | return ( |
| | | <SubMenu |
| | | key={node.id} |
| | | handleToggle={() => handleToggle(node.route)} |
| | | isOpen={state[node.route]} |
| | | name={node.name} |
| | | dense={dense} |
| | | icon={getIcon(node.icon)} |
| | | isSelected={selected} |
| | | > |
| | | {generateMenu(node.children)} |
| | | </SubMenu> |
| | | ); |
| | | } else { |
| | | if (node.component) { |
| | | const selected = isSelected(node.component); |
| | | // 在 generateMenu 函数中的 MenuItemLink 部分 |
| | | return ( |
| | | <SubMenu |
| | | <MenuItemLink |
| | | key={node.id} |
| | | handleToggle={() => handleToggle(node.route)} |
| | | isOpen={state[node.route]} |
| | | name={node.name} |
| | | to={node.component} |
| | | state={{ _scrollToTop: true }} |
| | | primaryText={translate(node.name)} |
| | | leftIcon={getIcon(node.icon)} |
| | | dense={dense} |
| | | icon={getIcon(node.icon)} |
| | | > |
| | | {generateMenu(node.children)} |
| | | </SubMenu> |
| | | sx={{ |
| | | backgroundColor: selected ? 'rgba(25, 118, 210, 0.08) !important' : 'transparent', |
| | | color: selected ? '#1976d2 !important' : 'text.secondary', |
| | | '&:hover': { |
| | | backgroundColor: selected ? 'rgba(25, 118, 210, 0.12) !important' : 'rgba(0, 0, 0, 0.04)', |
| | | }, |
| | | borderLeft: 'none', |
| | | borderRadius: '4px', |
| | | margin: '2px 8px', |
| | | width: 'calc(100% - 16px)', |
| | | transition: 'all 0.2s ease-in-out', |
| | | |
| | | // 缩小整体间距 |
| | | padding: '6px 8px', // 减少内边距 |
| | | minHeight: '36px', // 稍微减小高度 |
| | | |
| | | '& .RaMenuItemLink-icon': { |
| | | color: selected ? '#1976d2 !important' : 'text.secondary', |
| | | minWidth: '32px !important', // 缩小图标区域宽度 |
| | | marginRight: '4px', // 缩小图标和文字间距 |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | justifyContent: 'center', // 图标居中显示 |
| | | }, |
| | | |
| | | fontWeight: selected ? 600 : 400, |
| | | |
| | | // 确保文字内容左对齐 |
| | | '& .MuiListItemText-root': { |
| | | margin: 0, |
| | | '& .MuiTypography-root': { |
| | | textAlign: 'left', |
| | | justifyContent: 'flex-start', |
| | | fontSize: '0.875rem', // 稍微减小字体大小 |
| | | lineHeight: '1.3', |
| | | } |
| | | }, |
| | | }} |
| | | /> |
| | | ); |
| | | } else { |
| | | if (node.component) { |
| | | return ( |
| | | <MenuItemLink |
| | | key={node.id} |
| | | to={node.component} // correspond to Resource.name |
| | | state={{ _scrollToTop: true }} |
| | | // primaryText={translate(`resources.orders.name`, { |
| | | // smart_count: 2, |
| | | // })} |
| | | primaryText={node.name} |
| | | leftIcon={getIcon(node.icon)} |
| | | dense={dense} |
| | | /> |
| | | ); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | return isPending |
| | | ? (<div>Waiting for permissions...</div>) : |
| | | ( |
| | | <Box |
| | | sx={{ |
| | | width: sidebarIsOpen ? 200 : 50, |
| | | marginTop: 1, |
| | | marginBottom: 1, |
| | | transition: theme => |
| | | theme.transitions.create('width', { |
| | | easing: theme.transitions.easing.sharp, |
| | | duration: theme.transitions.duration.leavingScreen, |
| | | }), |
| | | }} |
| | | > |
| | | <Menu.Item |
| | | to="/dashboard" |
| | | primaryText="menu.dashboard" |
| | | leftIcon={<DashboardIcon />} |
| | | /> |
| | | {permissions && (generateMenu(permissions))} |
| | | {/* <Menu.ResourceItems /> */} |
| | | <Menu.Item |
| | | to="/settings" |
| | | primaryText="menu.settings" |
| | | leftIcon={<PersonIcon />} |
| | | /> |
| | | </Box> |
| | | ) |
| | | } |
| | | // 检查固定菜单是否选中 |
| | | const isDashboardSelected = location.pathname === '/dashboard'; |
| | | const isSettingsSelected = location.pathname === '/settings'; |
| | | |
| | | return isPending ? ( |
| | | <div>Waiting for permissions...</div> |
| | | ) : ( |
| | | <Box |
| | | sx={{ |
| | | width: sidebarIsOpen ? 200 : 50, |
| | | marginTop: 1, |
| | | marginBottom: 1, |
| | | transition: (theme) => |
| | | theme.transitions.create("width", { |
| | | easing: theme.transitions.easing.sharp, |
| | | duration: theme.transitions.duration.leavingScreen, |
| | | }), |
| | | // 菜单容器样式 |
| | | '& .MuiMenuItem-root': { |
| | | boxSizing: 'border-box', |
| | | } |
| | | }} |
| | | > |
| | | <Menu.Item |
| | | to="/dashboard" |
| | | primaryText="menu.dashboard" |
| | | leftIcon={<DashboardIcon />} |
| | | sx={{ |
| | | backgroundColor: isDashboardSelected ? 'rgba(25, 118, 210, 0.08) !important' : 'transparent', |
| | | color: isDashboardSelected ? '#1976d2 !important' : 'text.secondary', |
| | | '&:hover': { |
| | | backgroundColor: isDashboardSelected ? 'rgba(25, 118, 210, 0.12) !important' : 'rgba(0, 0, 0, 0.04)', |
| | | }, |
| | | borderLeft: isDashboardSelected ? '3px solid #1976d2' : '3px solid transparent', |
| | | borderRadius: '0 4px 4px 0', |
| | | margin: '1px 0', |
| | | width: '100%', |
| | | transition: 'all 0.2s ease-in-out', |
| | | '& .MuiListItemIcon-root': { |
| | | color: isDashboardSelected ? '#1976d2 !important' : 'text.secondary', |
| | | minWidth: 40, |
| | | } |
| | | }} |
| | | /> |
| | | {permissions && generateMenu(permissions)} |
| | | <Menu.Item |
| | | to="/settings" |
| | | primaryText="menu.settings" |
| | | leftIcon={<PersonIcon />} |
| | | sx={{ |
| | | backgroundColor: isSettingsSelected ? 'rgba(25, 118, 210, 0.08) !important' : 'transparent', |
| | | color: isSettingsSelected ? '#1976d2 !important' : 'text.secondary', |
| | | '&:hover': { |
| | | backgroundColor: isSettingsSelected ? 'rgba(25, 118, 210, 0.12) !important' : 'rgba(0, 0, 0, 0.04)', |
| | | }, |
| | | borderLeft: isSettingsSelected ? '3px solid #1976d2' : '3px solid transparent', |
| | | borderRadius: '0 4px 4px 0', |
| | | margin: '1px 0', |
| | | width: '100%', |
| | | transition: 'all 0.2s ease-in-out', |
| | | '& .MuiListItemIcon-root': { |
| | | color: isSettingsSelected ? '#1976d2 !important' : 'text.secondary', |
| | | minWidth: 40, |
| | | } |
| | | }} |
| | | /> |
| | | </Box> |
| | | ); |
| | | }; |
| | | |
| | | const findParentRoutes = (pathname, permissions) => { |
| | | if (!pathname || !permissions) { |
| | | return []; |
| | | } |
| | | const findMenu = (currentPermissions, path) => { |
| | | for (const item of currentPermissions) { |
| | | if (item.component === path) { |
| | | return item; |
| | | } |
| | | if (item.children) { |
| | | const found = findMenu(item.children, path); |
| | | if (found) { |
| | | return found; |
| | | } |
| | | } |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | const findParentRoutesRecursive = (item, allPermissions) => { |
| | | const parentRoutes = []; |
| | | let current = item; |
| | | while (current && current.parentId) { |
| | | const parent = allPermissions.find(permission => permission.id === current.parentId); |
| | | if (parent) { |
| | | parentRoutes.push(parent.route); |
| | | current = parent; |
| | | } else { |
| | | break; |
| | | } |
| | | } |
| | | |
| | | return parentRoutes; |
| | | }; |
| | | |
| | | const currentMenu = findMenu(permissions, pathname.replace("/", "")); |
| | | if (currentMenu) { |
| | | return findParentRoutesRecursive(currentMenu, permissions); |
| | | } |
| | | |
| | | if (!pathname || !permissions) { |
| | | return []; |
| | | } |
| | | const findMenu = (currentPermissions, path) => { |
| | | for (const item of currentPermissions) { |
| | | if (item.component === path) { |
| | | return item; |
| | | } |
| | | if (item.children) { |
| | | const found = findMenu(item.children, path); |
| | | if (found) { |
| | | return found; |
| | | } |
| | | } |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | const findParentRoutesRecursive = (item, allPermissions) => { |
| | | const parentRoutes = []; |
| | | let current = item; |
| | | while (current && current.parentId) { |
| | | const parent = allPermissions.find( |
| | | (permission) => permission.id === current.parentId, |
| | | ); |
| | | if (parent) { |
| | | parentRoutes.push(parent.route); |
| | | current = parent; |
| | | } else { |
| | | break; |
| | | } |
| | | } |
| | | |
| | | return parentRoutes; |
| | | }; |
| | | |
| | | const currentMenu = findMenu(permissions, pathname.replace("/", "")); |
| | | if (currentMenu) { |
| | | return findParentRoutesRecursive(currentMenu, permissions); |
| | | } |
| | | |
| | | return []; |
| | | }; |