Skip to content

中后台管理系统模板记录 ​

更新: 12/19/2025字数: 0 字阅读: 0 分钟

技术栈 ​

安装 ​

bash
npm install

运行 ​

bash
npm start

打包 ​

bash
npm run build

项目架构 ​

js
├── config [配置文件]
│   ├── config.js [配置文件]
│   ├── defaultSettings.js [antd 全局配置]
│   ├── proxy.js [代理配置]
│   ├── routes.js [菜单路由]
│   └── routeHistory.js [路由监听方法]
├── mock [mock 数据]
├── public [公共资源]
├── src [主入口]
│   ├── assets [静态资源]
│   ├── common [公共配置]
│   │   ├── enum.js [枚举]
│   │   ├── pattern.js [正则]
│   │   └── project.js [业务方法]
│   ├── components [公共组件](下方有详细说明)
│   ├── locales [语言设置]
│   ├── models [数据管理]
│   ├── pages [页面路由]
│   ├── services [api 服务]
│   └── utils [工具包]
│       ├── fileRequest.js [文件请求方法封装]
│       ├── form.js [表单处理]
│       ├── format.js [数据格式化]
│       ├── hooks.js [自定义 hook]
│       ├── menu.js [菜单权限相关配置]
│       ├── mgop.js [irs请求方法封装]
│       ├── query.js [地址参数出处理]
│       ├── request.js [请求相关配置(浙里办)]
│       └── utils.js [工具方法]
├── access.js [权限管理]
├── app.jsx [页面初始化,错误处理封装]
├── global.jsx [全局 js]
├── global.less [全局样式]
└── requestErrorConfig.js [数据请求拦截配置]

注意 ​

1、云端 mock 接口文档:https://16vg5t77t6.apifox.cn/api-187779436

2、获取用户信息等一些全局依赖的基础数据放在initialState 全局初始化数据中,初始化于app.jsx中的 getInitialState

3、Layout

  • 配置位于app.jsx

  • 单个页面不需要 layout 时,可在config.js中配置layout:false

  • 也可以使用 v4 方法,在 layout 文件夹中自定义 layout

4、引入第三方 script

由于新版本移除了 document.ejs, 因此当需要引入第三方 js 时,需要在config.js中进行配置headScripts相关内容。

同理,配置 <body> 中额外的 script 标签,使用script

5、umi4 中 props 默认为空对象,若要获取一下属性,可使用usePageProps

js
// utils/hooks.js
/**
 * 获得页面参数
 * @returns {Object} { location, access, params, route, queryParams, history }
 */
export function usePageProps() {
  const location = useLocation();
  const access = useAccess();
  const params = useParams();
  const { route } = useRouteData();
  const queryParams = getPageQuery();
  return {
    location,
    access,
    params,
    route,
    queryParams,
    history,
  };
}

components 通用组件库 ​

目录说明
ApplyItem用于渲染表单项
BaseBreadcrumb面包屑
BaseCascader级联选择器
BaseDatePicker时间组件(区间已经格式化为时间戳)
BaseDebounceSelect防抖选择器
BasePopconfirm气泡确认框组件
BasePwd修改密码表单
Captcha验证码
EmailSearch邮件输入提示框
FileUpload自定义上传组件
FormItemGroup带栅格的表单组,类似 ProForm.Group 功能
MenuTree菜单权限配置组件
Notice消息通知组件
QuillEditer富文本编辑器
TreeMap树级线性结构组件
BaseStepsForm分布表单组件
BaseEditableTable可编辑表格
MapModal通过地址获取经纬度弹窗
BaseTextCollapsen 行文本溢出展开收起组件
BaseCustomIcon自定义 icon 展示,通过 background 修改颜色

一、权限管理 ​

1.1 固定角色的不同菜单 ​

在src/access.js和config/route.js中配置对应的角色菜单权限,如下所述

js
// src/access.js
export default function access(initialState) {
  const { currentUser } = initialState || {};
  return {
    canAdmin: currentUser && currentUser.role === 'admin',
  };
}
js
// config/route.js
export default [
  {
    path: '/welcome',
    component: './Welcome',
    name: 'welcome',
    access: 'canAdmin',
  },
];

1.2 动态菜单 ​

