98 lines
5.1 KiB
TypeScript
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>
|
|
</>;
|
|
});
|