项目架构使用的是 Ant Design Pro 和 Midway 前后端分离方案 。在 Ant Design Pro 中,路由和菜单是组织起一个应用的关键骨架,pro 中的路由为了方便管理,使用了中心化的方式,在 config.ts 统一配置和管理。菜单是根据路由配置来生成的,菜单项名称,嵌套路径与路由高度耦合。
Ant Design Pro 的路由配置
通常,在一个Ant Design Pro项目中,在config.ts 中的路由配置如下:
在 Ant Design Pro 中根据路由配置生成的菜单:
权限控制系统是中后台系统中常见的需求之一,Ant Design Pro 已经为我们提供了权限控制的组件,实现一些基本的权限控制功能。详情看 Ant Design Pro 的权限管理
由于时间的关系,在实现菜单权限管理的功能时,并没有使用Ant Design Pro提供的权限控制组件来实现,而是在BasicLayout.tsx 文件中根据用户的角色来判断是否渲染该菜单项。
实现步骤:
1、在由配置中给每个路由添加 authority属性,配置准入权限
在 config.ts 文件的路由配置中给每个路由添加 authority: ['admin', 'user'],配置准入权限,可以配置多个角色,admin 值表示只有是管理员是才会显示该菜单,user 表示如果是普通用户,都会显示该菜单。(PS:Ant Design Pro提供的权限控制组件也是在congfig.ts文件中配置 authority 属性来配置准入权限)。因为没有使用框架提供的权限控制组件,在路由配置中配置相同的 authority 属性来配置菜单的准入权限,不影响我们自定义实现菜单的权限管理。
2、在 menuDataRender 函数中渲染菜单项
在 BasicLayout.tsx 文件的 menuDataRender 函数中根据后端返回的用户角色来判断是否渲染当前菜单项。(PS:项目中后端返回的用户角色数据保存在 dva 的 model 中)
menuDataRender 函数:
const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] => {
const menuListTemp = menuList.filter((item: MenuDataItem) => {
const authority: string[] = isString(item.authority)
? [item.authority]
: item.authority || [];
const authMap = new Map(authority.map((auth: string) => [auth, 1]));
// const localItem = { ...item, children: item.children ? menuDataRender(item.children) : [] };
if (!authMap.has(adminAuth)) {
return;
}
const localItem = { ...item, children: item.children ? menuDataRender(item.children) : [] };
return localItem as MenuDataItem;
});
return menuListTemp;
};
BasicLayout.tsx 文件完整代码:
import ProLayout, {
MenuDataItem,
BasicLayoutProps as ProLayoutProps,
Settings,
PageLoading,
} from '@ant-design/pro-layout';
// import { RouterTypes } from '@ant-design/pro-layout/typings'
import React, { useEffect, ReactNode, useState } from 'react';
import Link from 'umi/link';
import { Dispatch } from 'redux';
import { connect } from 'dva';
import { Layout, Breadcrumb, Icon, Menu, Alert, Spin } from 'antd';
import RightContent from '@/components/GlobalHeader/RightContent';
import { ConnectState } from '@/models/connect';
import { throttle } from 'lodash';
// import logo from '../assets/logo.png';
import styles from './BasicLayout.less';
// 导入配置
import config from '../../config/config';
export interface BasicLayoutProps extends ProLayoutProps {
breadcrumbNameMap: {
[path: string]: MenuDataItem;
};
route: ProLayoutProps['route'] & {
authority: string[];
};
settings: Settings;
dispatch: Dispatch;
collapsed: boolean;
adminAuth: string;
breadCrumb: string;
adminAuthLoading: boolean;
}
export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & {
breadcrumbNameMap: {
[path: string]: MenuDataItem;
};
};
// typeof 类型守卫
function isString(x: any): x is string {
return typeof x === 'string';
}
const BasicLayout: React.FC<BasicLayoutProps> = props => {
const { dispatch, settings, collapsed, adminAuth, adminAuthLoading } = props;
const [alertVisible, setAlertVisible] = useState(true);
useEffect(() => {
dispatch({
type: 'global/checkAdminAuth',
});
}, []);
const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] => {
const menuListTemp = menuList.filter((item: MenuDataItem) => {
const authority: string[] = isString(item.authority)
? [item.authority]
: item.authority || [];
const authMap = new Map(authority.map((auth: string) => [auth, 1]));
// const localItem = { ...item, children: item.children ? menuDataRender(item.children) : [] };
if (!authMap.has(adminAuth)) {
return;
}
const localItem = { ...item, children: item.children ? menuDataRender(item.children) : [] };
return localItem as MenuDataItem;
});
return menuListTemp;
};
const handleMenuCollapse = (): void => {
if (dispatch) {
dispatch({
type: 'global/changeLayoutCollapsed',
payload: !collapsed,
});
}
};
return (
<>
{window['env'] === 'prod' ? (
<>
<div
style={{
position: 'fixed',
width: '100%',
zIndex: 999,
left: 0,
top: 0,
}}
>
<Alert
message={
<span
style={{
color: 'red',
width: '100%',
fontSize: '14px',
fontWeight: 'bold',
textAlign: 'center',
}}
>
当前环境为生产环境,请注意操作
</span>
}
type="error"
showIcon
icon={<Icon type="exclamation-circle" />}
closable
onClose={(e: React.MouseEvent) => {
setAlertVisible(false);
}}
/>
</div>
{alertVisible && <div style={{ height: '39px' }}></div>}
</>
) : (
''
)}
<Layout className={styles.basicLayout}>
<Layout.Header>
<div className={styles.logo}>
<Link to="/">
<div className={styles.logoTitle}>
<h1>{settings.title}</h1>
<span>Hera manager platform</span>
</div>
</Link>
</div>
<div className={styles.rightContent}>
<RightContent />
</div>
</Layout.Header>
{adminAuthLoading ? (
<div className={styles.adminAuthLoadingBox} style={{ height: '100%' }}>
<Spin spinning={true} />
</div>
) : (
<>
<ProLayout
// logo={logo}
// menuHeaderRender={(logoDom, titleDom) => (
// <Link to="/">
// {logoDom}
// {titleDom}
// </Link>
// )}
className={styles.proLayoutContent}
collapsed={false}
siderWidth={200}
onCollapse={handleMenuCollapse}
menuItemRender={(menuItemProps, defaultDom) => {
if (menuItemProps.isUrl || menuItemProps.children) {
return defaultDom;
}
return <Link to={menuItemProps.path}>{defaultDom}</Link>;
}}
// breadcrumbRender={(routers = []) => [
// {
// path: '/',
// breadcrumbName: '首页',
// },
// ...routers,
// ]}
// itemRender={(route, params, routes, paths) => {
// const first = routes.indexOf(route) === 0;
// return first ? (
// <Link to={paths.join('/')}>{route.breadcrumbName}</Link>
// ) : (
// <span>{route.breadcrumbName}</span>
// );
// }}
// footerRender={footerRender}
menuDataRender={menuDataRender}
// rightContentRender={() => <RightContent />}
{...props}
{...settings}
menuHeaderRender={false}
footerRender={false}
/>
<Layout.Footer style={{ textAlign: 'center' }}>
Copyright © 2020 Alibaba groups. All rights reserved.
</Layout.Footer>
</>
)}
</Layout>
</>
);
};
export default connect(({ global, settings, loading }: ConnectState) => ({
collapsed: global.collapsed,
settings,
adminAuth: global.adminAuth || '',
breadCrumb: global.breadCrumb || '',
adminAuthLoading: loading.effects['global/checkAdminAuth'] || false,
}))(BasicLayout);