更多配置可看:入口

关于菜单高亮,可以使用 parentKeys 属性,当前推荐还是有父子关系,包在 routes 里

javascript
// config/route.js
export default [
  {
    path: '/product',
    hideInMenu: true,
    name: '产品管理',
  },
  {
    path: '/list/:id',
    hideInMenu: true,
    name: '列表详情',
    parentKeys: ['/product'],
  },
];

routes 可配置项

ts
export interface Setting {
  /**
   * @name false 时不展示顶栏
   */
  headerRender?: false;
  /**
   * @name false 时不展示页脚
   */
  footerRender?: false;
  /**
   * @name false 时不展示菜单
   */
  menuRender?: false;
  /**
   * @name false 时不展示菜单顶栏
   */
  menuHeaderRender?: false;

  /**
   * @name 固定顶栏
   **/
  fixedHeader: boolean;

  /**
   * @name 固定菜单
   */
  fixSiderbar: boolean;

  /**
   * @name theme for nav menu
   * @name 导航菜单的主题
   */
  navTheme: 'dark' | 'light' | 'realDark' | undefined;
  /**
   * @name nav menu position: `side` or `top`
   * @name 导航菜单的位置
   * @description side 为正常模式,top菜单显示在顶部,mix 两种兼有, false 不展示
   */
  layout: 'side' | 'top' | 'mix' | false;
  /**
   * @name 顶部导航的主题,mix 模式生效
   */
  headerTheme: 'dark' | 'light';
}

1.2.1 菜单权限配置 ​

  • utils/menu.js中配置菜单树(包括页面中的各种操作权限)

     注意:

    1、key 必须唯一

    2、若节点非菜单页面,key 的格式为 对应页面的 key.操作类型(基本操作类型已在枚举中定义)

    3、例:用户管理页面(account)的新增按钮的 key 为 account.${ACTION_TYPE.ADD.code}

    js
    export const MENU_TREE = [
      {
        key: 'templates',
        title: '应用模板',
        isShow: (props) => true, // TODO 自定义判断方法
        isDisabled: (props) => false, // TODO 自定义判断方法
        children: [
          {
            key: 'list',
            title: '列表应用',
            isShow: (props) => true,
            isDisabled: (props) => false,
          },
          {
            key: 'form',
            title: '表单应用',
            isShow: (props) => true,
            isDisabled: (props) => false,
          },
        ],
      },
      {
        key: 'account',
        title: '用户管理',
        isShow: (props) => true,
        isDisabled: (props) => false,
        children: [
          {
            key: `account.${ACTION_TYPE.ADD.code}`,
            title: '新增',
            isShow: (props) => true,
            isDisabled: (props) => false,
          },
          {
            key: `account.${ACTION_TYPE.UPDATE.code}`,
            title: '修改',
            isShow: (props) => true,
            isDisabled: (props) => false,
          },
          {
            key: `account.${ACTION_TYPE.DELETE.code}`,
            title: '删除',
            isShow: (props) => true,
            isDisabled: (props) => false,
          },
        ],
      },
    ];
  • 使用菜单权限配置组件@/components/MenuTree给不同的用户配置菜单权限(具体使用可见@/pages/Account)

1.2.2 路由和菜单的权限控制 ​

  • config/route.js中配置与上方utils/menu.js中相对应的 menuKey 和用于权限控制的权限名称

    js
    export default [
      ...{
        path: '/account',
        name: '用户管理',
        icon: 'crown',
        component: './Account',
        menuKey: 'account', // 唯一menuKey配置
        access: 'normalRouteFilter', // 权限名称配置
      },
    ];
  • 在access.js文件中加对应的路由判断方法

    js
    normalRouteFilter: (route) => {
      // 用以判断用户是否拥有路由权限
      return currentUser?.hasRoutesKeys?.includes(route.menuKey);
    };

    如果不需要动态菜单权限控制,可以在 routes 里配置 authority:['admin','user']这样的用户角色,然后 access 方法判断的是 route.authority.includes(user.role)

  • 获取动态菜单数据

    app.jsx文件getInitialState中调接口(一般是用户用户信息接口)获取动态菜单数据并处理

    js
    import { defaultKeys } from '@/utils/menu';
    
    export async function getInitialState() {
      const fetchUserInfo = async () => {
        try {
          const msg = await fetchProfile({
            skipErrorHandler: true,
          });
          const info = msg.data;
          // 获取动态菜单数据
          info.hasRoutesKeys = info?.menuData || defaultKeys;
          return info;
        } catch (error) {
          history.push(loginPath);
        }
        return undefined;
      };
    
      ...
    }

