一个后台页面,功能都做完了:筛选能查,表格能翻页,弹窗能保存。
但你盯着它看,总觉得差点意思。
查询是主按钮,新增是主按钮,每一行又摆着三四个蓝色操作;姓名、账号、状态、时间一样醒目;筛选一个框,表格一个框,外面再套一个框。
单独看,每个组件都没问题。放在一起,页面却没有重点。
这种"模板味",往往就出在组件之间的关系还没有被设计。
这篇只拿一个用户管理页动手:保留 Element Plus,保留业务信息,把布局、表格和主题接起来。
先看同一张页面的两种写法
基础组合:

调整以后:

两张图使用同一组数据,也都有姓名、账号、角色、状态和创建时间。查询、新增、编辑、分配角色、重置密码这些入口也保留了。
变化集中在四件事:
- 新增回到页头,查询留在筛选区。
- 姓名与账号合并为"用户",同一格内分出主次。
- 高频操作直接显示,低频操作收进"更多"。
- 用间距和轻量分隔建立层级,减少完整网格的存在感。
这组图是为本文制作的 Vue 3 + Element Plus 教学演示,使用虚构数据,并非某个项目改版前后的历史截图。示例中的操作频率是假设,实际项目需要根据用户工作流程决定。
接下来拆开看,这几步分别怎么落到代码里。
一、先把按钮放对位置,再考虑它是什么颜色
把查询、重置、新增、导出放进同一排,开发起来很顺手。但它们并不属于同一件事。
查询和重置控制的是当前列表的筛选条件 ;新增是在创建一个新的业务对象。位置相邻,却不意味着职责相同。
可以先整理成这样的骨架:
vue
<section class="user-page">
<header class="user-page__header">
<div>
<h1>用户管理</h1>
<p>管理团队账号、关联角色与访问状态</p>
</div>
<el-button type="primary" @click="openCreate">新增用户</el-button>
</header>
<form class="user-page__filters" @submit.prevent="search">
<!-- 带明确标签的筛选字段 -->
<el-button native-type="submit">查询</el-button>
<el-button @click="reset">重置</el-button>
</form>
<el-table class="admin-table" :data="users">
<!-- 业务列 -->
</el-table>
<footer class="user-page__pagination">
<!-- 分页 -->
</footer>
</section>
这里省略了业务逻辑,只看页面结构:进入页面先知道自己在哪里,接着可以新增,也可以缩小列表范围,最后处理具体的一行。
在这个示例里,"新增"是页面级主操作,所以保留实心主按钮。"查询"紧挨着筛选字段,位置已经表达了用途,可以降低颜色权重。
这不是"每个页面只能有一个主按钮"的硬规则。如果你的页面是交易流水查询,用户大部分时间都在反复查数据,查询完全可以成为最突出的操作。
先确认用户进来最常做什么,才能决定谁该抢眼。
同样,行内操作也不能一律藏起来。这里假设编辑最常用,分配角色、重置密码相对低频,才把后两项放进"更多"。如果客服每天都在重置密码,把它收进去只会增加操作次数。
二、表格的质感,先从"哪些字段应该一起读"开始
数据库里有几列,不代表界面就必须有几列。
姓名和账号通常是在共同回答"这是谁"。把它们分开,用户要来回扫两列;合在一起,姓名负责识别,账号负责消除重名歧义。
vue
<el-table-column label="用户" min-width="220">
<template #default="{ row }">
<div class="user-identity">
<strong>{{ row.displayName || row.username }}</strong>
<span>{{ row.username }}</span>
</div>
</template>
</el-table-column>
css
.user-identity {
display: flex;
min-width: 0;
flex-direction: column;
gap: 3px;
}
.user-identity strong {
color: var(--text-primary);
font-size: 14px;
font-weight: 600;
}
.user-identity span {
color: var(--text-secondary);
font-size: 12px;
}
同样的信息,现在有了阅读顺序。没有加插画,也不需要给每个人配一个随机颜色的头像。
但合并也有边界:如果用户经常单独按账号排序、批量复制账号,独立列可能更合适。视觉上更紧凑,不应牺牲实际任务。
另外两个容易见效的小调整:
让状态保留文字。"启用""停用"即使加了颜色,也要能直接读出来。颜色辅助扫描,文字负责明确含义。不要为了整齐,把所有状态都刷成品牌色。
**按数据类型决定对齐。**金额通常右对齐,更容易比较位数;姓名和描述左对齐,方便连续阅读;时间可以使用 font-variant-numeric: tabular-nums,让支持等宽数字的字体保持数字宽度一致。不要为了看起来规整,把所有列都居中。
三、边框、斑马纹和留白,都应该帮助读数据
border、stripe 都有适用场景。宽表格里跨很多列比对数据,清晰的网格或斑马纹可能很有用。
但只有几个字段的用户列表,如果同时出现卡片阴影、外边框、纵向网格、斑马纹,再给每个状态套一个标签,装饰本身就会占掉不少注意力。
本文这张表可以只保留横向分隔与 Hover 反馈,把纵向关系交给对齐和间距。
Element Plus 表格提供了可覆盖的 CSS 变量。下面放在全局样式文件 ,并通过 .admin-table 限定到采用这套规范的表格:Table 文档。
css
.admin-table {
--el-table-header-bg-color: var(--surface);
--el-table-tr-bg-color: var(--surface);
--el-table-row-hover-bg-color: var(--surface-hover);
--el-table-border-color: var(--line);
--el-table-text-color: var(--text-secondary);
--el-table-header-text-color: var(--text-primary);
}
.admin-table .el-table__cell {
padding: 12px 0;
}
.admin-table .cell {
padding-inline: 24px;
}
变量处理颜色,后两个选择器处理单元格的空间。它们依赖组件内部类名,升级组件库时要一起检查;不要把这些覆盖复制到每个业务页面里。
如果放进 Vue 的 scoped 样式,内部节点可能无法被普通后代选择器命中。可以使用项目统一的组件覆盖文件;确实需要局部覆盖时,再按 Vue 的规则使用 :deep()。选一种明确的维护方式,别让同一张表被多个地方轮流覆盖。
示例把页头、筛选字段和表格内容的起始位置统一到 24px,视觉上就有了一条稳定的对齐线。
这里的 12px、24px 都只是这张页面的取值。高频录入和密集检索的后台,需要更紧凑的行高;长文本多的列表,又需要允许换行。
大留白不是高级感的通行证。多看一屏数据,有时比多留一块空白更重要。
四、只改 --el-color-primary,为什么还不够?
很多人的第一步都是:
css
:root {
--el-color-primary: #087f5b;
}
然后发现:按钮正常时变绿了,Hover、浅色背景、选中态却没有完全跟着走。
原因是组件并不只消费一个主色。Element Plus 的主题还包含 primary-light-*、primary-dark-2 等色阶;只覆盖基础变量,不会自动重算已经生成的其他色阶。主题文档提供了 Sass 定制和 CSS 变量两种路径。
如果项目主色相对固定,可以在官方 Sass 变量模块首次加载前配置:
scss
// styles/element-theme.scss
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
'base': #087f5b,
),
)
);
// 全量样式引入示例;按需引入项目不要再加载这一行。
@use 'element-plus/theme-chalk/src/index.scss' as *;
这份入口应替代原来的预编译 element-plus/dist/index.css,而不是在它后面再叠一套。按需引入时,则需要让定制变量先于对应组件的 Sass 加载。
如果需要运行时换品牌色,也可以用 CSS 变量,但要同时维护相关色阶。本文演示为了直接在浏览器打开,采用了显式声明色阶的做法。
还有一个容易忽略的点:自动生成了色阶,不等于所有组合都适合阅读。尤其是浅色 Hover 背景配白字,要实际检查对比度,必要时单独调整按钮交互态。
五、深色主题暴露的问题,往往来自变量放错了地方
如果业务代码到处写着 #fff、#606266,增加深色主题就会变成到处补丁。
先把页面需要的颜色按用途命名:
css
:root {
--surface: #fff;
--surface-hover: #f2f6f4;
--text-primary: #202b33;
--text-secondary: #5c6872;
--line: #e3e8ec;
}
html[data-theme='dark'] {
color-scheme: dark;
--surface: #1c242c;
--surface-hover: #27333b;
--text-primary: #e8edf0;
--text-secondary: #b4c0c9;
--line: #35434e;
}
这只是正文、表面和分隔线的最小示例,不是完整主题。输入框、禁用态、状态色、遮罩和浮层还需要各自的定义。
这里有两个细节。
第一,**弱分隔线和输入框边界不要机械共用一种浅灰。**分隔线可以退后,输入框还得让人知道哪里能点、当前焦点在哪里。整页淡到看不清,并不会更精致。
第二,全局主题变量应放在 html 或 :root,不能只放在 .user-page。
例如 Select 的下拉浮层默认会被传送到页面其他位置。它不再是业务容器的后代,也就无法继承只定义在容器上的变量。Select 文档中的 teleported、append-to 描述了相关行为。
这就是"输入框已经变深色,下拉菜单还是白色"的一个常见排查方向。还要检查浮层实际消费的 --el-bg-color-overlay 等变量,不能只改输入框背景。

