ant design pro + umi4的动态菜单与动态路由

文章讲述了作者在umi4框架中遇到的自定义菜单和路由配置问题。最初尝试从umi3迁移方案,发现子路由在BasicLayout中不显示。然后尝试使用patchRoutes未成功,最终在ProComponents的高级用法中找到解决方案,即直接在app.tsx中配置菜单。同时,文中还提到了删除无子路由的菜单节点的方法和修改默认路由跳转的技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

困扰我好多天的自定义菜单及路由终于完成了,首先虽然看似为一个功能,但在umi4中是两项配置。

先说说我浪费了大量时间使用的处理方式:因为曾经的项目是umi3的,所以我就想原样搬过来,结果发现,布局页BasicLayout的子children不见了!

如果这个子路由能正常显示,那引个ProLayout也就不用大费周章找其他方法。参考:https://blog.youkuaiyun.com/eisha2015/article/details/114831390

后来,我又找到了umi的patchRoutes,但发现怎么重写路由,菜单都没有变化。参考:https://blog.youkuaiyun.com/weixin_43294560/article/details/107447241

最后还是翻到ProComponents菜单的高级用法,才发现菜单直接在app.tsx里配置就行了,相当于这个文件就是当初的ProLayout。

之后还将子路由routes为[]时,把父结点也清除的数据处理,以及使用patchClientRoutes来修改默认路由跳转,而原本routes.ts配置的路转就可以删除了。

所以修改的代码如下:

type Router = {
  name?: string,
  path: string,
  icon?: string | React.ReactNode,
  routes?: Router[],
  component?: string,
  redirect?: string,
}

// routes为[],则删除结点
const deleteroutes = (arr: Router[]) => {
  let childs = arr
  for (let i = childs.length; i--; i > 0) {
    const routes = childs[i].routes;
    if (routes) {
      if (routes.length) {
        // 处理antd的图标,全部引入包的大小爆增,不如用iconfontUrl
        // const icon = childs[i].icon;
        // if (typeof icon === 'string') {
        //   childs[i].icon = React.createElement(Icons[icon]);
        // }
        deleteroutes(routes);
      } else {
        delete childs[i];
      }
    }
  }
  return JSON.parse(JSON.stringify(arr).replace(/null,/g, '').replace(/,null/g, '')) as Router[];
}

let extraRoutes: Router[];
export const patchClientRoutes = ({ routes }: any) => {
  const r = extraRoutes;
  const to = r![0].routes![0].routes![0].path;
  routes.unshift({
    path: '/',
    element: <Navigate to={to || ''} replace />,
  });
  r.forEach(element => {
    routes.unshift({
      path: element.path,
      element: <Navigate to={element.routes![0].routes![0].path} replace />,
    });
  });
};

// 示例方法
export function render(oldRender: any) {
  setTimeout(() => {
    extraRoutes = deleteroutes([{
      name: '一级菜单A',
      path: '/a',
      routes: [
        {
          name: '二级菜单AA',
          path: '/a/aa',
          icon: 'icon1',
          routes: [
          ],
        },
        {
          name: '二级菜单BB',
          path: '/a/bb',
          icon: 'icon2',
          routes: [
            {
              name: '三级菜单AAA',
              path: '/a/aa/aaa',
              component: './a/aa/aaa',
            },
            {
              name: '三级菜单BBB',
              path: '/a/aa/bbb',
              component: './a/aa/bbb',
            },
          ],
        },
      ]
    },
    {
      name: '一级菜单B',
      path: '/b',
      routes: [
      ],
    }]);
    oldRender();
  }, 1000);
}


export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {
  return {
    menu: {
      // 每当 initialState?.currentUser?.userid 发生修改时重新执行 request
      // params: {
      //   userId: initialState?.currentUser?.userid,
      // },
      request: async (params, defaultMenuData) => {
        return extraRoutes;
      }
    },
    .......
  };
};
import Footer from '@/components/Footer';
import { UserOutlined } from '@ant-design/icons';
import type { Settings as LayoutSettings } from '@ant-design/pro-components';
import { Navigate, RunTimeLayoutConfig } from '@umijs/max';
import { history, Link } from '@umijs/max';
import defaultSettings from '../config/defaultSettings';
import { AvatarDropdown, AvatarName } from './components/RightContent/AvatarDropdown';
import { errorConfig } from './requestErrorConfig';
import React from 'react';
import { initEnums } from './services/api/global';
import { CurrentUser, fetchUser } from './services/api/login';
import { jumpLogin } from './tools';
import { fetchMenuTree } from './services/api/baseSetting/advanced/menu';
import { stamp } from './services';
import { Notice } from './components/RightContent/Notice';
const isDev = process.env.NODE_ENV === 'development';
const loginPath = '/user/login';

type Router = {
  name?: string,
  path: string,
  icon?: string | React.ReactNode,
  children?: Router[],
  component?: string,
  redirect?: string,
}

// routes为[],则删除结点
const deleteroutes = (arr: Router[]) => {
  let childs = arr
  for (let i = childs.length; i--; i > 0) {
    const routes = childs[i].children;
    if (routes) {
      if (routes.length) {
        // 处理antd的图标,全部引入包直接增加多1MB,不如用iconfontUrl
        // const icon = childs[i].icon;
        // if (typeof icon === 'string') {
        //   childs[i].icon = React.createElement(Icons[icon]);
        // }
        deleteroutes(routes);
      } else {
        delete childs[i];
      }
    }
  }
  return JSON.parse(JSON.stringify(arr).replace(/\[null,/g, '[').replace(/,null\]/g, ']')) as Router[];
}

