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

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;
  };
}

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;
  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 {
  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: ,
  CarryOutOutlined: ,
  smile: ,
  PicLeftOutlined: ,
  SettingOutlined: ,
  form: ,
  user: ,
};

// 转化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 = (
  
        Go Login
      
    }
  />
);
export type BasicLayoutProps = {
  breadcrumbNameMap: Record;
  route: ProLayoutProps['route'] & {
    authority: string[];
  };
  settings: Settings;
  dispatch: Dispatch;
  menuData: MenuDataItem[]; // dymanic menu
} & ProLayoutProps;
export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & {
  breadcrumbNameMap: Record;
};
/**
 * 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 = (
  
);

const BasicLayout: React.FC = (props) => {
  const {
    dispatch,
    children,
    settings,
    location = {
      pathname: '/',
    },
    menuData, // 菜单数据
    loading,
  } = props;

  const menuDataRef = useRef([]);
  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 (
    <>
       history.push('/')}
        menuItemRender={(menuItemProps, defaultDom) => {
          if (
            menuItemProps.isUrl ||
            !menuItemProps.path ||
            location.pathname === menuItemProps.path
          ) {
            return defaultDom;
          }

          return {defaultDom};
        }}
        breadcrumbRender={(routers = []) => [
          {
            path: '/',
            breadcrumbName: formatMessage({
              id: 'menu.home',
            }),
          },
          ...routers,
        ]}
        itemRender={(route, params, routes, paths) => {
          const first = routes.indexOf(route) === 0;
          return first ? (
            {route.breadcrumbName}
          ) : (
            {route.breadcrumbName}
          );
        }}
        footerRender={() => defaultFooterDom}
        // menuDataRender={menuDataRender}
        // menuDataRender={() => menuData} // menuDataRender属性中传入菜单,这样是不对后台数据做任何处理,直接显示成菜单
        // menuDataRender={() => menuDataRender(menuData)} // menuDataRender传入菜单,是后台返回的数据,经过前端鉴权后的数据。如当前登录身份为user,后台返回的菜单中有一个权限为authority,不经过处理会直接显示,而前端处理一下menuDataRender(menuData)后,这个菜单就不会显示出来。
        menuDataRender={() => menuDataRender(loopMenuItem(menuData))} // 先处理图标,再做前端鉴权后的数据处理
        menu={{
          loading,
        }}
        rightContentRender={() => }
        postMenuData={(menuData) => {
          menuDataRef.current = menuData || [];
          return menuData || [];
        }}
      >
        
          {children}
        
      
      
          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 -- 后端动态菜单)