You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

272 lines
8.4 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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;