基于 Ant Design Pro 的菜单栏权限控制

2,069 阅读3分钟

项目架构使用的是 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);