技术

B 端后台底座:把权限、菜单、请求收敛成三层

后台系统做多了会发现,真正需要沉淀的不是页面,而是权限模型、动态菜单和请求层这三块底座。

目录
  1. 页面越多,越需要底座
  2. 第一层:权限模型
  3. 第二层:动态菜单
  4. 第三层:请求层
  5. 目录切分
  6. 几条踩过的经验
  7. 小结

每个后台项目都从登录页开始,但让项目变难维护的从来不是登录页。

页面越多,越需要底座

后台系统有个特点:业务页面长得都差不多,但每个项目都会重新发明一遍权限判断、菜单渲染和请求封装。做了几个之后就能看清,真正值得沉淀的是三层底座。

第一层:权限模型

关键是想清楚「权限挂在什么上」。实践中比较稳的做法是三层收敛:

  1. 接口权限:由后端鉴权,前端只负责在 403 时给出统一提示;
  2. 路由权限:前端按角色过滤路由表,决定哪些页面可达;
  3. 操作权限:按钮级的 v-permission 指令,只做展示层收敛,不作为安全边界。
// 路由级权限:用一个纯函数过滤,便于单测
export function filterRoutes(routes: RouteRecord[], role: string): RouteRecord[] {
  return routes.filter((route) => {
    if (!route.meta?.roles) return true
    return route.meta.roles.includes(role)
  })
}

需要强调:前端权限只是体验层收敛,安全边界永远在后端。

第二层:动态菜单

菜单不要手写,让它从路由表里长出来:

字段含义说明
meta.title菜单文案同时用于面包屑
meta.icon图标名统一走图标组件
meta.hidden是否隐藏详情页不进菜单
meta.keepAlive是否缓存列表页常用

这样新增页面时只改一个地方,菜单、面包屑、标签页会同步跟上。

第三层:请求层

请求层的目标只有两个:错误处理统一、业务代码干净。

const http = axios.create({ timeout: 15000 })

http.interceptors.response.use(
  (res) => {
    const body = res.data
    if (body.code !== 0) {
      toast(body.message || '请求失败')
      return Promise.reject(new Error(body.message))
    }
    return body.data
  },
  (err) => {
    const status = err.response?.status
    if (status === 401) redirectToLogin()
    else if (status === 403) toast('没有操作权限')
    else toast('网络异常,请稍后重试')
    return Promise.reject(err)
  },
)

拦截器里只做「翻译」,把各种失败翻译成用户能懂的一句话;业务层拿到的永远是成功数据。

目录切分

src/
├─ api/          # 按业务域拆分的接口定义
├─ components/   # 通用组件(表格、表单、上传)
├─ layouts/      # 布局骨架
├─ router/       # 静态路由 + 权限过滤
├─ stores/       # 用户、权限、字典
├─ directives/   # v-permission 等
└─ views/        # 业务页面

几条踩过的经验

  • 别把字典表塞进组件:字典是全局缓存,用 store 管,页面里只读。
  • 表格分页参数规范化:page / pageSize / total 三个字段统一命名,后端不一致就在请求层适配,不要污染页面。
  • 表单校验复用:把常用的手机号、身份证等规则抽成 composable,别在每个页面重写正则。
  • 列表页一定要有「空状态」和「加载失败」两种形态:这是后台体验最容易被忽略的地方。

小结

底座的价值在于「下一个项目可以直接复用」。判断标准很简单:新开一个后台项目时,需要重写的业务无关代码有多少 —— 越少越好。