前置知识: Vue 3

Vue3 管理后台:Element Plus 与中后台工程模式

9 min中级

把中后台当成 Vue 工程能力密度最高的练习场:布局与菜单同源的路由心智模型、Element Plus 按需引入、CRUD 四件套页面模板、权限三层防线(路由级/按钮级/接口级)、文件流导出与 Pinia 状态组织,并说明为什么不要整包复制 vue-element-admin。

前置知识

没读过也可以,本文会带上需要的部分:第 2 节从「布局由什么组成」讲起,第 4 节的页面模板不需要任何组件库前置经验。

学习目标

读完本文你将能够:

  1. 说出管理后台为什么是「Vue 工程能力密度最高」的应用形态,并列出它比 C 端页面多出的五类关切;
  2. 用「路由与菜单同源」的心智模型组织中后台骨架,解释两份手写必然漂移的原因;
  3. 为 Element Plus 选择正确的引入方式,说出全量引入在首屏上的代价;
  4. 把任何 CRUD 页面抽象成「四件套」模板,并说出每个部件的状态归属;
  5. 落实权限三层防线,并解释为什么接口级权限才是真闸门;
  6. 实现文件流导出与用户状态持久化,避开 Blob 与刷新恢复的两个常见坑。

预计 30 到 45 分钟,含 2 个动手实验与 3 道练习。

1. 问题引入:为什么每个前端都躲不开管理后台

C 端页面比拼的是体验与创意:一个博客、一个电商首页,样式千姿百态。管理后台反过来:形态高度收敛,工程密度极高。几乎所有中后台都在重复同一批关切——

  1. 布局:侧边菜单、顶栏、标签页、内容区的组合与折叠;
  2. 权限:谁能看到哪个菜单、哪个按钮、调哪个接口;
  3. 表格与表单:列表查询、分页排序、弹窗编辑,占一个后台 70% 的页面;
  4. 数据流转:导出 Excel、上传附件、批量操作;
  5. 状态组织:登录态、用户信息、菜单数据在刷新后不丢。

这五类关切没有一件炫技,但每一件都能写崩——「能跑通一个后台」考察的是工程组织能力而非组件 API 记忆。竞赛里「应用软件系统开发」类项目的后台模块,考察的正是这五件:它们构成了可复用的能力,从学生项目到公司业务系统用的是同一套模式。

2. 布局骨架:路由与菜单同源

一个典型后台的视觉结构几乎是固定的:

text
+------+------------------------------------------+
| 侧边  |  顶栏(面包屑/用户/语言)                     |
| 菜单  +------------------------------------------+
|      |  标签页(访问过的页面)                       |
|      +------------------------------------------+
|      |                                          |
|      |  主内容区 <router-view />                  |
+------+------------------------------------------+

新手最常见的实现是菜单一份配置、路由一份配置,两处各写各的。这个做法有一个必炸的结构性缺陷:同一个「系统有哪些页面」的事实被写了两遍——加一个页面要改两处,漏一处就是「路由能访问但菜单看不到」或反过来。正确的心智模型是同源:路由配置是唯一事实,菜单由它派生。

typescript
// 路由元信息携带菜单所需的一切(伪代码骨架)
const routes = [
  {
    path: '/system',
    meta: { title: '系统管理', icon: 'Setting' },   // 菜单分组
    children: [
      { path: 'user',  meta: { title: '用户管理', requiresAuth: true } },
      { path: 'role',  meta: { title: '角色管理', requiresAuth: true } },
    ],
  },
];
// 菜单组件遍历 routes 渲染,不再有第二份菜单配置

派生方向是单向的:路由 → 菜单 → 面包屑 → 标签页,全都从同一份配置读 meta。需要「隐藏页面」(详情页不进菜单)就加一个 meta.hidden 标记,而不是把页面写进另一份清单。

3. Element Plus:选对引入方式

Element Plus 是 Vue3 生态最主流的中后台组件库(表格、表单、弹窗、日期选择开箱即用),也是竞赛技术清单的常客。它有两种引入方式,差别直接落在首屏体积上:

方式写法代价
全量引入app.use(ElementPlus) 一行打进整个组件库,首屏多几百 KB
按需引入unplugin 系插件自动按模板引入只打用到的组件,插件配置一次

中后台几乎总选按需引入:装 unplugin-vue-components 与 unplugin-auto-import 两个插件,模板里直接写 <el-table>,插件自动完成 import 与样式注册。这与 组件库工程化里讲的原则同源:组件库的价值在交互密度高的场景,但体积纪律不因为场景而豁免。

一个工程判断要提前想清楚:组件库解决「通用控件」,解决不了「业务部件」。带搜索栏、操作列、状态标签的业务表格仍然要你自己封装——这正是下一节的内容。

4. CRUD 四件套:一个页面模板走天下

抽掉业务词,后台 70% 的页面是同一个形状:

搜索表单

查询条件,受控提交

数据表格

列表渲染,操作列

分页器

页码/每页条数

弹窗表单

新增/编辑,校验后提交

搜索表单

查询条件,受控提交

数据表格

列表渲染,操作列

分页器

页码/每页条数

弹窗表单

新增/编辑,校验后提交

mermaid diagram

这四个部件的状态归属一旦定错,页面就会失控。纪律是查询条件与列表数据归页面(或 composable),表单草稿归弹窗组件:

typescript
// use-crud.ts:四件套的状态中枢(骨架)
export function useCrud(fetchPage: (q: Query) => Promise<Page<T>>) {
  const query = reactive({ page: 1, size: 10, keyword: '' });
  const rows = ref<T[]>([]);
  const total = ref(0);
  const loading = ref(false);

  async function load() {
    loading.value = true;
    try { ({ list: rows.value, total: total.value } = await fetchPage(query)); }
    finally { loading.value = false; }
  }
  function search() { query.page = 1; load(); }      // 新查询回到第一页
  function remove(id: number) { /* 调接口后 load() */ }
  return { query, rows, total, loading, load, search };
}