1.2.3 页面内的权限控制 ​

  • 在access.js文件中加对应的权限判断方法

    js
    canOperate: (routeKey, operateKey) => {
      return currentUser?.hasRoutesKeys?.includes(`${routeKey}.${operateKey}`);
    };
  • 通过 useAccess hook 来获取权限定义, Access 组件用于页面的元素显示和隐藏的控制

    jsx
    // import { useAccess, useRouteData } from '@umijs/max';
    import { usePageProps } from '@/utils/hooks';
    
    export default function Account() {
    
      // const access = useAccess();
      // const { route } = useRouteData();
    
      // 页面中常用的获取路由、权限等hook封装在了usePageProps中  可直接使用该hook
      const { route = {}, access = {} } = usePageProps();
      ...
    
      return {
        ...
        // 方法一:直接控制显隐
        <Access accessible={access.canOperate(route.menuKey, ACTION_TYPE.ADD.code)}>
          <Button
            type="primary"
            onClick={() => {...}}
            >
            <PlusOutlined /> 新增用户
          </Button>
        </Access>
    
        // 方法二:事件触发后做提示
        <Button
          type="primary"
          onClick={() => {
            if (access.canOperate(route.menuKey, ACTION_TYPE.ADD.code)) {
              ...
            } else {
              message.error(`暂无${ACTION_TYPE.ADD.desc}权限`)
            }
          }}
          >
          <PlusOutlined /> 新增用户
        </Button>
        ...
      }
    }

1.3 分组菜单 ​

  1. 在 config/defaultSetting 下,menu 的 type 改为 group 分组类型
  2. routes 配置,需要分组的在对应路由下加 group:'groupName'
  {
    path: '/templates',
    name: '应用模板',
    icon: 'StepForwardOutlined',
    key: 'templates',
    access: 'normalRouteFilter',
    group: '应用类型',
    routes: [
      {
        path: '/templates',
        redirect: '/templates/list',
      },
      {
        path: '/templates/list',
        name: '列表应用',
        component: './Templates/List',
        key: 'list',
        access: 'normalRouteFilter',
      },
      {
        path: '*',
        component: './404',
      },
    ],
  },

1.4 子菜单显示 icon ​

在 routes 里进行配置 icon,开启 showIcon: true。

需注意,这里只支持 iconfont 和 antdv4 之后的 icon

  {
    path: '/templates/list',
    name: '列表应用',
    component: './Templates/List',
    icon: 'StepForwardOutlined', // iconfont: icon-xxx
    showIcon: true,
    key: 'list',
    access: 'normalRouteFilter',
  },

1.5 自定义菜单 ​

效果

一级菜单置于顶部展示,鼠标悬浮在一级菜单上展示其子级所有菜单

使用配置

defaultSettings.js

js
 menu: {
    type: 'group', // sub 菜单 / group 分组菜单
    custom: true, // 自定义菜单
  },

routes.js

给对应的菜单配置 group

js
		{
        path: '/templates/list',
        name: '列表应用',
        group: '应用',
        component: './Templates/List',
        menuKey: 'list',
        access: 'normalRouteFilter',
    },
    ...

自定义效果开发

可在 app.jsx 中对MenuItem的 dom 进行修改

二、接口请求 ​

  • src/services文件夹中配置需要的接口方法有一些更改

    js
    export async function fetchOrgList(params, options) {
      return request(`${HOST}/v1/system/organization/list?${stringify(params)}`, {
        method: 'GET',
        ...(options || {}),
      });
    }
    
    export async function fetchOrgSave(params, options) {
      return request(`${HOST}/v1/system/organization/save`, {
        method: 'POST',
        data: params,
        ...(options || {}),
      });
    }

    options 可传skipErrorHandler: true,用于跳过默认的错误处理

  • 接口请求错误处理迁移到src/requestErrorConfig.js,这边非正常业务码都会用异常来捕获

