每个后台项目都从登录页开始,但让项目变难维护的从来不是登录页。
页面越多,越需要底座
后台系统有个特点:业务页面长得都差不多,但每个项目都会重新发明一遍权限判断、菜单渲染和请求封装。做了几个之后就能看清,真正值得沉淀的是三层底座。
第一层:权限模型
关键是想清楚「权限挂在什么上」。实践中比较稳的做法是三层收敛:
- 接口权限:由后端鉴权,前端只负责在 403 时给出统一提示;
- 路由权限:前端按角色过滤路由表,决定哪些页面可达;
- 操作权限:按钮级的
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,别在每个页面重写正则。
- 列表页一定要有「空状态」和「加载失败」两种形态:这是后台体验最容易被忽略的地方。
小结
底座的价值在于「下一个项目可以直接复用」。判断标准很简单:新开一个后台项目时,需要重写的业务无关代码有多少 —— 越少越好。