页面截图能确认大体层次,但验证主题还得亲自打开下拉菜单、移动鼠标、用 Tab 切换焦点,再看看禁用和空数据状态。
真正不协调的颜色,常常藏在这些时刻。
六、做到第二张页面,就该把这些规则收起来
第一张页面做好后,最容易发生的事是:下一张页面复制一份 CSS,再改几个数字。
几轮之后,用户页是 24px,角色页是 20px,菜单页又补了一层阴影。同一套组件库,再次拼出了三种风格。
我更愿意把这些内容按职责分开:
| 放在哪里 | 维护什么 |
|---|---|
| 主题文件 | 表面、文字、边框、状态与交互颜色 |
| 组件适配文件 | Element Plus 变量与必要的内部样式覆盖 |
| 列表页公共样式 | 页头、筛选、表格、分页的布局与间距 |
| 业务页面 | 字段内容、权限条件、操作顺序与数据行为 |
我维护的开源项目 LY Fullstack 里,用户管理页就采用了这种分工:页面保留业务列和操作,公共 CRUD 样式组织工作区,表格适配与主题变量分别维护。
想接着看真实项目,可以从这三个文件读起:
- 用户管理页:看用户信息分组、权限与操作怎样留在业务层。
- 公共 CRUD 布局:看页头、表格和分页怎样共享工作区。
- Element Plus 表格适配:看变量和样式覆盖怎样集中维护。
本文为了讲清取舍,做了更小的教学示例:比如把部分操作收进"更多"、调整双行内容的留白,这些不是项目原页面的逐项复刻。源码中的行高、操作布局应该结合它自己的业务场景理解。
这种分工也不意味着要立刻造一个"万能表格",把所有插槽、按钮和条件都塞进一份 JSON。先复用已经稳定的样式和布局,业务差异继续留在页面里,维护起来通常更直观。
下次接到后台页面,可以先检查这五件事
- 最重要的操作,能不能一眼找到?
- 本来属于同一个对象的信息,是否被拆得太散?
- 边框、标签和颜色,是在帮助阅读,还是彼此争抢注意力?
- 换到另一张业务页面,间距和交互规则是否仍然一致?
- 打开浮层、切换主题、出现空数据以后,页面是否还完整?
Element Plus 已经提供了很多可靠的零件。页面最终呈现出来的样子,取决于我们怎样安排这些零件,让用户更容易完成手头的事。
如果准备从自己的后台挑一处修改,我建议先看那一列永远摆满蓝色文字的"操作"。你可能会发现,真正每天都在点的,只有其中一两个。
不过,页面优化到这里,还有一件更值得继续往下做的事。
在 AI 已经能帮我们写接口、梳理数据模型的今天,前端开发者也可以把交付往前推进一步:让这张页面背后的功能真正跑起来。
点下"新增用户",数据怎么存进数据库?列表的筛选和分页,后端怎么接?"分配角色"和"重置密码",又该怎样校验权限,才能避免任何登录用户都能调用?
这些问题接起来,眼前这张页面才算有了完整的业务链路。
下一篇,我们就沿着这张用户管理页,用 NestJS + TypeScript,从数据库、接口到前后端联调,把它背后的真实功能做出来。
后端用的也是大家熟悉的 TypeScript,不用先换一门语言。如果你已经在用 Vue + TS,可以带着现有基础跟着做:从第一个接口开始,逐步补上数据库和权限这些知识,不必一上来就被"学后端"三个字吓住。过程中也会聊聊,哪些工作可以交给 AI,哪些规则需要我们自己想清楚。