js
 // 响应拦截器
  responseInterceptors: [
    (response) => {
      // 拦截响应数据,进行个性化处理
      const { data: res } = response;
      if (res?.code !== SUCCESS_CODE) {
        throw res;
      }
      return response;
    },
  ],
  • 当数据仅在单页面使用时,可以直接使用 useRequest

    js
    import { useRequest } from '@umijs/max';
    import { changeUsername } from '@/services/api';
    
    export default () => {
      const { run, loading } = useRequest(changeUsername, { manual: true });
    
      return (
        <Button onClick={() => run('new name')} loading={loading}>
          Edit
        </Button>
      );
    };

useRequest 和 useModel 结合的简易数据流 ​

当数据需要持久化时,可以使用该方法

1、src/services文件夹中配置需要的接口

2、model 中配置对应的 useRequest 接口请求,返回的数据 data,loading 及请求方法等放入 model 中(可参考models/tableList)

  • 若不需要处理数据,可直接使用 useRequest 解构出来的 data
  • 若需要处理数据,可以在 onSuccess 进行数据存储,也可以直接通过 formatResult 进行数据处理,数据处理好后 data 就是处理后的数据

需注意,onSuccess 成功回调拿到的直接是 data 里的数据,而 formatResult(成功时调用)拿到的是整个{code, data}的对象

js
import { message } from 'antd';
import { useRequest } from '@umijs/max';
import { fetchRuleList } from '@/services/api';

export default () => {
  const {
    run: fetchTableList,
    data: tableList,
    loading: tableLoading,
  } = useRequest((v) => fetchRuleList(v), {
    manual: true,
    onSuccess: (res) => res,
    onError: (res) => {
      message.error(res?.message || '请求失败');
    },
    // 数据处理
    formatResult: ({ data: res }) => {
      const arr = res?.items.map((item, index) => ({
        ...item,
        enabled: index % 2 === 0,
      }));
      return {
        ...res,
        items: arr,
      };
    },
  });

  return { fetchTableList, tableLoading, tableList };
};

3、使用时用 useModel 来获取 (具体使用可参考src/page/Application/Table)

js
const { fetchTableList, tableLoading, tableList } = useModel('tableList');

4、useModel 提供给第二个参数来做缓存依赖,但是每个都写又会加剧代码工作量,所以在 utils/hooks 里封装了 useMemoModel

/**
 * useModel depths 缓存
 * @param {*} model
 * @param {*} depthsArr 需要返回的参数名,多层级用.分割,如['tableList', 'account.currentUser']
 * @param {*} flattern 多层级是否扁平化
 * @returns
 * @example
 * const {tableList, currentUser = {}} = useMemoModel('user',['tableList','account.currentUser'])
 */
export function useMemoModel(model, depthsArr = [], flattern = true) {
  const props = useModel(model, (m) => {
    const tmp = {};
    depthsArr.forEach((v) => {
      const [k0, ...keys] = v?.split('.');
      if (!keys.length) {
        tmp[k0] = m[k0];
      }
      keys.forEach((k) => {
        if (flattern) {
          tmp[k] = m[k0][k];
          return;
        }
        tmp[k0][k] = m[k0][k];
      });
    });
    return tmp;
  });
  return props;
}

三、路由 ​

3.1 路由跳转 ​

同 umi3 一样,支持 history 方法跳转。由于 umi4 不再支持 props 传递 route props,只提供 hook 来进行调用,所以在 utils/hooks 里封装了 usePageProps

/**
 * 获得页面参数
 * @returns {Object}
 * { location, access: 权限, params: path参数,queryParams: query参数, route: 当前页面路由, history: history跳转方法, navigate: 路由组件跳转 }
 */
export function usePageProps() {
  const location = useLocation();
  const access = useAccess();
  const params = useParams();
  const { route } = useRouteData();
  const navigate = useNavigate();
  const queryParams = getPageQuery();
  return {
    location,
    access,
    params,
    route,
    queryParams,
    history,
    navigate
  };
}

我们可以在页面里进行调用

