Ant Design Pro V5精讲(实践篇三):动态菜单及权限控制
用户需求场景
一些软件产品,设计时就规划好了系统的操作角色,这些操作角色相对固定,例如:客户,管理员,这种场景完全只需要前端的路由与菜单配置方案,无需要动态菜单与权限分配功能。
一些软件产品,系统的操作角色是动态创建的,需要给角色灵活分配能使用哪些菜单项,这时候只能采用动态菜单的方案,即菜单的生成是根据后端的api生成的(通常根据用户的分配角色取得这些菜单项,并且菜单项去重后的集合)。
前端菜单与权限方案
只需要利用ant design pro V5现有的routes.ts文件和src/utils/access.ts配合即可完成菜单项及权限的控制。
这时候可以按前面篇章说的可以配上给二级菜单支持图标的显示。
后端菜单与权限方案
利用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, }; 复制代码
自行设计菜单项的配置模块功能,以及给角色分配菜单项权限的模块功能,同时编写后端api取得菜单项的方法,返回的json格式,参考routes.ts的格式。
作者:南极客
链接:https://juejin.cn/post/6982616430277558309