美文网首页
Ant Design Pro V4 -- 后端动态菜单

Ant Design Pro V4 -- 后端动态菜单

作者: 2010jing | 来源:发表于2021-02-08 17:57 被阅读0次

    01 版本信息

    • Ant Design Pro v4.5.0
    • umi v3.2.14
    • umi-request v1.0.8
    • Pro-layout v6.9.0
    • TypeScript v4.0.5
    • Flask后端 v1.1.2

    02 过程思路

    • 后端 使用 flask 提供菜单接口
    • 使用react hooks的useEffect 中使用dva的dispatch来请求菜单
    • BasicLayout.tsx 将从后台请求返回的菜单数据,传递给 menuDataRender属性中进行渲染

    03 代码实现

    Flask后端接口
    • 返回的数据中一定要有path, name。name可以覆盖前端写的name。
    • 返回的数据可以设置icon,但是不起作用,文章后面有提供解决方案。
    • 返回的数据的authority可以覆盖前端写的authority。如果返回的数据没有authority,则前端写的authority会生效。
    from flask import jsonify, g
    from app.libs.error_code import NotFound, DeleteSuccess, AuthFailed
    from app.libs.redprint import Redprint
    from app.libs.token_auth import auth
    from app.models.base import db
    from app.models.user import User
    
    # Redprint
    api = Redprint("user")
    
    @api.route("/menu", methods=["GET"])
    def get_menu():
        routes = [
            {
                "path": "/",
                "name": "home",
                "icon": "HomeOutlined",
                "component": "./home/index",
            },
            {
                "path": "/venue",
                "name": "venue",
                "icon": "CarryOutOutlined",
                "routes": [
                    {
                        "name": "T8-305",
                        "path": "/venue/view/T8-305",
                        "component": "./venue/index",
                    },
                    {
                        "name": "T8-306",
                        "path": "/venue/view/T8-306",
                        "component": "./venue/index",
                    },
                ],
            },
            {
                "path": "/officehour",
                "name": "officehour",
                "icon": "CarryOutOutlined",
                "authority": ["admin", "user"],
                "routes": [
                    {
                        "name": "hejing",
                        "path": "/officehour/view/hejing",
                        "component": "./venue/index",
                    },
                    {
                        "name": "helen",
                        "path": "/officehour/view/helen",
                        "component": "./venue/index",
                    },
                ],
            },
            {
                "path": "/form",
                "icon": "form",
                "name": "form",
                "routes": [
                    {"path": "/", "redirect": "/form/basic-form",},
                    {
                        "name": "basic-form",
                        "icon": "smile",
                        "path": "/form/basic-form",
                        "component": "./form/basic-form",
                    },
                    {
                        "name": "step-form",
                        "icon": "smile",
                        "path": "/form/step-form",
                        "component": "./form/step-form",
                    },
                    {
                        "name": "advanced-form",
                        "icon": "smile",
                        "path": "/form/advanced-form",
                        "component": "./form/advanced-form",
                    },
                ],
            },
            {"path": "/", "redirect": "/list/table-list",},
            {
                "name": "table-list",
                "icon": "smile",
                "path": "/list/table-list",
                "component": "./list/table-list",
            },
            {
                "name": "account",
                "icon": "user",
                "path": "/account",
                "routes": [
                    {"path": "/", "redirect": "/account/center",},
                    {
                        "name": "center",
                        "icon": "smile",
                        "path": "/account/center",
                        "component": "./account/center",
                    },
                    {
                        "name": "settings",
                        "icon": "smile",
                        "path": "/account/settings",
                        "component": "./account/settings",
                    },
                ],
            },
            {"component": "404",},
        ]
    
        return jsonify(routes)
    
    
    定义 menu 模型 menu.ts

    src\models\menu.ts

    import { Effect, Reducer } from 'umi';
    import { MenuDataItem } from '@ant-design/pro-layout';
    import { getMenuData } from '@/services/menu';
    
    export interface MenuModelState {
      menuData: MenuDataItem[];
      loading: boolean;
    }
    
    export interface MenuModelType {
      namespace: 'menu';
      state: {
        menuData: []; //  存储menu数据
        loading: true; // loading的初始值为true
      };
      effects: {
        fetchMenu: Effect;
      };
      reducers: {
        saveMenuData: Reducer<MenuModelState>;
      };
    }
    
    const MenuModel: MenuModelType = {
      namespace: 'menu',
      state: {
        menuData: [],
        loading: true,
      },
    
      effects: {
        *fetchMenu(_, { put, call }) {
          const response = yield call(getMenuData);
          console.log('yield call(getMenuData)');
          console.log(response);
          yield put({
            type: 'saveMenuData',
            payload: response,
          });
        },
      },
    
      reducers: {
        saveMenuData(state, action) {
          return {
            ...state,
            menuData: action.payload || [],
            loading: false, // 后台数据返回了,loading就改成false
          };
        },
      },
    };
    export default MenuModel;
    
    
    在connect中定义menu的类型

    src\models\connect.d.ts

    import type { MenuDataItem, Settings as ProSettings } from '@ant-design/pro-layout';
    import { GlobalModelState } from './global';
    import { UserModelState } from './user';
    import type { StateType } from './login';
    import { MenuModelState } from './menu';
    
    export { GlobalModelState, UserModelState };
    
    export type Loading = {
      global: boolean;
      effects: Record<string, boolean | undefined>;
      models: {
        global?: boolean;
        menu?: boolean;
        setting?: boolean;
        user?: boolean;
        login?: boolean;
      };
    };
    
    export type ConnectState = {
      global: GlobalModelState;
      loading: Loading;
      settings: ProSettings;
      user: UserModelState;
      login: StateType;
      menu: MenuModelState; // 定义menu的类型,MenuModelState是在src/models/menu.ts中定义的
    };
    
    export type Route = {
      routes?: Route[];
    } & MenuDataItem;
    
    
    获取菜单service

    src\services\menu.ts

    
    import { Constants } from '@/utils/constants';
    import request from '@/utils/request';
    
    export async function getMenuData(): Promise<any> {
      return request(`${Constants.baseUrl}/v1/user/menu`, {
        method: 'GET',
        data: { },
      });
    }
    
    
    后台返回的数据在前端项目中也还是要写的

    config\config.ts

    // https://umijs.org/config/
    import { defineConfig } from 'umi';
    import defaultSettings from './defaultSettings';
    import proxy from './proxy';
    
    const { REACT_APP_ENV } = process.env;
    
    export default defineConfig({
      hash: true,
      antd: {},
      dva: {
        hmr: true,
      },
      history: {
        type: 'browser',
      },
      locale: {
        // default zh-CN
        default: 'zh-CN',
        antd: true,
        // default true, when it is true, will use `navigator.language` overwrite default
        baseNavigator: true,
      },
      dynamicImport: {
        loading: '@/components/PageLoading/index',
      },
      targets: {
        ie: 11,
      },
      // umi routes: https://umijs.org/docs/routing
      routes: [
        {
          path: '/',
          component: '../layouts/BlankLayout',
          routes: [
            {
              path: '/user',
              component: '../layouts/UserLayout',
              routes: [
                {
                  path: '/user/login',
                  name: 'login',
                  component: './User/login',
                },
    
                {
                  path: '/user',
                  redirect: '/user/login',
                },
                {
                  name: 'register-result',
                  icon: 'smile',
                  path: '/user/register-result',
                  component: './user/register-result',
                },
                {
                  name: 'register',
                  icon: 'smile',
                  path: '/user/register',
                  component: './user/register',
                },
                {
                  component: '404',
                },
              ],
            },
            {
              path: '/',
              component: '../layouts/BasicLayout',
              Routes: ['src/pages/Authorized'],
              // authority: ['admin', 'user'],
    
              routes: [
                // home
                {
                  path: '/',
                  name: 'home',
                  icon: 'HomeOutlined',
                  component: './home/index',
                },
                // venue
                {
                  path: '/venue',
                  name: 'venue',
                  icon: 'CarryOutOutlined',
                  routes: [
                    {
                      name: 'T8-305',
                      path: '/venue/view/T8-305',
                      component: './venue/index',
                    },
                    {
                      name: 'T8-306',
                      path: '/venue/view/T8-306',
                      component: './venue/index',
                    },
                  ],
                },
                // officehour
                {
                  path: '/officehour',
                  name: 'officehour',
                  icon: 'CarryOutOutlined',
                  authority: ['admin', 'user'],
                  routes: [
                    {
                      name: 'hejing',
                      path: '/officehour/view/hejing',
                      component: './venue/index',
                    },
                    {
                      name: 'helen',
                      path: '/officehour/view/helen',
                      component: './venue/index',
                    },
                  ],
                },
                // {
                //   path: '/',
                //   redirect: '/dashboard/analysis',
                // },
                // {
                //   path: '/dashboard',
                //   name: 'dashboard',
                //   icon: 'dashboard',
                //   routes: [
                //     {
                //       path: '/',
                //       redirect: '/dashboard/analysis',
                //     },
                //     {
                //       name: 'analysis',
                //       icon: 'smile',
                //       path: '/dashboard/analysis',
                //       component: './dashboard/analysis',
                //     },
                //     {
                //       name: 'monitor',
                //       icon: 'smile',
                //       path: '/dashboard/monitor',
                //       component: './dashboard/monitor',
                //     },
                //     {
                //       name: 'workplace',
                //       icon: 'smile',
                //       path: '/dashboard/workplace',
                //       component: './dashboard/workplace',
                //     },
                //   ],
                // },
                {
                  path: '/form',
                  icon: 'form',
                  name: 'form',
    
                  routes: [
                    {
                      path: '/',
                      redirect: '/form/basic-form',
                    },
                    {
                      name: 'basic-form',
                      icon: 'smile',
                      path: '/form/basic-form',
                      component: './form/basic-form',
                    },
                    {
                      name: 'step-form',
                      icon: 'smile',
                      path: '/form/step-form',
                      component: './form/step-form',
                    },
                    {
                      name: 'advanced-form',
                      icon: 'smile',
                      path: '/form/advanced-form',
                      component: './form/advanced-form',
                    },
                  ],
                },
                // {
                //   path: '/list',
                //   icon: 'table',
                //   name: 'list',
    
                //   routes: [
                //     {
                //       path: '/list/search',
                //       name: 'search-list',
                //       component: './list/search',
                //       routes: [
                //         {
                //           path: '/list/search',
                //           redirect: '/list/search/articles',
                //         },
                //         {
                //           name: 'articles',
                //           icon: 'smile',
                //           path: '/list/search/articles',
                //           component: './list/search/articles',
                //         },
                //         {
                //           name: 'projects',
                //           icon: 'smile',
                //           path: '/list/search/projects',
                //           component: './list/search/projects',
                //         },
                //         {
                //           name: 'applications',
                //           icon: 'smile',
                //           path: '/list/search/applications',
                //           component: './list/search/applications',
                //         },
                //       ],
                //     },
                {
                  path: '/',
                  redirect: '/list/table-list',
                },
                {
                  name: 'table-list',
                  icon: 'smile',
                  path: '/list/table-list',
                  component: './list/table-list',
                },
                //     {
                //       name: 'basic-list',
                //       icon: 'smile',
                //       path: '/list/basic-list',
                //       component: './list/basic-list',
                //     },
                //     {
                //       name: 'card-list',
                //       icon: 'smile',
                //       path: '/list/card-list',
                //       component: './list/card-list',
                //     },
                //   ],
                // },
                // {
                //   path: '/profile',
                //   name: 'profile',
                //   icon: 'profile',
                //   routes: [
                //     {
                //       path: '/',
                //       redirect: '/profile/basic',
                //     },
                //     {
                //       name: 'basic',
                //       icon: 'smile',
                //       path: '/profile/basic',
                //       component: './profile/basic',
                //     },
                //     {
                //       name: 'advanced',
                //       icon: 'smile',
                //       path: '/profile/advanced',
                //       component: './profile/advanced',
                //     },
                //   ],
                // },
                // {
                //   name: 'result',
                //   icon: 'CheckCircleOutlined',
                //   path: '/result',
                //   routes: [
                //     {
                //       path: '/',
                //       redirect: '/result/success',
                //     },
                //     {
                //       name: 'success',
                //       icon: 'smile',
                //       path: '/result/success',
                //       component: './result/success',
                //     },
                //     {
                //       name: 'fail',
                //       icon: 'smile',
                //       path: '/result/fail',
                //       component: './result/fail',
                //     },
                //   ],
                // },
                // {
                //   name: 'exception',
                //   icon: 'warning',
                //   path: '/exception',
                //   routes: [
                //     {
                //       path: '/',
                //       redirect: '/exception/403',
                //     },
                //     {
                //       name: '403',
                //       icon: 'smile',
                //       path: '/exception/403',
                //       component: './exception/403',
                //     },
                //     {
                //       name: '404',
                //       icon: 'smile',
                //       path: '/exception/404',
                //       component: './exception/404',
                //     },
                //     {
                //       name: '500',
                //       icon: 'smile',
                //       path: '/exception/500',
                //       component: './exception/500',
                //     },
                //   ],
                // },
                {
                  name: 'account',
                  icon: 'user',
                  path: '/account',
                  routes: [
                    {
                      path: '/',
                      redirect: '/account/center',
                    },
                    {
                      name: 'center',
                      icon: 'smile',
                      path: '/account/center',
                      component: './account/center',
                    },
                    {
                      name: 'settings',
                      icon: 'smile',
                      path: '/account/settings',
                      component: './account/settings',
                    },
                  ],
                },
                // {
                //   name: 'editor',
                //   icon: 'highlight',
                //   path: '/editor',
                //   routes: [
                //     {
                //       path: '/',
                //       redirect: '/editor/flow',
                //     },
                //     {
                //       name: 'flow',
                //       icon: 'smile',
                //       path: '/editor/flow',
                //       component: './editor/flow',
                //     },
                //     {
                //       name: 'mind',
                //       icon: 'smile',
                //       path: '/editor/mind',
                //       component: './editor/mind',
                //     },
                //     {
                //       name: 'koni',
                //       icon: 'smile',
                //       path: '/editor/koni',
                //       component: './editor/koni',
                //     },
                //   ],
                // },
    
                {
                  component: '404',
                },
              ],
            },
          ],
        },
      ],
      // Theme for antd: https://ant.design/docs/react/customize-theme-cn
      theme: {
        'primary-color': defaultSettings.primaryColor,
      },
      title: false,
      ignoreMomentLocale: true,
      proxy: proxy[REACT_APP_ENV || 'dev'],
      publicPath: '/dist/', //在生成的js路径前,添加这个路径
      manifest: {
        basePath: '/',
      },
    });
    
    
    菜单渲染

    src\layouts\BasicLayout.tsx

    /**
     * Ant Design Pro v4 use `@ant-design/pro-layout` to handle Layout.
     * You can view component api by:
     * https://github.com/ant-design/ant-design-pro-layout
     */
    import type {
      MenuDataItem,
      BasicLayoutProps as ProLayoutProps,
      Settings,
    } from '@ant-design/pro-layout';
    import ProLayout, { DefaultFooter, SettingDrawer } from '@ant-design/pro-layout';
    import React, { useEffect, useMemo, useRef } from 'react';
    import type { Dispatch } from 'umi';
    import { Link, useIntl, connect, history } from 'umi';
    // import { GithubOutlined } from '@ant-design/icons';
    import { Result, Button } from 'antd';
    import Authorized from '@/utils/Authorized';
    import RightContent from '@/components/GlobalHeader/RightContent';
    import type { ConnectState } from '@/models/connect';
    import { getMatchMenu } from '@umijs/route-utils';
    import logo from '../assets/logo.png';
    
    // 导入对应的Icon
    import {
      SmileOutlined,
      CarryOutOutlined,
      FormOutlined,
      UserOutlined,
      HomeOutlined,
      PicLeftOutlined,
      SettingOutlined,
    } from '@ant-design/icons';
    
    // Icon的对应表
    const IconMap = {
      HomeOutlined: <HomeOutlined />,
      CarryOutOutlined: <CarryOutOutlined />,
      smile: <SmileOutlined />,
      PicLeftOutlined: <PicLeftOutlined />,
      SettingOutlined: <SettingOutlined />,
      form: <FormOutlined />,
      user: <UserOutlined />,
    };
    
    // 转化Icon  string --> React.ReactNode
    const loopMenuItem = (menus: MenuDataItem[]): MenuDataItem[] =>
      menus.map(({ icon, children, ...item }) => ({
        ...item,
        icon: icon && IconMap[icon as string],
        children: children && loopMenuItem(children),
      }));
    
    const noMatch = (
      <Result
        status={403}
        title="403"
        subTitle="Sorry, you are not authorized to access this page."
        extra={
          <Button type="primary">
            <Link to="/user/login">Go Login</Link>
          </Button>
        }
      />
    );
    export type BasicLayoutProps = {
      breadcrumbNameMap: Record<string, MenuDataItem>;
      route: ProLayoutProps['route'] & {
        authority: string[];
      };
      settings: Settings;
      dispatch: Dispatch;
      menuData: MenuDataItem[]; // dymanic menu
    } & ProLayoutProps;
    export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & {
      breadcrumbNameMap: Record<string, MenuDataItem>;
    };
    /**
     * use Authorized check all menu item
     */
    
    const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] =>
      menuList.map((item) => {
        const localItem = {
          ...item,
          children: item.children ? menuDataRender(item.children) : undefined,
        };
        return Authorized.check(item.authority, localItem, null) as MenuDataItem;
      });
    
    const defaultFooterDom = (
      <DefaultFooter
        copyright={`${new Date().getFullYear()} CrabShell`}
        links={
          []
        }
      />
    );
    
    const BasicLayout: React.FC<BasicLayoutProps> = (props) => {
      const {
        dispatch,
        children,
        settings,
        location = {
          pathname: '/',
        },
        menuData, // 菜单数据
        loading,
      } = props;
    
      const menuDataRef = useRef<MenuDataItem[]>([]);
      useEffect(() => {
        if (dispatch) {
          dispatch({
            type: 'user/fetchCurrent',
          });
          dispatch({
            type: 'menu/fetchMenu',
          });
        }
      }, []);
      /**
       * init variables
       */
    
      const handleMenuCollapse = (payload: boolean): void => {
        if (dispatch) {
          dispatch({
            type: 'global/changeLayoutCollapsed',
            payload,
          });
        }
      }; // get children authority
    
      const authorized = useMemo(
        () =>
          getMatchMenu(location.pathname || '/', menuDataRef.current).pop() || {
            authority: undefined,
          },
        [location.pathname],
      );
      const { formatMessage } = useIntl();
      return (
        <>
          <ProLayout
            logo={logo}
            formatMessage={formatMessage}
            {...props}
            {...settings}
            onCollapse={handleMenuCollapse}
            onMenuHeaderClick={() => history.push('/')}
            menuItemRender={(menuItemProps, defaultDom) => {
              if (
                menuItemProps.isUrl ||
                !menuItemProps.path ||
                location.pathname === menuItemProps.path
              ) {
                return defaultDom;
              }
    
              return <Link to={menuItemProps.path}>{defaultDom}</Link>;
            }}
            breadcrumbRender={(routers = []) => [
              {
                path: '/',
                breadcrumbName: formatMessage({
                  id: 'menu.home',
                }),
              },
              ...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={() => defaultFooterDom}
            // menuDataRender={menuDataRender}
            // menuDataRender={() => menuData} // menuDataRender属性中传入菜单,这样是不对后台数据做任何处理,直接显示成菜单
            // menuDataRender={() => menuDataRender(menuData)} // menuDataRender传入菜单,是后台返回的数据,经过前端鉴权后的数据。如当前登录身份为user,后台返回的菜单中有一个权限为authority,不经过处理会直接显示,而前端处理一下menuDataRender(menuData)后,这个菜单就不会显示出来。
            menuDataRender={() => menuDataRender(loopMenuItem(menuData))} // 先处理图标,再做前端鉴权后的数据处理
            menu={{
              loading,
            }}
            rightContentRender={() => <RightContent />}
            postMenuData={(menuData) => {
              menuDataRef.current = menuData || [];
              return menuData || [];
            }}
          >
            <Authorized authority={authorized!.authority} noMatch={noMatch}>
              {children}
            </Authorized>
          </ProLayout>
          <SettingDrawer
            settings={settings}
            onSettingChange={(config) =>
              dispatch({
                type: 'settings/changeSetting',
                payload: config,
              })
            }
          />
        </>
      );
    };
    
    export default connect(({ global, settings, menu }: ConnectState) => ({
      collapsed: global.collapsed,
      settings,
      menuData: menu.menuData, // connect连接menu
      loading: menu.loading,
    }))(BasicLayout);
    
    

    尽管这样可以做到从服务器返回的菜单数据,导航栏也是按照后台返回的数据显示。但是用户还是可以通过直接输入链接去打开不显示在菜单栏的页面。

    相关文章

      网友评论

          本文标题:Ant Design Pro V4 -- 后端动态菜单

          本文链接:https://www.haomeiwen.com/subject/ligxxltx.html