feat: integrate platform backend and application interfaces
This commit is contained in:
@@ -1,115 +1,97 @@
|
||||
import React, { memo, useState } from 'react';
|
||||
import React, { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Menu, MenuValue } from 'tdesign-react';
|
||||
import { Menu, type MenuValue } from '@miragenflow/ui/menu';
|
||||
import { MiragenFlowButton } from '@miragenflow/ui';
|
||||
import router, { IRouter } from 'router';
|
||||
import { resolve } from 'utils/path';
|
||||
import { useAppSelector } from 'modules/store';
|
||||
import { selectGlobal } from 'modules/global';
|
||||
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; }
|
||||
|
||||
interface IMenuProps {
|
||||
showLogo?: boolean;
|
||||
showOperation?: boolean;
|
||||
}
|
||||
|
||||
const renderMenuItems = (menu: IRouter[], parentPath = '') => {
|
||||
const navigate = useNavigate();
|
||||
return menu.map((item) => {
|
||||
const { children, meta, path } = item;
|
||||
|
||||
if (!meta || meta?.hidden === true) {
|
||||
// 无meta信息 或 hidden == true,路由不显示为菜单
|
||||
return null;
|
||||
}
|
||||
|
||||
const { Icon, title, single } = meta;
|
||||
const routerPath = resolve(parentPath, path);
|
||||
|
||||
if (!children || children.length === 0) {
|
||||
return (
|
||||
<MenuItem
|
||||
key={routerPath}
|
||||
value={routerPath}
|
||||
icon={Icon ? <Icon /> : undefined}
|
||||
onClick={() => navigate(routerPath)}
|
||||
>
|
||||
{title}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
if (single && children?.length > 0) {
|
||||
const firstChild = children[0];
|
||||
if (firstChild?.meta && !firstChild?.meta?.hidden) {
|
||||
const { Icon, title } = meta;
|
||||
const singlePath = resolve(resolve(parentPath, path), firstChild.path);
|
||||
return (
|
||||
<MenuItem
|
||||
key={singlePath}
|
||||
value={singlePath}
|
||||
icon={Icon ? <Icon /> : undefined}
|
||||
onClick={() => navigate(singlePath)}
|
||||
>
|
||||
{title}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SubMenu key={routerPath} value={routerPath} title={title} icon={Icon ? <Icon /> : undefined}>
|
||||
{renderMenuItems(children, routerPath)}
|
||||
</SubMenu>
|
||||
);
|
||||
});
|
||||
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 [active, setActive] = useState<MenuValue>(location.pathname); // todo
|
||||
|
||||
return (
|
||||
<HeadMenu
|
||||
expandType='popup'
|
||||
style={{ marginBottom: 20 }}
|
||||
value={active}
|
||||
theme={globalState.theme}
|
||||
onChange={(v) => setActive(v)}
|
||||
>
|
||||
{renderMenuItems(router)}
|
||||
</HeadMenu>
|
||||
);
|
||||
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 { version } = globalState;
|
||||
const bottomText = globalState.collapsed ? version : `元境幻生管理台 ${version}`;
|
||||
|
||||
return (
|
||||
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}
|
||||
collapsed={globalState.collapsed}
|
||||
operations={props.showOperation ? <div className={Style.menuTip}>{bottomText}</div> : undefined}
|
||||
logo={props.showLogo ? <MenuLogo collapsed={globalState.collapsed} /> : undefined}
|
||||
>
|
||||
{renderMenuItems(router)}
|
||||
</Menu>
|
||||
);
|
||||
operations={props.showOperation ? <div className={Style.menuTip}>元境幻生管理台 {globalState.version}</div> : undefined}
|
||||
logo={props.showLogo ? <MenuLogo collapsed={false} /> : undefined}
|
||||
>{renderMenuItems(router, scopes, roles)}</Menu>
|
||||
</>;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user