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.

293 lines
9.1 KiB
TypeScript

import React, { useState, ReactNode, useRef, useEffect, useMemo } 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 getUrlParams from '@/utils/getUrlParams';
import styles from '@/style/layout.module.less';
import NoAccess from '@/pages/exception/403';
import { openWindow, OpenWindowOptions } from '@/utils/common';
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 { settings } = useSelector((state) => state.global);
const { userInfo, userLoading } = useSelector((state) => state.user);
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 && !pathname.includes('ideContainer');
const showFooter = settings?.footer && urlParams.footer !== false;
const showWrapperPadding = settings?.noWrapperPadding || pathname.includes('ideContainer');
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: string) {
if (key === 'componentDevelopment') {
const url = `/ideContainer`;
const params: OpenWindowOptions = {
target: '_blank',
menu: false,
identity: key
};
openWindow(url, params);
}
else setSelectedKeys([key]);
}
function toggleCollapse() {
setCollapsed((collapsed) => !collapsed);
}
const paddingLeft = showMenu ? { paddingLeft: menuWidth } : {};
const paddingTop = showNavbar ? { paddingTop: navbarHeight } : {};
const paddingStyle = { ...paddingLeft, ...paddingTop };
const routeMapMemo = useMemo(() => {
const map = new Map<string, ReactNode[]>();
function buildRouteMap(_routes: IRoute[], parentNode = []) {
_routes.forEach((route) => {
const { breadcrumb = true, ignore } = route;
map.set(
`/${route.key}`,
breadcrumb ? [...parentNode, route.name] : []
);
const visibleChildren = (route.children || []).filter((child) => {
const { ignore, breadcrumb = true } = child;
if (ignore || route.ignore) {
map.set(
`/${child.key}`,
breadcrumb ? [...parentNode, route.name, child.name] : []
);
}
return !ignore;
});
if (!ignore && visibleChildren.length) {
buildRouteMap(visibleChildren, [...parentNode, route.name]);
}
});
}
buildRouteMap(routes);
return map;
}, [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 });
if (route?.openWindow) {
return (
<MenuItem key={route.key}>
<a suppressHydrationWarning>{titleDom}</a>
</MenuItem>
);
}
else {
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 = routeMapMemo.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={showWrapperPadding ? styles['layout-content-wrapper-no-padding '] : 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>
{routeMapMemo.has(pathname) ? children : <NoAccess />}
</Content>
</div>
{/*{showFooter && <Footer />}*/}
</Layout>
</Layout>
)}
</Layout>
);
}
export default PageLayout;