Files
Prism/UI-DESIGN-SYSTEM.md
T
renjue 2780eaf30c
CI / test (push) Successful in 1m55s
CI / docker (push) Failing after 24s
feat: Prism HTTP/SOCKS5 代理网关及管理台
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 15:11:50 +08:00

56 KiB
Raw Blame History

UI 设计规范

适用范围:基于 Vue 3 + Element Plus 2.x 的后台管理系统 Web 页面
本文档为唯一设计规范:色彩、间距、布局、组件样式、全局 CSS 均定义于本文件(§6 分模块说明 + §8.9 完整 CSS)。项目实施时从本文档复制代码即可,不依赖其他设计资产文件。


目录

  1. 页面整体风格
  2. 布局标准
  3. 字体标准
  4. 颜色规范
  5. 图标使用规范
  6. 组件与页面标准样式
  7. 交互原则
  8. 附录:复刻指南

1. 页面整体风格

1.1 设计定位

本规范定义一套浅色、克制、数据导向的后台界面风格。视觉语言强调可读性与操作效率,避免装饰性元素喧宾夺主。

维度 规范
整体调性 专业、冷静、工具感;类似现代 SaaS 控制台
色彩策略 大面积中性灰白底 + 青绿品牌色点缀
层次表达 字号阶梯灰度阶梯区分,不靠重阴影
容器形态 白底细边框卡片(1px + 8px 圆角),无浮起阴影
信息密度 列表/表格偏紧凑(size="small"),表单区留白充分
技术底座 Element Plus 组件 + 全局 CSS 变量覆盖

1.2 核心设计原则

原则 说明
浅色优先 页面背景 #f4f4f5,内容区白卡片;禁止深色全屏侧栏
克制用色 主色青绿仅用于品牌、主按钮、导航激活态、关键正向状态
细边框卡片 白底 + 1px 浅灰边框 + 8px 圆角,禁止大面积 box-shadow
8px 间距节奏 所有间距使用间距 Token(4px 仅用于极紧凑场景)
层次靠字号与灰度 标题 #18181b,正文 #3f3f46,辅助 #71717a
组件复用优先 使用 PageHeaderAppCardStatCard 等标准壳层,禁止页面内自建容器体系
国际化友好 文案走 i18n;布局预留中英文长度差异(按钮组 flex-wrap

1.3 视觉示意

┌──────────────┬────────────────────────────────────┐
│   Sidebar    │  Topbar52px,白底,底部分割线)    │
│   240px      ├────────────────────────────────────┤
│   半透明浅灰  │  Main Contentpadding 24px        │
│   品牌区+导航  │  .pagemax-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 : 1fr1fr : 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-hintmargin-top 6px 紧贴表单项控件底部,视觉上等同 8px 体系

--primary-hover 已定义但未写独立 CSS 规则,主按钮 hover 由 Element Plus 通过 --el-color-primary-dark-2 自动处理。

2.3 布局组件尺寸

组件 尺寸 其他规则
侧栏 .sidebar 240px15rem 右边框 1px;背景半透明 + backdrop-filter: blur(8px)
侧栏品牌区 .sidebar-brand 高随内容;padding 20px 16px 品牌图标 32×32px
导航项 .nav-item 高随内容;padding 8px 12pxmargin 左右 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: 14px1rem = 14px)。所有字号使用 rem 相对根字号。

层级 类名 / 场景 字号 字重 行高 颜色
页面标题 .page-title 1.5rem21px 600 默认 --foreground
登录品牌标题 .page-title(登录页) 同上 600 默认 --foreground
卡片标题 .app-card__title 0.9375rem13px 600 默认 --foreground
品牌名 .brand-name 1.125rem15.75px 600 默认 --foreground
统计数值 .stat-card__value 1.75rem24.5px 600 默认 --foreground
正文 / 导航 / 顶栏 .nav-item, .topbar-title, 表格正文 0.875rem12.25px 400500 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.75rem10.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 字重与样式

样式 使用场景 禁止场景
600Semibold 页面标题、卡片标题、表头、导航激活态、分组标题 .section-title 大段正文
500Medium 导航默认态、顶栏次要文字
400Regular 正文、描述、表单标签
加粗 <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.5rem8px 卡片、按钮、输入框、导航项、内嵌块
登录品牌图标 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 线型)。下表为常见导航/功能场景的推荐映射,各项目可按业务替换,但须保持风格一致。

场景 推荐图标 含义
首页 / 仪表盘 OdometerHomeFilled 总览、监控
列表 / 规则 / 配置项 ListMenu 条目化管理
连接 / 网络 ConnectionShare 链路、拓扑
外部关联 / 链接 Link 外部数据源
切换 / 路由 / 策略 Switch 分流、调度
日志 / 文档 DocumentTickets 记录、流水
用户 / 权限 UserLock 身份、安全
系统设置 Setting 全局配置
新建 Plus 创建资源
编辑 Edit 修改
删除 Delete 移除(配合 danger 按钮,不单用图标)
刷新 Refresh 重新加载
导入 / 导出 UploadDownload 数据交换

5.4 使用原则

原则 说明
导航必带图标 侧栏每项左侧固定 16px 图标,增强扫读性
颜色跟随文字 图标 currentColor,不单独设色
禁止混用风格 统一 Outlined 线型图标(Element Plus Icons
操作按钮 主操作优先文字按钮;图标-only 按钮需 aria-label
品牌图标不外借 Logo 仅用于品牌区,不用于普通操作按钮

6. 组件与页面标准样式

本节 6.16.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.cssapp.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
状态 / 布尔 80100px
操作 120160pxfixed="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 480520px destroy-on-close
导入/中等表单 560640px 同上
复杂列表弹窗 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-boxheight: 280px
主系列色 #0f7a62var(--primary)
次系列色 #71717avar(--muted-foreground)
第三系列及以后 #4da08c#76b8a8#a0d0c4primary-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
页内标题 PageHeadertitle 为页面 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(默认 32pxlarge 40px
错误信息 接口错误映射为用户可读的本地化文案(如限流、权限不足)

7.7 国际化

原则 规范
文案 所有可见文字走国际化方案(如 i18n),禁止硬编码用户可见字符串
时间 显示格式 YYYY-MM-DD HH:mm:ssISO 替换 T 并截断)
布局弹性 按钮组、工具栏 flex-wrap: wrap 适配长文案

8. 附录:复刻指南

按以下顺序实施,可在新项目中复刻本文档定义的全部视觉(Vue 3 + Element Plus 2.x)。

8.1 壳层布局完整模板

DOM 层级(class 名称不可改):

.layout
├── .sidebar.custom-scrollbar
│   ├── .sidebar-brand
│   │   ├── .brand-icon → 项目自有 Logo20px
│   │   └── .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-icon20px)与 .login-brand-icon24px)容器即可;须使用 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

  1. 将 §8.9 复制为项目全局样式文件,按 §6.0 顺序在 main.ts 中引入
  2. 复制 §8.1 壳层布局、§8.3 公共组件(文件名自定)
  3. 页面根节点 <div class="page">
  4. PageHeader(标题 + 描述 + actions
  5. 主内容 AppCard;表单页加 padded
  6. 列表筛选用 .card-toolbar,分页用 .card-pager
  7. 间距与颜色仅使用 CSS 变量与全局类
  8. 在主布局中注册导航项与图标(§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.16.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 设计规范