Files
MiragenFlow/admin/src/layouts/components/Menu.tsx
T

98 lines
5.1 KiB
TypeScript

import React, { memo, useEffect, useMemo, useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { Menu, type MenuValue } from '@miragenflow/ui/menu';
import { MiragenFlowButton } from '@miragenflow/ui';
import router, { IRouter } from 'router';
import { useAppDispatch, useAppSelector } from 'modules/store';
import { selectGlobal, toggleMenu } from 'modules/global';
import MenuLogo from './MenuLogo';
import Style from './Menu.module.less';
const { SubMenu, MenuItem, HeadMenu } = Menu;
interface IMenuProps { showLogo?: boolean; showOperation?: boolean; }
const routePath = (item: IRouter, parentPath = '') => item.absolute ? item.path : `${parentPath}${item.path}` || '/';
const isVisible = (item: IRouter, scopes: string[], roles: string[]) => {
if (!item.meta || item.meta.hidden) return false;
if (item.meta.requiredScope && !roles.includes('super_admin') && !scopes.includes(item.meta.requiredScope)) return false;
return true;
};
const visibleChildren = (item: IRouter, scopes: string[], roles: string[]) => (item.children || []).filter((child) => isVisible(child, scopes, roles));
const hasVisibleChild = (item: IRouter, scopes: string[], roles: string[]) => visibleChildren(item, scopes, roles).length > 0;
const renderMenuItems = (menu: IRouter[], scopes: string[], roles: string[], parentPath = ''): React.ReactNode[] => menu.map((item) => {
const { children, meta } = item;
if (!meta || !isVisible(item, scopes, roles)) return null;
const currentPath = routePath(item, parentPath);
if (children?.length) {
const visible = visibleChildren(item, scopes, roles);
if (!visible.length) return null;
if (visible.length === 1) {
const child = visible[0];
const childPath = routePath(child, currentPath);
const ChildIcon = child.meta?.Icon || meta.Icon;
return <MenuItem key={childPath} value={childPath} icon={ChildIcon ? <ChildIcon /> : undefined}>{child.meta?.title || meta.title}</MenuItem>;
}
return <SubMenu key={currentPath} value={currentPath} title={meta.title} icon={meta.Icon ? <meta.Icon /> : undefined}>
{renderMenuItems(visible, scopes, roles, currentPath)}
</SubMenu>;
}
return <MenuItem key={currentPath} value={currentPath} icon={meta.Icon ? <meta.Icon /> : undefined}>{meta.title}</MenuItem>;
});
const expandedFor = (menu: IRouter[], pathname: string, scopes: string[], roles: string[], parent = ''): string[] => menu.flatMap((item) => {
if (!item.children?.length || !hasVisibleChild(item, scopes, roles)) return [];
const visible = visibleChildren(item, scopes, roles);
if (visible.length === 1) return [];
const current = routePath(item, parent);
const childPaths = visible.map((child) => routePath(child, current));
return childPaths.includes(pathname) ? [current] : expandedFor(visible, pathname, scopes, roles, current);
});
export const HeaderMenu = memo(() => {
const globalState = useAppSelector(selectGlobal);
const location = useLocation();
const navigate = useNavigate();
const user = useAppSelector((state) => state.user.userInfo as { scopes?: string[]; roles?: string[] });
const [active, setActive] = useState<MenuValue>(location.pathname);
useEffect(() => setActive(location.pathname), [location.pathname]);
return <HeadMenu className={Style.headerMenu} expandType='popup' style={{ marginBottom: 20 }} value={active} theme={globalState.theme} onChange={(value) => { setActive(value); navigate(String(value)); }}>
{renderMenuItems(router, user.scopes || [], user.roles || [])}
</HeadMenu>;
});
export default memo((props: IMenuProps) => {
const location = useLocation();
const navigate = useNavigate();
const dispatch = useAppDispatch();
const globalState = useAppSelector(selectGlobal);
const user = useAppSelector((state) => state.user.userInfo as { scopes?: string[]; roles?: string[] });
const scopes = user.scopes || [];
const roles = user.roles || [];
const requiredExpanded = useMemo(() => expandedFor(router, location.pathname, scopes, roles), [location.pathname, scopes.join(','), roles.join(',')]);
const [expanded, setExpanded] = useState<MenuValue[]>(requiredExpanded);
useEffect(() => {
setExpanded((current) => Array.from(new Set([...current, ...requiredExpanded])));
}, [requiredExpanded.join('|')]);
const onNavigate = (value: MenuValue) => {
navigate(String(value));
if (window.innerWidth < 1000) dispatch(toggleMenu(true));
};
if (globalState.collapsed) return null;
return <>
<MiragenFlowButton htmlType='button' type='text' className={Style.mobileMenuBackdrop} aria-label='关闭菜单' onClick={() => dispatch(toggleMenu(true))} />
<Menu
width='232px'
style={{ flexShrink: 0, height: '100%' }}
className={Style.menuPanel2}
value={location.pathname}
expanded={expanded}
onChange={onNavigate}
onExpand={setExpanded}
theme={globalState.theme}
operations={props.showOperation ? <div className={Style.menuTip}>元境幻生管理台 {globalState.version}</div> : undefined}
logo={props.showLogo ? <MenuLogo collapsed={false} /> : undefined}
>{renderMenuItems(router, scopes, roles)}</Menu>
</>;
});