|
|
import React, { useState, ReactNode, useRef, useEffect } from 'react';
|
|
|
import { Layout, Menu, Breadcrumb, Spin } from '@arco-design/web-react';
|
|
|
import cs from 'classnames';
|
|
|
import {
|
|
|
IconApps,
|
|
|
IconMenuFold,
|
|
|
IconMenuUnfold,
|
|
|
IconArchive,
|
|
|
IconUnorderedList,
|
|
|
IconMindMapping,
|
|
|
IconCommon,
|
|
|
IconCode,
|
|
|
IconHome,
|
|
|
IconStorage
|
|
|
} from '@arco-design/web-react/icon';
|
|
|
import { useSelector } from 'react-redux';
|
|
|
import { useRouter } from 'next/router';
|
|
|
import Link from 'next/link';
|
|
|
import qs from 'query-string';
|
|
|
import Navbar from '../components/NavBar';
|
|
|
import Footer from '../components/Footer';
|
|
|
import useRoute, { IRoute } from '@/routes';
|
|
|
import useLocale from '@/utils/useLocale';
|
|
|
import { GlobalState } from '@/store';
|
|
|
import getUrlParams from '@/utils/getUrlParams';
|
|
|
import styles from '@/style/layout.module.less';
|
|
|
import NoAccess from '@/pages/exception/403';
|
|
|
|
|
|
const MenuItem = Menu.Item;
|
|
|
const SubMenu = Menu.SubMenu;
|
|
|
|
|
|
const Sider = Layout.Sider;
|
|
|
const Content = Layout.Content;
|
|
|
|
|
|
function getIconFromKey(key) {
|
|
|
switch (key) {
|
|
|
case 'dashboard/workplace':
|
|
|
return <IconHome className={styles.icon} />;
|
|
|
case 'scene':
|
|
|
return <IconArchive className={styles.icon} />;
|
|
|
case 'application':
|
|
|
return <IconApps className={styles.icon} />;
|
|
|
case 'instance':
|
|
|
return <IconUnorderedList className={styles.icon} />;
|
|
|
case 'componentDevelopment':
|
|
|
return <IconCode className={styles.icon} />;
|
|
|
case 'componentLibrary':
|
|
|
return <IconMindMapping className={styles.icon} />;
|
|
|
case 'compositeCompLibrary':
|
|
|
return <IconCommon className={styles.icon} />;
|
|
|
case 'componentMarket':
|
|
|
return <IconStorage className={styles.icon} />;
|
|
|
default:
|
|
|
return <div className={styles['icon-empty']} />;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function PageLayout({ children }: { children: ReactNode }) {
|
|
|
const urlParams = getUrlParams();
|
|
|
const router = useRouter();
|
|
|
const pathname = router.pathname;
|
|
|
const currentComponent = qs.parseUrl(pathname).url.slice(1);
|
|
|
const locale = useLocale();
|
|
|
const { userInfo, settings, userLoading } = useSelector(
|
|
|
(state: GlobalState) => state
|
|
|
);
|
|
|
|
|
|
const [collapsed, setCollapsed] = useState<boolean>(false);
|
|
|
|
|
|
const [routes, defaultRoute] = useRoute(userInfo?.permissions);
|
|
|
|
|
|
const defaultSelectedKeys = [currentComponent || defaultRoute];
|
|
|
const paths = (currentComponent || defaultRoute).split('/');
|
|
|
const defaultOpenKeys = paths.slice(0, paths.length - 1);
|
|
|
|
|
|
const [selectedKeys, setSelectedKeys] =
|
|
|
useState<string[]>(defaultSelectedKeys);
|
|
|
const [openKeys, setOpenKeys] = useState<string[]>(defaultOpenKeys);
|
|
|
|
|
|
const navbarHeight = 60;
|
|
|
const menuWidth = collapsed ? 48 : settings?.menuWidth;
|
|
|
|
|
|
const showNavbar = settings?.navbar && urlParams.navbar !== false;
|
|
|
const showMenu = settings?.menu && urlParams.menu !== false;
|
|
|
const showFooter = settings?.footer && urlParams.footer !== false;
|
|
|
|
|
|
const routeMap = useRef<Map<string, ReactNode[]>>(new Map());
|
|
|
const menuMap = useRef<
|
|
|
Map<string, { menuItem?: boolean; subMenu?: boolean }>
|
|
|
>(new Map());
|
|
|
|
|
|
const [breadcrumb, setBreadCrumb] = useState([]);
|
|
|
|
|
|
function onClickMenuItem(key) {
|
|
|
setSelectedKeys([key]);
|
|
|
}
|
|
|
|
|
|
function toggleCollapse() {
|
|
|
setCollapsed((collapsed) => !collapsed);
|
|
|
}
|
|
|
|
|
|
const paddingLeft = showMenu ? { paddingLeft: menuWidth } : {};
|
|
|
const paddingTop = showNavbar ? { paddingTop: navbarHeight } : {};
|
|
|
const paddingStyle = { ...paddingLeft, ...paddingTop };
|
|
|
|
|
|
// 初始化时创建并实例路由映射
|
|
|
useEffect(() => {
|
|
|
// 仅构建 routeMap,不渲染菜单
|
|
|
function buildRouteMap(_routes: IRoute[], parentNode = []) {
|
|
|
_routes.forEach((route) => {
|
|
|
const { breadcrumb = true, ignore } = route;
|
|
|
|
|
|
routeMap.current.set(
|
|
|
`/${route.key}`,
|
|
|
breadcrumb ? [...parentNode, route.name] : []
|
|
|
);
|
|
|
|
|
|
const visibleChildren = (route.children || []).filter((child) => {
|
|
|
const { ignore, breadcrumb = true } = child;
|
|
|
if (ignore || route.ignore) {
|
|
|
routeMap.current.set(
|
|
|
`/${child.key}`,
|
|
|
breadcrumb ? [...parentNode, route.name, child.name] : []
|
|
|
);
|
|
|
}
|
|
|
return !ignore;
|
|
|
});
|
|
|
|
|
|
if (!ignore && visibleChildren.length) {
|
|
|
buildRouteMap(visibleChildren, [...parentNode, route.name]);
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
buildRouteMap(routes);
|
|
|
}, [routes]);
|
|
|
|
|
|
function renderRoutes(locale) {
|
|
|
return function travel(_routes: IRoute[], level, parentNode = []) {
|
|
|
return _routes.map((route) => {
|
|
|
const { ignore } = route;
|
|
|
const iconDom = getIconFromKey(route.key);
|
|
|
const titleDom = (
|
|
|
<>
|
|
|
{iconDom} {locale[route.name] || route.name}
|
|
|
</>
|
|
|
);
|
|
|
|
|
|
const visibleChildren = (route.children || []).filter((child) => {
|
|
|
return !child.ignore;
|
|
|
});
|
|
|
|
|
|
if (ignore) {
|
|
|
return '';
|
|
|
}
|
|
|
|
|
|
if (visibleChildren.length) {
|
|
|
menuMap.current.set(route.key, { subMenu: true });
|
|
|
return (
|
|
|
<SubMenu key={route.key} title={titleDom}>
|
|
|
{travel(visibleChildren, level + 1, [...parentNode, route.name])}
|
|
|
</SubMenu>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
menuMap.current.set(route.key, { menuItem: true });
|
|
|
return (
|
|
|
<MenuItem key={route.key}>
|
|
|
<Link href={`/${route.key}`} passHref>
|
|
|
<a suppressHydrationWarning>{titleDom}</a>
|
|
|
</Link>
|
|
|
</MenuItem>
|
|
|
);
|
|
|
});
|
|
|
};
|
|
|
}
|
|
|
|
|
|
function updateMenuStatus() {
|
|
|
const pathKeys = pathname.split('/');
|
|
|
const newSelectedKeys: string[] = [];
|
|
|
const newOpenKeys: string[] = [...openKeys];
|
|
|
while (pathKeys.length > 0) {
|
|
|
const currentRouteKey = pathKeys.join('/');
|
|
|
const menuKey = currentRouteKey.replace(/^\//, '');
|
|
|
const menuType = menuMap.current.get(menuKey);
|
|
|
if (menuType && menuType.menuItem) {
|
|
|
newSelectedKeys.push(menuKey);
|
|
|
}
|
|
|
if (menuType && menuType.subMenu && !openKeys.includes(menuKey)) {
|
|
|
newOpenKeys.push(menuKey);
|
|
|
}
|
|
|
pathKeys.pop();
|
|
|
}
|
|
|
setSelectedKeys(newSelectedKeys);
|
|
|
setOpenKeys(newOpenKeys);
|
|
|
}
|
|
|
|
|
|
useEffect(() => {
|
|
|
const routeConfig = routeMap.current.get(pathname);
|
|
|
setBreadCrumb(routeConfig || []);
|
|
|
updateMenuStatus();
|
|
|
}, [pathname]);
|
|
|
|
|
|
return (
|
|
|
<Layout className={styles.layout}>
|
|
|
<div
|
|
|
className={cs(styles['layout-navbar'], {
|
|
|
[styles['layout-navbar-hidden']]: !showNavbar
|
|
|
})}
|
|
|
>
|
|
|
<Navbar show={showNavbar} />
|
|
|
</div>
|
|
|
{userLoading ? (
|
|
|
<Spin className={styles['spin']} />
|
|
|
) : (
|
|
|
<Layout>
|
|
|
{showMenu && (
|
|
|
<Sider
|
|
|
className={styles['layout-sider']}
|
|
|
width={menuWidth}
|
|
|
collapsed={collapsed}
|
|
|
onCollapse={setCollapsed}
|
|
|
trigger={null}
|
|
|
collapsible
|
|
|
breakpoint="xl"
|
|
|
style={paddingTop}
|
|
|
>
|
|
|
<div className={styles['menu-wrapper']}>
|
|
|
<Menu
|
|
|
collapse={collapsed}
|
|
|
onClickMenuItem={onClickMenuItem}
|
|
|
selectedKeys={selectedKeys}
|
|
|
openKeys={openKeys}
|
|
|
onClickSubMenu={(_, openKeys) => {
|
|
|
setOpenKeys(openKeys);
|
|
|
}}
|
|
|
>
|
|
|
{renderRoutes(locale)(routes, 1)}
|
|
|
</Menu>
|
|
|
</div>
|
|
|
<div className={styles['collapse-btn']} onClick={toggleCollapse}>
|
|
|
{collapsed ? <IconMenuUnfold /> : <IconMenuFold />}
|
|
|
</div>
|
|
|
</Sider>
|
|
|
)}
|
|
|
<Layout className={styles['layout-content']} style={paddingStyle}>
|
|
|
<div className={styles['layout-content-wrapper']}>
|
|
|
{!!breadcrumb.length && (
|
|
|
<div className={styles['layout-breadcrumb']}>
|
|
|
<Breadcrumb>
|
|
|
{breadcrumb.map((node, index) => (
|
|
|
<Breadcrumb.Item key={index}>
|
|
|
{typeof node === 'string' ? locale[node] || node : node}
|
|
|
</Breadcrumb.Item>
|
|
|
))}
|
|
|
</Breadcrumb>
|
|
|
</div>
|
|
|
)}
|
|
|
<Content>
|
|
|
{routeMap.current.has(pathname) ? children : <NoAccess />}
|
|
|
</Content>
|
|
|
</div>
|
|
|
{/*{showFooter && <Footer />}*/}
|
|
|
</Layout>
|
|
|
</Layout>
|
|
|
)}
|
|
|
</Layout>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
export default PageLayout;
|