Ant Design Pro V5精讲(实践篇三):动态菜单及权限控制

  • 用户需求场景

  • 一些软件产品,设计时就规划好了系统的操作角色,这些操作角色相对固定,例如:客户,管理员,这种场景完全只需要前端的路由与菜单配置方案,无需要动态菜单与权限分配功能。
  • 一些软件产品,系统的操作角色是动态创建的,需要给角色灵活分配能使用哪些菜单项,这时候只能采用动态菜单的方案,即菜单的生成是根据后端的api生成的(通常根据用户的分配角色取得这些菜单项,并且菜单项去重后的集合)。

前端菜单与权限方案

  1. 只需要利用ant design pro V5现有的routes.ts文件和src/utils/access.ts配合即可完成菜单项及权限的控制。
  2. 这时候可以按前面篇章说的可以配上给二级菜单支持图标的显示。

后端菜单与权限方案

  1. 利用ProLayout的menuDataRender方法,可以动态取得后端api的菜单项信息,通常根据登录用户的拥有的角色,把这些角色的所有菜单项权限(不允许重复,即去重后)的集合取回来,把这个数组配置给menuDataRender。

详见以下代码,重点看app.tsx新增的menuDataRender方法:

重点说明一:此方案二级菜单的图标不能显示,所以要注释掉原来配置二级菜单显示图标的功能。

重点说明二:routes.ts中同时也要配置菜单项的内容,即菜单项还需要路由的配置,否则无法使用。

重点说明三:避免把图标库全部引入的方案,会增大工程3M的大小文件,采用只引入用到的图标,以下方案做了推荐。

重点说明四:注意loopMenuItem函数的功能,它把图标字符串转换为图标节点对象,否则一级菜单无法显示图标。

// 南极客 增加动态菜单支持 2021.7.8
import type { Settings as LayoutSettings, BasicLayoutProps, MenuDataItem } from '@ant-design/pro-layout';
import { PageLoading } from '@ant-design/pro-layout';
import { notification } from 'antd';
import type { RequestConfig, RunTimeLayoutConfig } from 'umi';
import { history, Link } from 'umi';
import RightContent from '@/components/RightContent';
import Footer from '@/components/Footer';
import type { ResponseError } from 'umi-request';
import { currentUser as queryCurrentUser } from './services/ant-design-pro/api';
// import { BookOutlined, LinkOutlined } from '@ant-design/icons';
/* 以下内容 南极客 add 2021.7.8 */
import { BookOutlined, LinkOutlined, SmileOutlined, HeartOutlined } from '@ant-design/icons';
import * as Icon from '@ant-design/icons';
import React from 'react';

const isDev = process.env.NODE_ENV === 'development';
const loginPath = '/user/login';

// 南极客 add 2021.7.8 动态菜单
const iconEnum  = {
  SmileOutlined: <SmileOutlined />,
  HeartOutlined: <HeartOutlined />,
};

/** 获取用户信息比较慢的时候会展示一个 loading */
export const initialStateConfig = {
  loading: <PageLoading />,
};

/**
 * @see  https://umijs.org/zh-CN/plugins/plugin-initial-state
 * */
export async function getInitialState(): Promise<{
  settings?: Partial<LayoutSettings>;
  currentUser?: API.CurrentUser;
  fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
  // 南极客 add 2021.7.8 动态菜单
  menu: MenuDataItem[];
}> {
  const fetchUserInfo = async () => {
    try {
      const currentUser = await queryCurrentUser();
      return currentUser;
    } catch (error) {
      history.push(loginPath);
    }
    return undefined;
  };
  // 如果是登录页面,不执行
  if (history.location.pathname !== loginPath) {
    const currentUser = await fetchUserInfo();
    // 南极客 2021.7.8 add
    // const menuData = await getMenuList(currentUser?.userid || '');
    // 模拟后台取得的api 数据
    const menuData = [
      {
        path: '/admin',
        name: 'admin',
        icon: 'HeartOutlined',
        component: './Admin',
        routes: [
          {
            path: '/admin/sub-page',
            name: 'sub-page',
            icon: 'HeartOutlined',
            component: './Welcome',
          },
        ],
      },
      {
        name: 'ebyte.sys.userlist',
        icon: 'SmileOutlined',
        path: '/userlist',
        component: './UserList',
      },
      {
        name: 'list.table-list',
        icon: 'HeartOutlined',
        path: '/list',
        component: './TableList',
      },
    ];
    return {
      fetchUserInfo,
      currentUser,
      settings: {},
      // 南极客 增加动态菜单2021.7.8
      menu: menuData,
    };
  }
  return {
    fetchUserInfo,
    settings: {},
    // 南极客 2021.7.8 add
    menu: [],
  };
}

