feat: integrate platform backend and application interfaces

This commit is contained in:
Qiufeng
2026-09-02 21:09:47 +08:00
parent cd9ac1af70
commit b16390dd41
648 changed files with 102488 additions and 17737 deletions
+73 -91
View File
@@ -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>
</>;
});