javascript
// /detail/2?type=1
const Detail = () => {
  const {
    location, // 不带publicPath的地址信息
    access, // 权限
    params,
    route,
    queryParams,
    history,
    navigate,
  } = usePageProps();
  console.log(params); // {id: 2}
  console.log(queryParams); // {type: 1}

  // 页面跳转支持两种
  // push
  history.push('/check');
  navigate('/check');

  // replace
  history.replace('/check');
  navigate('/check', { replace: true });

  // back
  history.goBack();
  navigate(-1);
};

这里推荐使用 navigate 进行跳转,但注意,navigate 只支持在组件内使用。

navigate 特效:

  • 支持相对路径
// 当前路径 /admin/account/list
navigate('../detail') // 等同于 /admin/account/detail
navigate('../../detail') // 等同于 /admin/detail
  • 支持将参数放在 state 里进行传递,且页面刷新并不会影响【路径复制新开窗口,相关 state 无法获得,重要参数请放路径】
navigate('/admin/account/detail', {
    state: {
        type: 2
    }
})

// Detail.jsx
const { location } = usePageProps();
console.log(location.state) // {type: 2}

3.2 面包屑 ​

在 components 里封装了 BaseBreadCrumb 面包屑组件,支持:当前窗口内,点击面包屑某一级菜单,会去匹配路由历史最近的一次地址。

如:从/list?pageSize=20 跳转进详情,点击面包屑返回出来依旧携带参数

// 支持在ProLayout、PageContainer使用
<PageContainer
    breadcrumbRender={({ breadcrumb: { routes } }) => {
        return <BaseBreadcrumb breadcrumb={routes} />;
      }}
/>

四、多页签 ​

功能效果 ​

1、基本的多页签展示及切换功能

2、页签切换保留位置与数据

3、刷新浏览器,页签依旧保留

4、可配置项(关闭页签/刷新浏览器时的提示、是否可关闭页签、是否打开新页签)

5、右键菜单(关闭选中标签、关闭右侧标签页、关闭其他标签页)

使用(开启多页签模式) ​

defaultSettings.js 配置

js
{
  ...,
  multipleTabs: true, // 开启多页签模式
}

routes.js 配置

js
{
  ...
  {
    path: '/templates/step-form',
    name: '分步表单',
    // group: '应用',
    component: './Templates/StepForm',
    menuKey: 'step',
    access: 'normalRouteFilter',
    // 多页签相关配置
    tabProps: {
      closeTip: true, // 关闭页签/刷新浏览器时的提示,默认为false
    },
  },
  ...
}

tabProps 配置 ​

参数说明默认值
closeTip关闭页签/刷新浏览器时的提示false
openNew页面是否在新页签打开。例:首次登录时需要修改初始密码,则从首页跳转到修改初始密码,而不是打开新页签,需要配置 openNew 为 false。true
closable页签是否可关闭true
skipNew跳转其他页面时是否打开新页签。例:完成首次登录初始密码修改后,在当前页签跳转到首页,而不是打开新页签,配置 skipNew 为 false。true

开发注意事项 ​

1、在同一个页签跳转详情页的路由配置

当业务需要页面都在同一个页签展示时,注意路由配置需要存在包含关系

正确示例:/system/role 和 /system/role/add(存在包含关系,页面跳转处于同一页签)

错误示例:/system/role/list 和 /system/role/add(不存在包含关系,跳转时打开新页签)

2、二级页返回上一页

例:从列表页打开详情页后,此时记录的上一页为列表页。此时多次切换页签再回到详情页时,记录的上一页则变成了最后一次切换到详情页前的页面路由,而非列表页,此时返回页面则会出现逻辑错误。

解决:当需要返回上一页时,将history.go(-1)修改成goBack()

引入及使用:

js
import { useGoBack } from '@/utils/hooks';

const { goBack } = useGoBack(); // 引入自定义hooks中的返回上一页方法

goBack(); // 解决因多页签切换导致返回的上一页面不正确的问题

3、给 PageContainer 中配置了 footer 的页面加配置,用于解决打开有 footer 的页面后,切换到其他页签,footer 依然存在的问题

js
footerToolBarProps={{ portalDom: false }}

4、在当前页面自动关闭当前页签

js
import { useModel } from '@umijs/max';

const { closeTab } = useModel('tabs');

closeTab(); // 关闭当前页签