弹窗里的新增/编辑表单是独立的草稿状态:打开时从行数据拷贝一份,点保存才提交——永远不要让表格行对象直接双向绑定进表单,否则用户点「取消」后表格里的数据已经改了(经典 bug,面试常考)。把四件套固化成一个页面模板后,新页面的成本从「写一天」降到「改字段十分钟」。

动手实验一:基于上面的 useCrud 骨架,写一个「用户管理」页面:搜索框 + 表格 + 分页 + 新增弹窗。数据可以用本地假数组模拟分页。做完把模板存进你的工具箱——它会被用很多年。

5. 权限三层防线

权限是后台的安全底线,正确的结构是三层各管一段:

  1. 路由级:无权访问的页面不注册路由(动态路由按角色过滤后 addRoute),配合全局守卫兜底跳转 403——它管的是「进不进得来」;
  2. 按钮级:页面内「编辑/删除」这类操作按钮,用自定义指令 v-permission 按权限码显隐——它管的是「看得见什么操作」;
  3. 接口级:后端校验每个请求的权限——它管的是「这条数据真的能不能动」。
typescript
// v-permission 指令(骨架)
app.directive('permission', {
  mounted(el: HTMLElement, binding) {
    const codes: string[] = useUserStore().permissionCodes;
    if (!codes.includes(binding.value)) el.remove();   // 无权直接移除节点
  },
});

三层的权重不对等:前两层是体验优化,第三层才是安全边界。前端的一切权限控制都可以被绕过(打开控制台直接调接口),所以「按钮藏起来了所以接口安全」是危险错觉——接口级校验必须由后端独立完成,前端做的一切都是为了让用户界面「少见到他做不到的事」。这个认知在答辩和面试里都是高频考点。

6. 导出、状态与刷新恢复

文件流导出:后台高频需求「导出 Excel」的正确姿势是后端生成文件、前端下载二进制流——不要在前端用 JS 拼大 Excel(内存与格式都是坑)。两个易错点:

typescript
// 1) axios 必须声明 responseType,否则二进制被当文本解析,文件必然损坏
const res = await http.get('/export/users', { params: query, responseType: 'blob' });
// 2) 从响应头取文件名时注意 RFC 5987 编码的中文文件名
const url = URL.createObjectURL(res.data);
const a = document.createElement('a');
a.href = url; a.download = decodeFileName(res.headers['content-disposition']);
a.click(); URL.revokeObjectURL(url);   // 用完释放,长驻后台页面尤其要记得

Pinia 组织用户态:登录后拿到的 token、用户信息、权限码放在一个 store;token 持久化到 localStorage(配合请求层 401 拦截登出),权限码放内存即可(刷新后随用户信息重新拉取)——token 丢了要重新登录是合理的,权限码丢了不该让用户重新登录。

刷新恢复:F5 之后 Vue 应用整个重建。标签页、展开的菜单这类「界面状态」要么不恢复(合理默认),要么自己存 sessionStorage;业务查询条件一般不持久化(避免用户忘记自己在看过滤后的数据)。恢复策略想清楚「丢了会不会造成事故」,而不是所有状态都无脑持久化。

7. 动手实验二与常见困惑

动手实验二:在实验一的模板上加两件事:v-permission 指令(权限码用假数组模拟)控制「删除」按钮显隐;一个导出按钮走文件流下载(后端可以用任何语言起一个返回固定二进制的接口,或先用 Blob 模拟)。两个实验合起来,你就拥有了后台开发最核心的肌肉记忆。

Q:要不要直接用 vue-element-admin 起项目? A:读它的目录结构(路由如何组织、权限如何分层)很有教学价值;但 2026 年不建议整包复制起项目——那套模板带着大量你用不到的演示页与旧依赖,删干净的成本比新建高。正确姿势:新建 Vue3 + Vite + Element Plus 项目,按第 2-4 节的模式自己搭骨架,卡住时再去读 vue-element-admin 对应部分的实现。

Q:菜单与路由「同源」后,后端下发的权限菜单怎么接? A:把「路由配置表」拆成两段:前端持有全部页面的路由映射(path 到组件),登录后请求用户的菜单权限,按权限从映射表里筛选出可注册的路由再 addRoute。前端管「页面长什么样」,后端管「你能看哪些」——职责边界与第 5 节的三层防线一致。

Q:表格很大很卡怎么办? A:先确认分页真的在工作(别把全量数据渲染进表格);确实需要长列表时上虚拟滚动(Element Plus 的表格虚拟化或独立方案)。与 性能实践的原则相同:中后台的性能问题九成出在「一次渲染太多 DOM」,而不是 Vue 本身。

8. 自检清单

  1. 解释「路由与菜单同源」,并说出两份配置各改一半时出现的两种故障;
  2. 说出 CRUD 四件套各部件的状态归属,以及「取消按钮 bug」的根因;
  3. 默写权限三层防线,并解释为什么接口级不可省略;
  4. 说出文件流导出的两个必踩坑(responseType 与文件名编码);
  5. 你的后台要加「登录后按角色显示不同菜单」,列出改动点(后端菜单接口、路由筛选、菜单组件三处)。

学完本篇,你已经具备搭起完整中后台骨架的能力。下一步可以把 AI 能力接进后台——用 Vue3 流式 AI 对话组件给系统加一个智能助手面板,或回到 Vue3 项目实战对照 C 端项目的组织差异。