56 KiB
UI 设计规范
适用范围:基于 Vue 3 + Element Plus 2.x 的后台管理系统 Web 页面
本文档为唯一设计规范:色彩、间距、布局、组件样式、全局 CSS 均定义于本文件(§6 分模块说明 + §8.9 完整 CSS)。项目实施时从本文档复制代码即可,不依赖其他设计资产文件。
目录
1. 页面整体风格
1.1 设计定位
本规范定义一套浅色、克制、数据导向的后台界面风格。视觉语言强调可读性与操作效率,避免装饰性元素喧宾夺主。
| 维度 | 规范 |
|---|---|
| 整体调性 | 专业、冷静、工具感;类似现代 SaaS 控制台 |
| 色彩策略 | 大面积中性灰白底 + 青绿品牌色点缀 |
| 层次表达 | 靠字号阶梯与灰度阶梯区分,不靠重阴影 |
| 容器形态 | 白底细边框卡片(1px + 8px 圆角),无浮起阴影 |
| 信息密度 | 列表/表格偏紧凑(size="small"),表单区留白充分 |
| 技术底座 | Element Plus 组件 + 全局 CSS 变量覆盖 |
1.2 核心设计原则
| 原则 | 说明 |
|---|---|
| 浅色优先 | 页面背景 #f4f4f5,内容区白卡片;禁止深色全屏侧栏 |
| 克制用色 | 主色青绿仅用于品牌、主按钮、导航激活态、关键正向状态 |
| 细边框卡片 | 白底 + 1px 浅灰边框 + 8px 圆角,禁止大面积 box-shadow |
| 8px 间距节奏 | 所有间距使用间距 Token(4px 仅用于极紧凑场景) |
| 层次靠字号与灰度 | 标题 #18181b,正文 #3f3f46,辅助 #71717a |
| 组件复用优先 | 使用 PageHeader、AppCard、StatCard 等标准壳层,禁止页面内自建容器体系 |
| 国际化友好 | 文案走 i18n;布局预留中英文长度差异(按钮组 flex-wrap) |
1.3 视觉示意
┌──────────────┬────────────────────────────────────┐
│ Sidebar │ Topbar(52px,白底,底部分割线) │
│ 240px ├────────────────────────────────────┤
│ 半透明浅灰 │ Main Content(padding 24px) │
│ 品牌区+导航 │ .page(max-width 1400px) │
│ │ PageHeader → StatGrid → AppCard │
└──────────────┴────────────────────────────────────┘
登录页为独立全屏居中布局,无侧栏与顶栏。
2. 布局标准
2.1 栅格系统
PC 端(≥1025px)
| 项目 | 规范 |
|---|---|
| 基准栅格 | 12 列逻辑栅格(内容区 max-width: 1400px 内划分) |
| 列间距(gutter) | 16px(--space-4) |
| 页面水平边距 | 主内容区内边距 24px(--space-6) |
| 典型列分配 | 统计卡 4×3 列;双栏内容 1.4fr : 1fr 或 1fr : 1fr;三栏等分 1fr × 3 |
常用网格类:
| 类名 | PC 列数 | 说明 |
|---|---|---|
.stat-grid |
4 列 | 概览指标卡 |
.content-grid--2 |
2 列等宽 | 并列卡片 |
.content-grid--2-1 |
2 列(约 7:5) | 主内容 + 侧信息 |
.content-grid--3 |
3 列等宽 | 三列面板(按需扩展) |
平板(641px – 1024px)
| 项目 | 规范 |
|---|---|
| 有效栅格 | 8 列逻辑栅格 |
.stat-grid |
2 列 |
.content-grid--* |
全部折叠为 1 列 |
| 主内容 padding | 保持 24px |
移动端(≤640px)
| 项目 | 规范 |
|---|---|
| 有效栅格 | 4 列逻辑栅格 |
.stat-grid |
1 列 |
.page-header |
纵向堆叠(标题在上,操作在下) |
| 主内容 padding | 16px(--space-4) |
| 底部 Tab(若启用) | 固定底部,高度 56px,图标 20px + 文字 10px |
默认以 PC 为主、移动端通过响应式折叠适配;若启用底部 Tab 导航,须使用本节尺寸,不得自定义高度。
2.2 间距体系(8px 基准)
硬性规则:页面级、组件级外边距与内边距必须使用间距 Token,禁止出现 5px、7px、10px、15px 等非体系数值。
| Token | 值 | 是否 8 的倍数 | 典型用途 |
|---|---|---|---|
--space-1 |
4px | ½×8,仅紧凑场景 | 标题与副标题间距、行内 code 水平 padding |
--space-2 |
8px | ✓ | 按钮组 gap、导航项内边距、表格单元格垂直 padding |
--space-3 |
12px | — | 列表项间距、卡片底栏提示 |
--space-4 |
16px | ✓ | 卡片间距、工具栏下边距、移动端页面 padding |
--space-5 |
20px | — | 卡片水平内边距、统计卡内边距 |
--space-6 |
24px | ✓ | 页头下边距、主内容 padding、区块分隔 |
--space-8 |
32px | ✓ | 登录卡片内边距、大区块分隔 |
垂直节奏(页面级)
.page-header下方:24px.stat-grid、.content-grid下方:24px- 相邻
.app-card间距:16px;页面最后一个卡片无底边距
已知例外(保留,勿改)
| 位置 | 值 | 原因 |
|---|---|---|
.form-hint 的 margin-top |
6px |
紧贴表单项控件底部,视觉上等同 8px 体系 |
--primary-hover 已定义但未写独立 CSS 规则,主按钮 hover 由 Element Plus 通过 --el-color-primary-dark-2 自动处理。
2.3 布局组件尺寸
| 组件 | 尺寸 | 其他规则 |
|---|---|---|
侧栏 .sidebar |
宽 240px(15rem) |
右边框 1px;背景半透明 + backdrop-filter: blur(8px) |
侧栏品牌区 .sidebar-brand |
高随内容;padding 20px 16px |
品牌图标 32×32px |
导航项 .nav-item |
高随内容;padding 8px 12px;margin 左右 8px |
圆角 8px;图标 16px |
导航分组标签 .nav-group-label |
padding 16px 16px 8px |
字号 12px,全大写 |
顶栏 .topbar |
高 52px |
左右 padding 24px;白底 + 底边框 |
主内容 .main-content |
flex 填满剩余高度 | padding 24px(移动端 16px);纵向滚动 |
页面容器 .page |
max-width 1400px |
无水平居中强制(随主内容左对齐) |
| 底部 Tab(预留) | 高 56px |
图标 20px,标签 10px,最多 5 项 |
登录卡片 .login-card |
max-width 外层 400px;内边距 32px |
品牌图标 48×48px,圆角 12px |
2.4 断点
| 断点 | 媒体查询 | 主要变化 |
|---|---|---|
| 平板 | max-width: 1024px |
统计 2 列;内容网格单列 |
| 手机 | max-width: 640px |
统计 1 列;页头纵向;主内容 16px padding |
2.5 布局结构(ASCII)
PC (≥1025px)
┌─ sidebar 240px ─┬──────────── main-area ────────────────────┐
│ [brand 32px] │ topbar 52px │
│ [nav × N] ├───────────────────────────────────────────┤
│ │ padding 24px │
│ │ ┌─ page max 1400px ─────────────────┐ │
│ │ │ page-header │ │
│ │ │ stat-grid (4 col, gap 16) │ │
│ │ │ app-card │ │
│ │ └────────────────────────────────────┘ │
└─────────────────┴───────────────────────────────────────────┘
Mobile (≤640px)
┌──────────────── main-area 100% ────────────────┐
│ topbar 52px │
├────────────────────────────────────────────────┤
│ padding 16px │
│ page-header (column) │
│ stat-grid (1 col) │
│ app-card │
├────────────────────────────────────────────────┤
│ [optional bottom-tab 56px] │
└────────────────────────────────────────────────┘
3. 字体标准
3.1 字体族
| 场景 | 字体栈 |
|---|---|
| 中文界面 | "PingFang SC", "Microsoft YaHei", "Noto Sans SC", "Helvetica Neue", Arial, sans-serif |
| 英文界面 | ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif |
| 合并栈(推荐默认) | ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif` |
| 等宽(代码/文件名) | ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace` |
中英文共用同一 sans 栈:西文优先 system 字体,CJK 由系统自动 fallback 至苹方/微软雅黑。禁止为中文单独引入 Web Font,保持加载性能。
3.2 字号阶梯
根元素 font-size: 14px(1rem = 14px)。所有字号使用 rem 相对根字号。
| 层级 | 类名 / 场景 | 字号 | 字重 | 行高 | 颜色 |
|---|---|---|---|---|---|
| 页面标题 | .page-title |
1.5rem(21px) |
600 | 默认 | --foreground |
| 登录品牌标题 | .page-title(登录页) |
同上 | 600 | 默认 | --foreground |
| 卡片标题 | .app-card__title |
0.9375rem(13px) |
600 | 默认 | --foreground |
| 品牌名 | .brand-name |
1.125rem(15.75px) |
600 | 默认 | --foreground |
| 统计数值 | .stat-card__value |
1.75rem(24.5px) |
600 | 默认 | --foreground |
| 正文 / 导航 / 顶栏 | .nav-item, .topbar-title, 表格正文 |
0.875rem(12.25px) |
400–500 | 1.4(表格) | --el-text-color-regular 或 --muted-foreground |
| 描述 / 标签说明 | .page-desc, .stat-card__label |
0.875rem |
400 | 默认 | --muted-foreground |
| 表头 / 辅助 | 表头 th, .form-hint, .nav-group-label |
0.75rem(10.5px) |
600(表头)/ 400 | 默认 | --muted-foreground |
| 极小说明 | .form-hint, code.inline-code |
0.75rem |
400 | 默认 | --muted-foreground |
| 等宽数据 | .kv-block code, .meta-list |
0.8125rem |
400 | 默认 | 继承 |
3.3 字重与样式
| 样式 | 使用场景 | 禁止场景 |
|---|---|---|
| 600(Semibold) | 页面标题、卡片标题、表头、导航激活态、分组标题 .section-title |
大段正文 |
| 500(Medium) | 导航默认态、顶栏次要文字 | — |
| 400(Regular) | 正文、描述、表单标签 | — |
加粗 <strong> |
行内强调少量关键词 | 整段文字 |
斜体 <em> |
极少使用;仅引用或术语首次出现 | 按钮、导航、表头 |
letter-spacing: -0.02em |
仅 .page-title |
其他文字 |
text-transform: uppercase |
仅 .nav-group-label |
其他文字 |
3.4 Element Plus 表单标签
| 场景 | label-width |
|---|---|
| 简单 CRUD 对话框 | 100px |
| 字段名较长 | 110px |
| 系统设置长表单 | 120px |
| 嵌套子表单 | 90px |
| 登录页 | size="large"(EP 默认大号控件) |
4. 颜色规范
4.1 色彩角色一览
品牌层:primary 系列(操作引导)
中性层:background / card / muted / border(结构)
语义层:success / warning / danger / info(状态)
文本层:foreground / regular / muted-foreground(可读性)
4.2 完整色板
品牌色
| Token | 色值 | 用途 |
|---|---|---|
--primary |
#0f7a62 |
主按钮、品牌图标底、导航激活文字、链接强调 |
--primary-hover |
#0d6b56 |
主按钮 hover |
--primary-foreground |
#fafafa |
主色背景上的文字/图标 |
--el-color-primary-light-3 |
#4da08c |
EP 组件浅色变体 |
--el-color-primary-light-5 |
#76b8a8 |
— |
--el-color-primary-light-7 |
#a0d0c4 |
— |
--el-color-primary-light-8 |
#b8dcd2 |
— |
--el-color-primary-light-9 |
#d0e8e1 |
浅色背景提示 |
--el-color-primary-dark-2 |
#0c624e |
主色按下/深色态 |
中性色
| Token | 色值 | 用途 |
|---|---|---|
--background |
#f4f4f5 |
页面底色、登录页背景 |
--foreground |
#18181b |
主标题、强强调正文 |
--card |
#ffffff |
卡片、顶栏、弹窗内容区背景 |
--card-foreground |
#18181b |
卡片上主文字 |
--muted |
#f4f4f5 |
表头背景、内嵌块、行内 code 底 |
--muted-foreground |
#71717a |
描述、表头文字、顶栏标题、占位性文字 |
--border |
#e4e4e7 |
卡片边框、分割线、顶栏底边 |
--input |
#e4e4e7 |
输入框边框(与 border 一致) |
侧栏专用
| Token | 色值 | 用途 |
|---|---|---|
--sidebar |
rgba(250, 250, 250, 0.85) |
侧栏背景(半透明) |
--sidebar-foreground |
#18181b |
侧栏主文字 |
--sidebar-accent |
#f4f4f5 |
导航 hover / active 背景 |
--sidebar-border |
#e4e4e7 |
侧栏右边框 |
语义色
| Token / 类名 | 色值 | 用途 |
|---|---|---|
--success |
#047857 |
成功状态(与 EP success 同步) |
--warning |
#b45309 |
警告状态 |
--danger / --destructive |
#dc2626 |
危险、错误、删除 |
--info |
#a1a1aa |
中性、禁用、分类标签 |
--el-color-success |
#047857 |
el-tag type="success" |
--el-color-warning |
#b45309 |
el-tag type="warning" |
--el-color-danger |
#dc2626 |
el-tag type="danger" |
--el-color-info |
#a1a1aa |
el-tag type="info" |
.status-good |
#047857 |
数值/状态优良 |
.status-warn |
#b45309 |
数值/状态偏高 |
.status-bad |
#dc2626 |
数值/状态很差 |
.status-muted |
--muted-foreground |
无数据/超时/不可用 |
.meta-list__ok |
var(--success) |
列表项就绪 |
.meta-list__warn |
var(--warning) |
列表项缺失/警告 |
文本色(Element Plus 对齐)
| Token | 色值 | 用途 |
|---|---|---|
--el-text-color-primary |
#18181b |
EP 主要文字 |
--el-text-color-regular |
#3f3f46 |
表格正文 |
--el-text-color-secondary |
#71717a |
EP 次要文字 |
4.3 颜色使用规范
| 规则 | 说明 |
|---|---|
| 禁止硬编码色值 | 新样式必须使用 CSS 变量 |
| 主色克制 | 单屏主色面积不超过 15%;除主按钮与激活导航外不大面积铺主色 |
| 状态色统一 | 成功/失败/禁用一律用 el-tag 语义 type,不自创色块 |
| 边框优先于阴影 | 容器分隔用 1px solid var(--border),不用阴影 |
| 危险操作 | 破坏性操作用 type="danger" + link(表格行内)或二次确认弹框 |
| 图表 | 遵循 §6.16 配色与 grid 配置 |
4.4 圆角
| Token | 值 | 用途 |
|---|---|---|
--radius |
0.5rem(8px) |
卡片、按钮、输入框、导航项、内嵌块 |
| 登录品牌图标 | 12px |
仅 .login-brand-icon,略大以强化品牌 |
5. 图标使用规范
5.1 图标体系
| 类型 | 来源 | 说明 |
|---|---|---|
| 品牌图标 | 项目自有 Logo(自行实现) | 产品标识;仅用于侧栏 .brand-icon、登录页 .login-brand-icon;各项目图形不同,本文档不提供参考实现 |
| 功能图标 | @element-plus/icons-vue |
导航、操作提示;通过 <el-icon> 包裹 |
| 状态 | 优先 el-tag 文字 |
不以图标替代明确状态文案 |
5.2 尺寸规范
| 场景 | 尺寸 |
|---|---|
| 侧栏品牌区 Logo | 20px |
| 登录页品牌 Logo | 24px |
| 侧栏导航图标 | 16px(<el-icon :size="16">) |
| 按钮内图标(若使用) | 14px |
| 底部 Tab(预留) | 20px |
5.3 常见场景图标推荐
图标库统一使用 @element-plus/icons-vue(Outlined 线型)。下表为常见导航/功能场景的推荐映射,各项目可按业务替换,但须保持风格一致。
| 场景 | 推荐图标 | 含义 |
|---|---|---|
| 首页 / 仪表盘 | Odometer、HomeFilled |
总览、监控 |
| 列表 / 规则 / 配置项 | List、Menu |
条目化管理 |
| 连接 / 网络 | Connection、Share |
链路、拓扑 |
| 外部关联 / 链接 | Link |
外部数据源 |
| 切换 / 路由 / 策略 | Switch |
分流、调度 |
| 日志 / 文档 | Document、Tickets |
记录、流水 |
| 用户 / 权限 | User、Lock |
身份、安全 |
| 系统设置 | Setting |
全局配置 |
| 新建 | Plus |
创建资源 |
| 编辑 | Edit |
修改 |
| 删除 | Delete |
移除(配合 danger 按钮,不单用图标) |
| 刷新 | Refresh |
重新加载 |
| 导入 / 导出 | Upload、Download |
数据交换 |
5.4 使用原则
| 原则 | 说明 |
|---|---|
| 导航必带图标 | 侧栏每项左侧固定 16px 图标,增强扫读性 |
| 颜色跟随文字 | 图标 currentColor,不单独设色 |
| 禁止混用风格 | 统一 Outlined 线型图标(Element Plus Icons) |
| 操作按钮 | 主操作优先文字按钮;图标-only 按钮需 aria-label |
| 品牌图标不外借 | Logo 仅用于品牌区,不用于普通操作按钮 |
6. 组件与页面标准样式
本节 6.1–6.14 为分模块说明(便于查阅);合并后的完整全局 CSS 见 §8.9。两处内容须保持一致。
6.0 技术栈与引入
| 项目 | 要求 |
|---|---|
| Vue | ^3.4 |
| Element Plus | ^2.4(2.x 均可,须使用 CSS 变量主题) |
| 图标 | @element-plus/icons-vue |
| 图表(可选) | echarts ^5 |
main.ts 引入顺序(顺序不可颠倒):
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' // ① 先 Element Plus 基础样式
import './styles/global.css' // ② 再将本文档 §8.9 复制为全局样式文件后引入
const app = createApp(App)
app.use(ElementPlus) // ③ 全量注册(或按需,但变量须生效)
app.mount('#app')
依赖说明:
- 全局样式仅来源于本文档 §8.9,文件名与存放路径由项目自定(如
global.css、app.css)。 - 按钮、输入框、弹框、Tabs 等无需逐组件写 CSS,通过
:root中的--el-*变量与品牌色对齐。 - 表格、卡片容器、布局壳层使用本文档自定义 class,须与 §8.1 壳层 DOM 结构配合。
6.1 设计 Token 与基础重置
:root {
--background: #f4f4f5;
--foreground: #18181b;
--card: #ffffff;
--card-foreground: #18181b;
--primary: #0f7a62;
--primary-hover: #0d6b56;
--primary-foreground: #fafafa;
--muted: #f4f4f5;
--muted-foreground: #71717a;
--border: #e4e4e7;
--input: #e4e4e7;
--destructive: #dc2626;
--success: #047857;
--warning: #b45309;
--danger: #dc2626;
--info: #a1a1aa;
--sidebar: rgba(250, 250, 250, 0.85);
--sidebar-foreground: #18181b;
--sidebar-accent: #f4f4f5;
--sidebar-border: #e4e4e7;
--sidebar-width: 15rem;
--radius: 0.5rem;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--el-color-primary: #0f7a62;
--el-color-primary-light-3: #4da08c;
--el-color-primary-light-5: #76b8a8;
--el-color-primary-light-7: #a0d0c4;
--el-color-primary-light-8: #b8dcd2;
--el-color-primary-light-9: #d0e8e1;
--el-color-primary-dark-2: #0c624e;
--el-color-success: #047857;
--el-color-warning: #b45309;
--el-color-danger: #dc2626;
--el-color-info: #a1a1aa;
--el-border-radius-base: 0.5rem;
--el-border-color: #e4e4e7;
--el-bg-color-page: #f4f4f5;
--el-text-color-primary: #18181b;
--el-text-color-regular: #3f3f46;
--el-text-color-secondary: #71717a;
--el-font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
}
* { box-sizing: border-box; }
html, body, #app {
margin: 0;
padding: 0;
height: 100%;
font-size: 14px;
font-family: var(--el-font-family);
background: var(--background);
color: var(--foreground);
}
6.2 布局壳层(侧栏 / 顶栏 / 主内容)
.layout {
display: flex;
height: 100vh;
}
.sidebar {
width: var(--sidebar-width);
flex-shrink: 0;
background: var(--sidebar);
border-right: 1px solid var(--sidebar-border);
backdrop-filter: blur(8px);
display: flex;
flex-direction: column;
overflow-y: auto;
}
.sidebar-brand {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-5) var(--space-4);
}
.brand-icon {
width: 32px;
height: 32px;
border-radius: var(--radius);
background: var(--primary);
color: var(--primary-foreground);
display: flex;
align-items: center;
justify-content: center;
}
.brand-name {
font-size: 1.125rem;
font-weight: 600;
}
.nav-group-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted-foreground);
padding: var(--space-4) var(--space-4) var(--space-2);
}
.nav-item {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
margin: 0 var(--space-2);
border-radius: var(--radius);
color: var(--muted-foreground);
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
}
.nav-item:hover { background: var(--sidebar-accent); }
.nav-item.active {
background: var(--sidebar-accent);
color: var(--primary);
font-weight: 600;
}
.main-area {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.topbar {
height: 52px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--space-6);
background: var(--card);
border-bottom: 1px solid var(--border);
}
.topbar-title {
font-size: 0.875rem;
color: var(--muted-foreground);
}
.main-content {
flex: 1;
overflow-y: auto;
padding: var(--space-6);
}
6.3 页面头与网格
.page {
max-width: 1400px;
}
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: var(--space-6);
gap: var(--space-4);
}
.page-actions {
display: flex;
align-items: center;
gap: var(--space-2);
flex-shrink: 0;
flex-wrap: wrap;
}
.page-title {
font-size: 1.5rem;
font-weight: 600;
letter-spacing: -0.02em;
margin: 0 0 var(--space-1);
}
.page-desc {
font-size: 0.875rem;
color: var(--muted-foreground);
margin: 0;
}
.stat-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-4);
margin-bottom: var(--space-6);
}
.stat-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--space-5);
}
.stat-card__label {
font-size: 0.875rem;
color: var(--muted-foreground);
margin-bottom: var(--space-2);
}
.stat-card__value {
font-size: 1.75rem;
font-weight: 600;
}
.content-grid {
display: grid;
gap: var(--space-4);
margin-bottom: var(--space-6);
}
.content-grid--2-1 { grid-template-columns: 1.4fr 1fr; }
.content-grid--2 { grid-template-columns: 1fr 1fr; }
.content-grid--3 { grid-template-columns: repeat(3, 1fr); }
6.4 卡片与工具栏
.app-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: var(--space-4);
}
.page > .app-card:last-child,
.page > .content-grid:last-child {
margin-bottom: 0;
}
.app-card__header {
padding: var(--space-4) var(--space-5);
border-bottom: 1px solid var(--border);
}
.app-card__title {
font-size: 0.9375rem;
font-weight: 600;
margin: 0;
}
.app-card__body--padded { padding: var(--space-5); }
.card-toolbar {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-4) var(--space-5) 0;
margin-bottom: var(--space-4);
flex-wrap: wrap;
}
.card-toolbar--desc {
align-items: flex-start;
}
.card-pager {
padding: var(--space-4) var(--space-5);
display: flex;
justify-content: flex-end;
}
.card-footer-hint {
padding: 0 var(--space-5) var(--space-3);
margin: 0;
}
.card-footer-hint + .card-pager {
padding-top: var(--space-3);
}
.card-inset-block {
margin: 0 var(--space-5) var(--space-4);
padding: var(--space-3);
background: var(--muted);
border-radius: var(--radius);
font-size: 0.875rem;
}
.tabs-bar {
padding: 0 var(--space-5);
border-bottom: 1px solid var(--border);
}
.tabs-bar .el-tabs__header { margin-bottom: 0; }
.page-toolbar {
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-4);
flex-wrap: wrap;
}
.inline-actions {
display: flex;
align-items: center;
gap: var(--space-2);
flex-wrap: wrap;
}
.card-header-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
}
6.5 表单、间距工具类
.form-hint {
margin-top: 6px;
font-size: 0.75rem;
color: var(--muted-foreground);
}
.form-hint--flush { margin-top: 0; }
.u-mt-3 { margin-top: var(--space-3); }
.u-mt-4 { margin-top: var(--space-4); }
.u-mb-3 { margin-bottom: var(--space-3); }
.u-mb-4 { margin-bottom: var(--space-4); }
.u-mb-6 { margin-bottom: var(--space-6); }
.text-center { text-align: center; }
.w-full { width: 100%; }
code.inline-code {
font-size: 0.75rem;
padding: 0 var(--space-1);
background: var(--muted);
border-radius: var(--radius);
}
.section-title {
font-size: 0.875rem;
font-weight: 600;
margin-bottom: var(--space-4);
}
.section-title:not(:first-child) { margin-top: var(--space-6); }
.el-form .el-select { width: 100%; }
6.6 登录页
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: var(--background);
padding: var(--space-6);
}
.login-wrap { max-width: 400px; width: 100%; text-align: center; }
.login-brand-icon {
width: 48px;
height: 48px;
border-radius: 12px;
background: var(--primary);
color: var(--primary-foreground);
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: var(--space-4);
}
.login-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--space-8);
margin-top: var(--space-6);
text-align: left;
}
6.7 表格
.app-card .el-table {
--el-table-header-bg-color: var(--muted);
font-size: 0.875rem;
}
.app-card .el-table--small .el-table__cell {
padding: var(--space-2) var(--space-3);
}
.app-card .el-table--small th.el-table__cell {
font-size: 0.75rem;
font-weight: 600;
color: var(--muted-foreground);
}
.app-card .el-table--small td.el-table__cell {
color: var(--el-text-color-regular);
}
.app-card .el-table--small .el-table__row {
height: 40px;
}
.app-card .el-table .el-button.is-link {
padding: 0;
height: auto;
}
.app-card .el-table .cell {
line-height: 1.4;
}
.table-scroll--sm { max-height: 320px; }
.table-scroll--md { max-height: 360px; }
.table-scroll--lg { max-height: 480px; }
.card-pager .el-pagination {
font-size: 0.875rem;
}
表格列宽约定
| 列类型 | 宽度 |
|---|---|
| 时间 | 168px |
| 状态 / 布尔 | 80–100px |
| 操作 | 120–160px,fixed="right" |
| 主文本 | min-width,不设固定宽 |
6.8 工具栏控件宽度
.toolbar-select { width: 120px; }
.toolbar-select--country { width: 160px; }
.toolbar-select--wide { width: 280px; }
.toolbar-input { width: 200px; }
.toolbar-input--wide { width: 260px; }
.toolbar-input--grow { flex: 1; min-width: 200px; }
.table-cell-select { width: 100%; min-width: 200px; }
.card-toolbar .toolbar-input--grow {
flex: 1;
min-width: 200px;
}
6.9 按钮规范(Element Plus 约定)
| 场景 | 写法 |
|---|---|
| 页面主操作 | <el-button type="primary"> |
| 页头次要操作 | 默认按钮(无 type) |
| 表格行内操作 | <el-button size="small" link> |
| 表格行内删除 | <el-button size="small" type="danger" link> |
| 顶栏退出 | <el-button text> |
| 登录提交 | <el-button type="primary" class="w-full" size="large"> |
| 加载中 | :loading="true",禁止重复点击 |
6.10 弹框与抽屉
| 类型 | 宽度 | 属性 |
|---|---|---|
| 简单 CRUD | 480–520px |
destroy-on-close |
| 导入/中等表单 | 560–640px |
同上 |
| 复杂列表弹窗 | 720px |
同上 |
| 二级编辑 | 抽屉 50% |
— |
底部按钮顺序:取消(默认)在左,确认(type="primary")在右。
6.11 标签(Tag)
统一 size="small":
| 语义 | type |
|---|---|
| 启用 / 成功 / 存活 | success |
| 禁用 / 中性 | info |
| 失败 / 错误 | danger |
| 警告 / 未就绪 | warning |
| 可选项高亮 | primary |
6.12 状态色与辅助块
.status-good { color: #047857; }
.status-warn { color: #b45309; }
.status-bad { color: #dc2626; }
.status-muted { color: var(--muted-foreground); }
.kv-block p {
margin: 0 0 var(--space-2);
}
.kv-block p:last-child {
margin-bottom: 0;
}
.kv-block .label {
display: inline-block;
width: 5rem;
color: var(--muted-foreground);
}
.kv-block code {
font-size: 0.8125rem;
}
.cell-stack {
display: flex;
flex-direction: column;
gap: var(--space-2);
width: 100%;
}
.meta-list {
margin: 0;
padding: 0;
list-style: none;
font-size: 0.8125rem;
color: var(--muted-foreground);
}
.meta-list li {
display: flex;
justify-content: space-between;
gap: var(--space-3);
padding: var(--space-1) 0;
}
.meta-list__mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
.meta-list__ok { color: var(--el-color-success); }
.meta-list__warn { color: var(--el-color-warning); }
.chart-box {
height: 280px;
}
6.13 滚动条与响应式
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: #e4e4e7 transparent;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #e4e4e7;
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
@media (max-width: 1024px) {
.stat-grid { grid-template-columns: repeat(2, 1fr); }
.content-grid--2-1,
.content-grid--2,
.content-grid--3 { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
.stat-grid { grid-template-columns: 1fr; }
.main-content { padding: var(--space-4); }
.page-header { flex-direction: column; }
}
6.14 全局工具类速查
| 类名 | 用途 |
|---|---|
.card-toolbar |
卡片内筛选行 |
.card-pager |
卡片内分页(右对齐) |
.card-header-row |
卡片头标题 + 右侧操作 |
.form-hint |
表单项下方说明 |
.section-title |
长表单分组标题 |
.page-toolbar |
页内独立工具条 |
.tabs-bar |
卡片内 Tab 容器 |
.toolbar-select / .toolbar-input |
工具栏控件宽度 |
.table-scroll--sm/md/lg |
表格最大高度 |
.w-full |
宽度 100% |
.u-mb-4 / .u-mt-3 等 |
标准外边距 |
.status-good / .status-warn / .status-bad |
行内数值/状态语义色 |
.kv-block |
键值对结果展示(常配合 .card-inset-block) |
.cell-stack |
表格单元格内纵向堆叠 |
.meta-list |
元数据清单列表 |
.custom-scrollbar |
细滚动条(Firefox + WebKit) |
6.15 Element Plus 适配说明
以下组件依靠 EP 默认样式 + :root 变量即可,禁止重复覆盖(除非本节另有说明):
| 组件 | 使用约定 | 自定义 CSS |
|---|---|---|
el-button |
主操作 type="primary";行内 size="small" link |
无(颜色来自 --el-color-primary*) |
el-input / el-select |
表单内 select 宽度 100% |
仅 .el-form .el-select { width: 100% } |
el-dialog |
destroy-on-close;宽度见 §6.10 |
无 |
el-drawer |
二级编辑 size="50%" |
无 |
el-tabs |
放在 .tabs-bar 内 |
仅 .tabs-bar .el-tabs__header { margin-bottom: 0 };激活色来自 --el-color-primary |
el-pagination |
放在 .card-pager 内 |
仅 font-size: 0.875rem |
el-tag |
统一 size="small" |
语义色来自 --el-color-success 等 |
el-message / el-message-box |
操作反馈 | 无 |
el-form |
label-width 见 §3.4 |
无 |
Tabs 推荐结构:
<AppCard title="列表标题">
<div class="tabs-bar">
<el-tabs v-model="activeTab">
<el-tab-pane label="Tab A" name="a" />
<el-tab-pane label="Tab B" name="b" />
</el-tabs>
</div>
<!-- 表格或内容 -->
</AppCard>
Dialog 推荐结构:
<el-dialog v-model="visible" title="标题" width="520px" destroy-on-close>
<el-form label-width="100px">…</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="save">确认</el-button>
</template>
</el-dialog>
6.16 图表规范(ECharts)
含折线图的统计页使用 .chart-box 容器(高度 280px)。图表配置须与品牌色一致:
| 配置项 | 值 |
|---|---|
| 容器 class | chart-box(height: 280px) |
| 主系列色 | #0f7a62(var(--primary)) |
| 次系列色 | #71717a(var(--muted-foreground)) |
| 第三系列及以后 | #4da08c、#76b8a8、#a0d0c4(primary-light 阶梯) |
grid |
{ left: 40, right: 16, top: 16, bottom: 24 } |
| 坐标轴字号 | 12(与 0.857rem 接近) |
| 坐标轴颜色 | #71717a |
| 分割线 | #e4e4e7 |
| 折线 | smooth: true;线宽 2 |
| tooltip | trigger: 'axis' |
| 图例 | 顶部或底部,字号 12 |
chart.setOption({
grid: { left: 40, right: 16, top: 16, bottom: 24 },
xAxis: {
type: 'category',
data: labels,
axisLabel: { color: '#71717a', fontSize: 12 },
axisLine: { lineStyle: { color: '#e4e4e7' } },
},
yAxis: {
type: 'value',
axisLabel: { color: '#71717a', fontSize: 12 },
splitLine: { lineStyle: { color: '#e4e4e7' } },
},
series: [
{ name: '主指标', type: 'line', smooth: true, data: a, color: '#0f7a62' },
{ name: '次指标', type: 'line', smooth: true, data: b, color: '#71717a' },
],
tooltip: { trigger: 'axis' },
})
7. 交互原则
7.1 反馈与状态
| 原则 | 规范 |
|---|---|
| 操作反馈 | 成功/失败使用消息提示(如 ElMessage);破坏性操作须二次确认(如 ElMessageBox.confirm) |
| 加载态 | 列表 v-loading;按钮 :loading;全页加载用卡片级 loading |
| 空状态 | 表格无数据依赖 EP 默认 empty;需文案时走 i18n |
| 禁用态 | 批量任务进行中禁用与之互斥的行内操作 |
| 乐观更新 | 不做乐观更新;等 API 成功后再刷新列表 |
7.2 导航与层级
| 原则 | 规范 |
|---|---|
| 路由即导航 | 侧栏链接与当前路由同步,激活态高亮 |
| 顶栏标题 | 显示当前一级导航名称,非重复页面 H1 |
| 页内标题 | PageHeader 的 title 为页面 H1,描述可选 |
| 退出登录 | 顶栏右侧 text 按钮,清除会话后跳转登录页 |
7.3 表单与对话框
| 原则 | 规范 |
|---|---|
| 关闭销毁 | 对话框统一 destroy-on-close,避免脏数据残留 |
| 提交防重 | 提交按钮 loading 期间禁止重复提交 |
| 取消不保存 | 取消仅关闭弹层,不触发 API |
| 密码字段 | type="password" + show-password |
| 字段说明 | 用 .form-hint,不用 placeholder 替代说明 |
7.4 列表与表格
| 原则 | 规范 |
|---|---|
| 主操作位置 | 新建/导入放 PageHeader#actions;筛选放 .card-toolbar |
| 行内操作 | 链接式小按钮,删除放最右 |
| 分页 | 放 .card-pager,右对齐 |
| 刷新 | 提供显式「刷新」按钮;除监控大屏等实时场景外,不依赖隐式自动刷新 |
| 固定列 | 操作列 fixed="right" |
7.5 动效与过渡
| 原则 | 规范 |
|---|---|
| 克制动效 | 仅使用 EP 默认过渡(对话框、下拉、Message) |
| 禁止花哨动画 | 不加自定义 page transition、弹跳、视差 |
| 侧栏 | 桌面端固定展开,不用折叠动画(移动端待扩展) |
7.6 无障碍与可用性
| 原则 | 规范 |
|---|---|
| 对比度 | 正文与背景对比度 ≥ 4.5:1 |
| 焦点 | 依赖 EP 默认 focus ring,不自行移除 |
| 点击目标 | 按钮最小高度遵循 EP(默认 32px,large 40px) |
| 错误信息 | 接口错误映射为用户可读的本地化文案(如限流、权限不足) |
7.7 国际化
| 原则 | 规范 |
|---|---|
| 文案 | 所有可见文字走国际化方案(如 i18n),禁止硬编码用户可见字符串 |
| 时间 | 显示格式 YYYY-MM-DD HH:mm:ss(ISO 替换 T 并截断) |
| 布局弹性 | 按钮组、工具栏 flex-wrap: wrap 适配长文案 |
8. 附录:复刻指南
按以下顺序实施,可在新项目中复刻本文档定义的全部视觉(Vue 3 + Element Plus 2.x)。
8.1 壳层布局完整模板
DOM 层级(class 名称不可改):
.layout
├── .sidebar.custom-scrollbar
│ ├── .sidebar-brand
│ │ ├── .brand-icon → 项目自有 Logo(20px)
│ │ └── .brand-name
│ ├── .nav-group-label(可选分组)
│ └── a.nav-item[.active] × N(含 el-icon 16px)
└── .main-area
├── header.topbar
│ ├── .topbar-title
│ └── 顶栏操作(如 el-button text)
└── main.main-content.custom-scrollbar
└── router-view → 各页面 .page
<!-- layouts/MainLayout.vue — 文件名自定 -->
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Odometer, List, Setting } from '@element-plus/icons-vue'
const appName = '应用名称' // 替换为实际产品名
const route = useRoute()
const router = useRouter()
const navItems = [
{ path: '/', label: '概览', icon: Odometer },
{ path: '/items', label: '列表', icon: List },
{ path: '/settings', label: '设置', icon: Setting },
]
const pageTitle = computed(() => {
return navItems.find((n) => n.path === route.path)?.label ?? '后台'
})
function logout() {
router.push('/login')
}
</script>
<template>
<div class="layout">
<aside class="sidebar custom-scrollbar">
<div class="sidebar-brand">
<div class="brand-icon">
<!-- 项目自有 Logo,显示尺寸 20px -->
</div>
<span class="brand-name">{{ appName }}</span>
</div>
<div class="nav-group-label">管理</div>
<router-link
v-for="item in navItems"
:key="item.path"
:to="item.path"
class="nav-item"
:class="{ active: route.path === item.path }"
>
<el-icon :size="16"><component :is="item.icon" /></el-icon>
{{ item.label }}
</router-link>
</aside>
<div class="main-area">
<header class="topbar">
<span class="topbar-title">{{ pageTitle }}</span>
<el-button text @click="logout">退出</el-button>
</header>
<main class="main-content custom-scrollbar">
<router-view />
</main>
</div>
</div>
</template>
8.2 登录页完整模板
<!-- views/Login.vue — 文件名自定 -->
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const appName = '应用名称'
const router = useRouter()
const password = ref('')
const loading = ref(false)
async function submit() {
loading.value = true
try {
// await auth.login(password.value)
router.push('/')
} finally {
loading.value = false
}
}
</script>
<template>
<div class="login-page">
<div class="login-wrap">
<div class="login-brand-icon">
<!-- 项目自有 Logo,显示尺寸 24px -->
</div>
<h1 class="page-title">{{ appName }}</h1>
<p class="page-desc">一句话产品说明</p>
<div class="login-card">
<h3 class="app-card__title u-mb-6">登录</h3>
<el-form size="large" @submit.prevent="submit">
<el-form-item label="密码">
<el-input v-model="password" type="password" show-password placeholder="请输入密码" />
</el-form-item>
<el-button type="primary" native-type="submit" :loading="loading" class="w-full">
登录
</el-button>
</el-form>
<p class="form-hint u-mt-4 text-center">登录说明文字</p>
</div>
</div>
</div>
</template>
8.3 公共组件完整源码
<!-- components/PageHeader.vue -->
<script setup lang="ts">
defineProps<{ title: string; description?: string }>()
</script>
<template>
<div class="page-header">
<div>
<h1 class="page-title">{{ title }}</h1>
<p v-if="description" class="page-desc">{{ description }}</p>
</div>
<div v-if="$slots.actions" class="page-actions">
<slot name="actions" />
</div>
</div>
</template>
<!-- components/AppCard.vue -->
<script setup lang="ts">
defineProps<{ title?: string; padded?: boolean }>()
</script>
<template>
<div class="app-card">
<div v-if="title || $slots.header" class="app-card__header">
<slot name="header">
<h3 class="app-card__title">{{ title }}</h3>
</slot>
</div>
<div :class="padded ? 'app-card__body--padded' : ''">
<slot />
</div>
</div>
</template>
<!-- components/StatCard.vue -->
<script setup lang="ts">
defineProps<{ label: string; value: string | number }>()
</script>
<template>
<div class="stat-card">
<div class="stat-card__label">{{ label }}</div>
<div class="stat-card__value">{{ value }}</div>
</div>
</template>
品牌 Logo 由各项目自行提供,放入 .brand-icon(20px)与 .login-brand-icon(24px)容器即可;须使用 currentColor 或适配主色底上的 --primary-foreground。
8.4 标准页面模板
<template>
<div class="page">
<PageHeader title="页面标题" description="一句话说明">
<template #actions>
<el-button type="primary">主操作</el-button>
</template>
</PageHeader>
<div class="stat-grid">
<StatCard label="指标 A" value="128" />
<StatCard label="指标 B" value="64" />
</div>
<AppCard>
<div class="card-toolbar">
<el-input class="toolbar-input" placeholder="搜索" clearable />
<el-button type="primary">查询</el-button>
</div>
<el-table :data="items" v-loading="loading" size="small">
<el-table-column prop="name" label="名称" min-width="160" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button size="small" type="danger" link @click="remove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="card-pager">
<el-pagination layout="prev, pager, next" :total="100" />
</div>
</AppCard>
</div>
</template>
8.5 带 Tab 的列表页
<AppCard title="数据列表">
<div class="tabs-bar">
<el-tabs v-model="activeTab">
<el-tab-pane label="实时" name="live" />
<el-tab-pane label="历史" name="history" />
</el-tabs>
</div>
<div v-if="activeTab === 'live'" class="card-toolbar">…</div>
<el-table size="small" class="table-scroll--md">…</el-table>
<div class="card-pager">…</div>
</AppCard>
8.6 新页面 Checklist
- 将 §8.9 复制为项目全局样式文件,按 §6.0 顺序在
main.ts中引入 - 复制 §8.1 壳层布局、§8.3 公共组件(文件名自定)
- 页面根节点
<div class="page"> PageHeader(标题 + 描述 + actions)- 主内容
AppCard;表单页加padded - 列表筛选用
.card-toolbar,分页用.card-pager - 间距与颜色仅使用 CSS 变量与全局类
- 在主布局中注册导航项与图标(§5.3)
8.7 反模式
| 不要 | 应该 |
|---|---|
| 深色侧栏 | 浅色半透明侧栏 |
页面内裸 el-card 作主容器 |
AppCard |
| 无标题直接上表格 | 先 PageHeader |
| 硬编码色值 | CSS 变量 |
| 重阴影 | 细边框 |
每页自定义 .toolbar / .pager |
.card-toolbar / .card-pager |
表单卡片不加 padded |
AppCard padded |
style="margin-top: 12px" |
.u-mt-3 等工具类 |
| 非 8px 倍数的随意间距 | 间距 Token |
| 在 EP 样式之前引入全局样式 | 先 element-plus/dist/index.css,再引入 §8.9 全局样式 |
8.8 推荐目录结构(示例)
styles/global.css ← 由本文档 §8.9 复制生成,路径与文件名自定
components/PageHeader.vue
components/StatCard.vue
components/AppCard.vue
layouts/MainLayout.vue ← 见 §8.1,文件名自定
views/Login.vue ← 见 §8.2,文件名自定
样式变更时,须同时更新本文档 §6.1–6.14 与 §8.9 两处 CSS,保持内容一致。
8.9 完整全局样式(一键复制)
以下为设计规范定义的完整全局 CSS,直接复制到项目全局样式文件即可(§6.1–6.14 为分模块对照版,内容须与本节一致)。
:root {
--background: #f4f4f5;
--foreground: #18181b;
--card: #ffffff;
--card-foreground: #18181b;
--primary: #0f7a62;
--primary-hover: #0d6b56;
--primary-foreground: #fafafa;
--muted: #f4f4f5;
--muted-foreground: #71717a;
--border: #e4e4e7;
--input: #e4e4e7;
--destructive: #dc2626;
--success: #047857;
--warning: #b45309;
--danger: #dc2626;
--info: #a1a1aa;
--sidebar: rgba(250, 250, 250, 0.85);
--sidebar-foreground: #18181b;
--sidebar-accent: #f4f4f5;
--sidebar-border: #e4e4e7;
--sidebar-width: 15rem;
--radius: 0.5rem;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--el-color-primary: #0f7a62;
--el-color-primary-light-3: #4da08c;
--el-color-primary-light-5: #76b8a8;
--el-color-primary-light-7: #a0d0c4;
--el-color-primary-light-8: #b8dcd2;
--el-color-primary-light-9: #d0e8e1;
--el-color-primary-dark-2: #0c624e;
--el-color-success: #047857;
--el-color-warning: #b45309;
--el-color-danger: #dc2626;
--el-color-info: #a1a1aa;
--el-border-radius-base: 0.5rem;
--el-border-color: #e4e4e7;
--el-bg-color-page: #f4f4f5;
--el-text-color-primary: #18181b;
--el-text-color-regular: #3f3f46;
--el-text-color-secondary: #71717a;
--el-font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
}
* { box-sizing: border-box; }
html, body, #app {
margin: 0;
padding: 0;
height: 100%;
font-size: 14px;
font-family: var(--el-font-family);
background: var(--background);
color: var(--foreground);
}
.layout {
display: flex;
height: 100vh;
}
.sidebar {
width: var(--sidebar-width);
flex-shrink: 0;
background: var(--sidebar);
border-right: 1px solid var(--sidebar-border);
backdrop-filter: blur(8px);
display: flex;
flex-direction: column;
overflow-y: auto;
}
.sidebar-brand {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-5) var(--space-4);
}
.brand-icon {
width: 32px;
height: 32px;
border-radius: var(--radius);
background: var(--primary);
color: var(--primary-foreground);
display: flex;
align-items: center;
justify-content: center;
}
.brand-name {
font-size: 1.125rem;
font-weight: 600;
}
.nav-group-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted-foreground);
padding: var(--space-4) var(--space-4) var(--space-2);
}
.nav-item {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
margin: 0 var(--space-2);
border-radius: var(--radius);
color: var(--muted-foreground);
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
}
.nav-item:hover { background: var(--sidebar-accent); }
.nav-item.active {
background: var(--sidebar-accent);
color: var(--primary);
font-weight: 600;
}
.main-area {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.topbar {
height: 52px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--space-6);
background: var(--card);
border-bottom: 1px solid var(--border);
}
.topbar-title {
font-size: 0.875rem;
color: var(--muted-foreground);
}
.main-content {
flex: 1;
overflow-y: auto;
padding: var(--space-6);
}
.page {
max-width: 1400px;
}
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: var(--space-6);
gap: var(--space-4);
}
.page-actions {
display: flex;
align-items: center;
gap: var(--space-2);
flex-shrink: 0;
flex-wrap: wrap;
}
.page-title {
font-size: 1.5rem;
font-weight: 600;
letter-spacing: -0.02em;
margin: 0 0 var(--space-1);
}
.page-desc {
font-size: 0.875rem;
color: var(--muted-foreground);
margin: 0;
}
.stat-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-4);
margin-bottom: var(--space-6);
}
.stat-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--space-5);
}
.stat-card__label {
font-size: 0.875rem;
color: var(--muted-foreground);
margin-bottom: var(--space-2);
}
.stat-card__value {
font-size: 1.75rem;
font-weight: 600;
}
.app-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: var(--space-4);
}
.page > .app-card:last-child,
.page > .content-grid:last-child {
margin-bottom: 0;
}
.app-card__header {
padding: var(--space-4) var(--space-5);
border-bottom: 1px solid var(--border);
}
.app-card__title {
font-size: 0.9375rem;
font-weight: 600;
margin: 0;
}
.app-card__body--padded { padding: var(--space-5); }
.content-grid {
display: grid;
gap: var(--space-4);
margin-bottom: var(--space-6);
}
.content-grid--2-1 { grid-template-columns: 1.4fr 1fr; }
.content-grid--2 { grid-template-columns: 1fr 1fr; }
.content-grid--3 { grid-template-columns: repeat(3, 1fr); }
.card-toolbar {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-4) var(--space-5) 0;
margin-bottom: var(--space-4);
flex-wrap: wrap;
}
.card-toolbar--desc {
align-items: flex-start;
}
.card-pager {
padding: var(--space-4) var(--space-5);
display: flex;
justify-content: flex-end;
}
.card-footer-hint {
padding: 0 var(--space-5) var(--space-3);
margin: 0;
}
.card-footer-hint + .card-pager {
padding-top: var(--space-3);
}
.card-inset-block {
margin: 0 var(--space-5) var(--space-4);
padding: var(--space-3);
background: var(--muted);
border-radius: var(--radius);
font-size: 0.875rem;
}
.tabs-bar {
padding: 0 var(--space-5);
border-bottom: 1px solid var(--border);
}
.tabs-bar .el-tabs__header { margin-bottom: 0; }
.page-toolbar {
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-4);
flex-wrap: wrap;
}
.inline-actions {
display: flex;
align-items: center;
gap: var(--space-2);
flex-wrap: wrap;
}
.card-header-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
}
.form-hint {
margin-top: 6px;
font-size: 0.75rem;
color: var(--muted-foreground);
}
.form-hint--flush { margin-top: 0; }
.u-mt-3 { margin-top: var(--space-3); }
.u-mt-4 { margin-top: var(--space-4); }
.u-mb-3 { margin-bottom: var(--space-3); }
.u-mb-4 { margin-bottom: var(--space-4); }
.u-mb-6 { margin-bottom: var(--space-6); }
.text-center { text-align: center; }
.w-full { width: 100%; }
code.inline-code {
font-size: 0.75rem;
padding: 0 var(--space-1);
background: var(--muted);
border-radius: var(--radius);
}
.section-title {
font-size: 0.875rem;
font-weight: 600;
margin-bottom: var(--space-4);
}
.section-title:not(:first-child) { margin-top: var(--space-6); }
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: var(--background);
padding: var(--space-6);
}
.login-wrap { max-width: 400px; width: 100%; text-align: center; }
.login-brand-icon {
width: 48px;
height: 48px;
border-radius: 12px;
background: var(--primary);
color: var(--primary-foreground);
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: var(--space-4);
}
.login-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--space-8);
margin-top: var(--space-6);
text-align: left;
}
.status-good { color: #047857; }
.status-warn { color: #b45309; }
.status-bad { color: #dc2626; }
.status-muted { color: var(--muted-foreground); }
.toolbar-select { width: 120px; }
.toolbar-select--country { width: 160px; }
.toolbar-select--wide { width: 280px; }
.toolbar-input { width: 200px; }
.toolbar-input--wide { width: 260px; }
.toolbar-input--grow { flex: 1; min-width: 200px; }
.table-cell-select { width: 100%; min-width: 200px; }
.el-form .el-select { width: 100%; }
.app-card .el-table {
--el-table-header-bg-color: var(--muted);
font-size: 0.875rem;
}
.app-card .el-table--small .el-table__cell {
padding: var(--space-2) var(--space-3);
}
.app-card .el-table--small th.el-table__cell {
font-size: 0.75rem;
font-weight: 600;
color: var(--muted-foreground);
}
.app-card .el-table--small td.el-table__cell {
color: var(--el-text-color-regular);
}
.app-card .el-table--small .el-table__row {
height: 40px;
}
.app-card .el-table .el-button.is-link {
padding: 0;
height: auto;
}
.app-card .el-table .cell {
line-height: 1.4;
}
.table-scroll--sm { max-height: 320px; }
.table-scroll--md { max-height: 360px; }
.table-scroll--lg { max-height: 480px; }
.card-toolbar .toolbar-input--grow {
flex: 1;
min-width: 200px;
}
.card-pager .el-pagination {
font-size: 0.875rem;
}
.kv-block p {
margin: 0 0 var(--space-2);
}
.kv-block p:last-child {
margin-bottom: 0;
}
.kv-block .label {
display: inline-block;
width: 5rem;
color: var(--muted-foreground);
}
.kv-block code {
font-size: 0.8125rem;
}
.cell-stack {
display: flex;
flex-direction: column;
gap: var(--space-2);
width: 100%;
}
.meta-list {
margin: 0;
padding: 0;
list-style: none;
font-size: 0.8125rem;
color: var(--muted-foreground);
}
.meta-list li {
display: flex;
justify-content: space-between;
gap: var(--space-3);
padding: var(--space-1) 0;
}
.meta-list__mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
.meta-list__ok { color: var(--el-color-success); }
.meta-list__warn { color: var(--el-color-warning); }
.chart-box {
height: 280px;
}
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: #e4e4e7 transparent;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #e4e4e7;
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
@media (max-width: 1024px) {
.stat-grid { grid-template-columns: repeat(2, 1fr); }
.content-grid--2-1,
.content-grid--2,
.content-grid--3 { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
.stat-grid { grid-template-columns: 1fr; }
.main-content { padding: var(--space-4); }
.page-header { flex-direction: column; }
}
浅色后台管理系统 UI 设计规范