// 南极客 2021.7.8 add 递归生成菜单
const loopMenuItem = (menus: MenuDataItem[]): MenuDataItem[] => (
  menus.map(({ icon, children, ...item }) => {
    return {
      ...item,
      //  icon: icon && IconMap[icon as string],
      // 方案一: 此用法会把所有的图标库引入,造成增加工程3M大小,请谨用
      // icon: icon && React.createElement(Icon[icon]),
      // 方案二: 建议用此方案,只引入用到的图标,避免图标库全部引入
      icon: icon && iconEnum [icon],
      children: children && loopMenuItem(children),
    }
  })
);

// https://umijs.org/zh-CN/plugins/plugin-layout
// 南极客 fixed 2021.7.8
// export const layout: RunTimeLayoutConfig = ({ initialState }) =>  {
export const layout = ({ initialState, }: {
  initialState: { settings?: LayoutSettings; currentUser?: API.CurrentUser; menu: MenuDataItem[]; };
}): BasicLayoutProps => {
  return {
    // 南极客 2021.7.8 add 动态菜单支持
    menuDataRender: () => loopMenuItem(initialState.menu),
    // 南极客 修补:二级图标正常显示2021.7.8,动态菜单下不管用,只会显示图标的名称
    /*
     menuItemRender: (menuItemProps, defaultDom) => {
      if (
        menuItemProps.isUrl || !menuItemProps.path) {
        return defaultDom;
      }
      // 支持二级菜单显示icon
      return (
       <Link to={menuItemProps.path}>
          {menuItemProps.pro_layout_parentKeys 
          && menuItemProps.pro_layout_parentKeys.length > 0 &&
          menuItemProps.icon}{defaultDom}
        </Link>
    );
  },
  */
    rightContentRender: () => <RightContent />,
    disableContentMargin: false,
    /* 去掉水印功能 南极客 2021.4.22
    waterMarkProps: {
      content: initialState?.currentUser?.name,
    },
    */
    footerRender: () => <Footer />,
    onPageChange: () => {
      const { location } = history;
      // 如果没有登录,重定向到 login
      if (!initialState?.currentUser && location.pathname !== loginPath) {
        history.push(loginPath);
      }
    },
    links: isDev
      ? [
        /* 去掉菜单底部链接  南极客 2021.4.22
          <Link to="/umi/plugin/openapi" target="_blank">
            <LinkOutlined />
            <span>openAPI 文档</span>
          </Link>,
          <Link to="/~docs">
            <BookOutlined />
            <span>业务组件文档</span>
          </Link>,
          */
      ]
      : [],
    menuHeaderRender: undefined,
    // 自定义 403 页面
    // unAccessible: <div>unAccessible</div>,
    ...initialState?.settings,
  };
};

const codeMessage = {
  200: '服务器成功返回请求的数据。',
  201: '新建或修改数据成功。',
  202: '一个请求已经进入后台排队(异步任务)。',
  204: '删除数据成功。',
  400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
  401: '用户没有权限(令牌、用户名、密码错误)。',
  403: '用户得到授权,但是访问是被禁止的。',
  404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
  405: '请求方法不被允许。',
  406: '请求的格式不可得。',
  410: '请求的资源被永久删除,且不会再得到的。',
  422: '当创建一个对象时,发生一个验证错误。',
  500: '服务器发生错误,请检查服务器。',
  502: '网关错误。',
  503: '服务不可用,服务器暂时过载或维护。',
  504: '网关超时。',
};

/** 异常处理程序
 * @see https://beta-pro.ant.design/docs/request-cn
 */
const errorHandler = (error: ResponseError) => {
  const { response } = error;
  if (response && response.status) {
    const errorText = codeMessage[response.status] || response.statusText;
    const { status, url } = response;

    notification.error({
      message: `请求错误 ${status}: ${url}`,
      description: errorText,
    });
  }

  if (!response) {
    notification.error({
      description: '您的网络发生异常,无法连接服务器',
      message: '网络异常',
    });
  }
  throw error;
};

// https://umijs.org/zh-CN/plugins/plugin-request
export const request: RequestConfig = {
  errorHandler,
};

复制代码
  1. 自行设计菜单项的配置模块功能,以及给角色分配菜单项权限的模块功能,同时编写后端api取得菜单项的方法,返回的json格式,参考routes.ts的格式。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享