let extraRoutes: Router[];

export const patchClientRoutes = ({ routes }: any) => {
  if (localStorage.getItem('admin-token') && extraRoutes) {
    const to = extraRoutes![0].children![0].children![0].path;
    routes.unshift({
      path: '/',
      element: <Navigate to={to} replace />,
    });
    extraRoutes.forEach(element => {
      routes.unshift({
        path: element.path,
        element: <Navigate to={element.children![0].children![0].path} replace />,
      });
    });
  }
};

export function render(oldRender: any) {
  if (localStorage.getItem('admin-token')) {
    fetchMenuTree().then((res) => {
      if (res.data) {
        const { menuTree } = res.data;
        menuTree.forEach(m => {
          m.children?.forEach(c => {
            c.children?.forEach(l => {
              l.path = `${l.path}?version=${stamp}`
            })
          })
        })
        extraRoutes = deleteroutes(res.data.menuTree);
        oldRender();
      } else {
        if (res.code !== 952) {
          jumpLogin();
        } else {
          extraRoutes = [{
            path: '',
            children: [{
              path: '',
              children: [{
                path: '/access',
              }]
            }],
          }];
        }
        oldRender();
      }
    });
  } else {
    oldRender();
  }
}

/**
 * @see  https://umijs.org/zh-CN/plugins/plugin-initial-state
 * */
export async function getInitialState(): Promise<{
  settings?: Partial<LayoutSettings>;
  currentUser?: CurrentUser;
  loading?: boolean;
  fetchUserInfo?: () => Promise<CurrentUser | undefined>;
}> {
  const fetchUserInfo = async () => {
    try {
      const msg = await fetchUser();
      if (msg.code === 952) {
        if (window.location.pathname !== '/access') history.push('/access');
      } else {
        return msg.data.user;
      }
    } catch (error) {
      console.error(error)
      jumpLogin();
    }
    return undefined;
  };
  // 如果不是登录页面,执行
  const { location } = history;
  if (location.pathname !== loginPath) {
    // 请求用户信息
    const currentUser = await fetchUserInfo();
    window.auths = currentUser?.authorityValues || [];
    // 请求枚举值
    await initEnums();
    return {
      fetchUserInfo,
      currentUser,
      settings: defaultSettings as Partial<LayoutSettings>,
    };
  }
  return {
    fetchUserInfo,
    settings: defaultSettings as Partial<LayoutSettings>,
  };
}

// ProLayout 支持的api https://procomponents.ant.design/components/layout
export const layout: RunTimeLayoutConfig = ({ initialState }) => {
  return {
    actionsRender: () => [<Notice key="notice" />],
    avatarProps: {
      icon: <UserOutlined />,
      title: <AvatarName />,
      style: { background: '#84B64A' },
      render: (_, avatarChildren) => {
        return <AvatarDropdown menu={true}>{avatarChildren}</AvatarDropdown>;
      },
    },
    waterMarkProps: isDev ? {
      content: initialState?.currentUser?.fullName,
    } : undefined,
    footerRender: () => <Footer />,
    onPageChange: () => {
      const { location } = history;
      // 如果没有登录,重定向到 login
      if (!initialState?.currentUser && location.pathname !== loginPath && location.pathname !== '/access') {
        jumpLogin();
      }
    },
    layoutBgImgList: [
      {
        src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/D2LWSqNny4sAAAAAAAAAAAAAFl94AQBr',
        left: 85,
        bottom: 100,
        height: '303px',
      },
      {
        src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/C2TWRpJpiC0AAAAAAAAAAAAAFl94AQBr',
        bottom: -68,
        right: -45,
        height: '303px',
      },
      {
        src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/F6vSTbj8KpYAAAAAAAAAAAAAFl94AQBr',
        bottom: 0,
        left: 0,
        width: '331px',
      },
    ],
    links: isDev ? [] : [],
    menu: {
      // 每当 initialState?.currentUser?.userid 发生修改时重新执行 request
      // params: {
      //   userId: initialState?.currentUser?.userid,
      // },
      request: async () => {
        return extraRoutes;
      }
    },
    menuHeaderRender: undefined,
    // 自定义 403 页面
    // unAccessible: <div>unAccessible</div>,
    // 增加一个 loading 的状态
    childrenRender: (children) => {
      // if (initialState?.loading) return <PageLoading />;
      return (
        <>
          {children}
        </>
      );
    },
    ...initialState?.settings,
    token: {
      sider: {
        colorBgMenuItemSelected: '#e2f9c7',
        colorTextMenuSelected: '#84B64A',
        colorTextMenuItemHover: '#84B64A',
        colorTextMenuActive: '#84B64A',
      },
      header: {
        colorBgHeader: '#292f33',
        colorHeaderTitle: '#fff',
        colorTextMenu: '#dfdfdf',
        colorTextMenuSelected: '#84B64A !important',
        colorTextMenuActive: '#84B64A',
        colorBgMenuItemSelected: '#22272b',
      },
    }
  };
};

/**
 * @name request 配置,可以配置错误处理
 * 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。
 * @doc https://umijs.org/docs/max/request#配置
 */
export const request = {
  ...errorConfig,
};

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值