Files
Prism/docs/UI-DESIGN-SYSTEM.md
renjue fe8ea784d0
CI / docker (push) Successful in 2m54s
feat: Prism HTTP/SOCKS5 代理网关及管理台
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 15:58:28 +08:00

2014 lines
56 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 │ 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 : 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 | 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.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 字重与样式
| 样式 | 使用场景 | 禁止场景 |
|------|----------|----------|
| **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](#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` |
| 状态 / 布尔 | `80100px` |
| 操作 | `120160px``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 | `480520px` | `destroy-on-close` |
| 导入/中等表单 | `560640px` | 同上 |
| 复杂列表弹窗 | `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(默认 32pxlarge 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 → 项目自有 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
```
```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.16.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 设计规范*