# UI 设计规范

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

---

## 目录

1. [页面整体风格](#1-页面整体风格)
2. [布局标准](#2-布局标准)
3. [字体标准](#3-字体标准)
4. [颜色规范](#4-颜色规范)
5. [图标使用规范](#5-图标使用规范)
6. [组件与页面标准样式](#6-组件与页面标准样式)
7. [交互原则](#7-交互原则)
8. [附录：复刻指南](#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` |
| **组件复用优先** | 使用 `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](#616-图表规范echarts) 配色与 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](#89-完整全局样式一键复制)**。两处内容须保持一致。

### 6.0 技术栈与引入

| 项目 | 要求 |
|------|------|
| Vue | `^3.4` |
| Element Plus | `^2.4`（2.x 均可，须使用 CSS 变量主题） |
| 图标 | `@element-plus/icons-vue` |
| 图表（可选） | `echarts` `^5` |

**`main.ts` 引入顺序（顺序不可颠倒）：**

```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 与基础重置

```css
: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 布局壳层（侧栏 / 顶栏 / 主内容）

```css
.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 页面头与网格

```css
.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 卡片与工具栏

```css
.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 表单、间距工具类

```css
.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 登录页

```css
.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 表格

```css
.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 工具栏控件宽度

```css
.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 状态色与辅助块

```css
.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 滚动条与响应式

```css
.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 推荐结构：**

```vue
<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 推荐结构：**

```vue
<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` |

```ts
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
```

```vue
<!-- 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 登录页完整模板

```vue
<!-- 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 公共组件完整源码

```vue
<!-- 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>
```

```vue
<!-- 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>
```

```vue
<!-- 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 标准页面模板

```vue
<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 的列表页

```vue
<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.1–6.14** 与 **§8.9** 两处 CSS，保持内容一致。

### 8.9 完整全局样式（一键复制）

以下为**设计规范定义的完整全局 CSS**，直接复制到项目全局样式文件即可（§6.1–6.14 为分模块对照版，内容须与本节一致）。

```css
: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 设计规范*
