# 06 — 主题系统与视觉设计 > 参考:vue-vben-admin(HSL Token + Tailwind 桥接 + CSS layer + EP 适配器)、soybean-admin(单一 ThemeSetting 类型 + 三端同步)、vue-pure-admin(EP↔Tailwind v4 变量桥接)、ant-design-pro(antd token + data-theme);补充:gin-vue-admin(EP SCSS 定制三层法) --- ## 一、设计 Token:单一事实源 ### 1.1 Token 分层模型 ``` 语义层(唯一手写处) --background --foreground --primary --card --border --radius ... ↓ 派生(工具类桥接) Tailwind 层 bg-background text-foreground(@theme 映射) ↓ 派生(适配器) 组件库层 --el-color-primary / antd token —— 从语义层变量计算得出 ↓ 派生(算法) 色阶层 primary-50 … primary-950 / light-3 … light-9 ``` **铁律:语义 Token 是唯一手写源,其余全部派生。** 任何地方再出现硬编码色值(`#409eff`、`rgb(64,158,255)`)都是违规。 ### 1.2 vben 的 Token 定义(shadcn 范式) ```css /* vben: packages/@core/base/design/src/design-tokens/default.css */ :root { --background: 0 0% 100%; /* HSL 三元组,不带 hsl() 包装 */ --primary: 211 91% 39%; --border: 214 32% 91%; --radius: 0.5rem; } .dark { /* 整组覆盖,暗色即换一套值 */ --background: 224 71% 4%; --primary: 210 98% 56%; } ``` 两个细节值千金: 1. **裸 HSL 值不带 `hsl()` 包装**——这样 `hsl(var(--primary) / 0.5)` 可直接拼透明度(soybean 同款:RGB 拆三段 `r g b` 供 `rgb(var(--x) / 0.5)`) 2. **暗色 = `.dark` class 整组覆盖**,而非媒体查询——用户偏好优先于系统 ### 1.3 Tailwind v4 桥接(vben) ```css /* internal/tailwind-config/src/theme.css */ @custom-variant dark (&:is(.dark *)); /* class 策略暗色 */ @theme inline { --color-background: hsl(var(--background)); /* 语义 Token → 工具类 */ --color-primary: hsl(var(--primary)); } @layer properties, theme, base, components, utilities; /* 级联层声明 */ ``` **CSS layer 声明是组件库共存的关键**:把组件库样式压在低层,工具类天然可覆盖,避免 `!important` 军备竞赛。 ### 1.4 pure 的 EP↔Tailwind 桥接与 soybean 的三端同步(★★★★☆) **EP 工程引入原子类的最短路径(pure):** ```css /* vue-pure-admin: src/style/tailwind.css —— Tailwind v4 CSS-first 写法 */ @custom-variant dark (&:is(.dark *)); /* class 策略暗色 */ @theme { --color-bg_color: var(--el-bg-color); /* EP 变量 → 原子色 token */ --color-primary: var(--el-color-primary); /* 于是 bg-bg_color / text-primary 与组件库同色 */ } ``` soybean 则是「单一强类型配置对象」路线:`themeSettings`(`App.Theme.ThemeSetting` 一份 schema 即文档)驱动 **CSS 变量 + NaiveUI GlobalThemeOverrides + UnoCSS theme 三端同步**,且用 `BUILD_TIME` 作覆盖标志——**发版自动淘汰用户本地旧主题缓存**(防「新版本旧皮肤」)。两案共同点:换肤 = 改上游变量,组件库与原子类自动跟随。 --- ## 二、暗色模式 ### 2.1 实现要点(三标杆殊途同归) | 环节 | 做法 | 反面 | |------|------|------| | 切换机制 | html 挂 class(vben/pure `.dark`)或 `data-theme="dark"`(adp) | `prefers-color-scheme` 媒体查询直连(用户无法覆盖系统) | | 跟随系统 | watch `prefers-color-scheme` 作为 **auto 模式初值**,用户手动选择后以用户为准 | — | | 持久化 | localStorage 单 key;**读取要在 app 挂载前**(防白闪,必要时内联脚本) | 挂载后再初始化(首屏闪白) | | 组件库 | EP:引 `dark/css-vars.css` 一行;antd:token 切换 | 自写暗色样式覆盖组件库 | 三态标准:`light | dark | auto`(auto 跟随系统)。附加无障碍模式(可选):灰度 / 色弱滤镜(`html-grey` / `html-weakness`,一行 filter + class 切换;值班大屏场景有真实需求)。 **暗色的分层策略与浅色不同**:阴影在深底上几乎不可见——暗色模式靠**边框 + 表面亮度差**分层(卡片 = 页面底色提亮一档),不要只调阴影。 ### 2.2 双端 key 一致(迁移期,必须) 同产品双栈并存的工程,暗色 key **必须**两端同名(推荐 `.theme.mode`),切换机制统一(class + data-theme 双挂成本最低)。key 不一致 = 同浏览器两端口偏好不互通 + 迁移时暗色选择器要全量改。 --- ## 三、组件库主题定制(Element Plus) 标杆做法(vben):语义 Token **派生**组件库变量——`useElementPlusDesignTokens` 监听主题变化,把语义 Token 实时映射为 `--el-color-primary` 全系(暗色下自动反转色阶方向)。**换肤不碰组件库,只改上游 Token。** 补充范式(gva 的 EP 专属三层法): ``` 第 1 层 编译期:SCSS 定制色板 @forward 'element-plus/theme-chalk/src/common/var.scss' with ($colors: ('primary': ('base': #409EFF), ...)); 第 2 层 运行时:JS 生成 --el-color-primary 及 light-1..10 / dark-1..2 / -bg 色阶写入 documentElement —— 换主色不重新编译 (gva setBodyPrimaryColor:light/dark 两套明度算法) 第 3 层 暗色变量:element-plus/theme-chalk/dark/css-vars.css + html.dark { --el-bg-color: ... } 局部覆盖 ``` pure 的 `setEpThemeColor()`(`useDataThemeChange.ts`)是第 2 层的更简实现:`documentElement.style.setProperty` + 循环生成 light-1~9/dark-1~2,且暗色下**色阶方向反转**。品牌主色运行时换肤(站点名/Logo/主色/favicon 联动)是底座「品牌可配」能力的核心——主色、圆角(`--el-border-radius-base`)、基础字号纳入同一变量体系。 --- ## 四、视觉执行规范(写出好看代码的数值层) > 本章前三节解决「主题怎么搭」,本节解决「页面怎么写才好看」。所有数值**必须**落为 CSS 变量后使用——AI/开发者写页面时只允许引用变量,不允许现场发明数值。 ### 4.1 品牌可配项 | 配置项 | 生效点 | 存储 | |--------|--------|------| | 站点名称 | 侧栏 logo 文案 / document.title 后缀 | 后端配置 | | Logo | 侧栏 + favicon | 后端配置 | | 主色 | `--el-color-primary` 系列运行时混色 | 后端配置或用户偏好 | | 暗色 | 用户偏好 | localStorage(统一命名空间) | pure 的补充范式:**运行时平台配置**(`public/platform-config.json`,Title/Layout/Theme/DarkMode 等 30+ 项,启动拉取注入)——改配置不发版(详见 08 章 env 分层)。 ### 4.2 基础 Token 全表(新工程第一天就建) ```css :root { /* —— 背景层次(浅色值示例;暗色整组覆盖,靠亮度差分层) —— */ --bg-page: #f5f6f8; /* 页面底 */ --bg-card: #ffffff; /* 卡片 / 弹层 */ --bg-hover: rgba(0, 0, 0, 0.04); /* 行/项 hover */ --bg-muted: #f5f7fa; /* 次级区块 / 代码块 */ /* —— 文字层次(四档灰,禁用两档灰打天下) —— */ --text-primary: rgba(0, 0, 0, 0.88); /* 正文 */ --text-secondary: rgba(0, 0, 0, 0.65); /* 次要说明、表头 */ --text-placeholder: rgba(0, 0, 0, 0.38); /* 占位、禁用标签 */ --text-disabled: rgba(0, 0, 0, 0.25); --text-inverse: #ffffff; /* 深底上的文字 */ /* —— 边框(三级) —— */ --border-strong: rgba(0, 0, 0, 0.26); /* 输入框 */ --border-base: rgba(0, 0, 0, 0.15); /* 卡片描边 */ --border-light: rgba(0, 0, 0, 0.08); /* 分割线 */ /* —— 语义色(对齐组件库 success/warning/danger/info) —— */ /* —— 间距(4px 基数,全站只用这六档) —— */ --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-6: 24px; --space-8: 32px; /* —— 圆角(三档) —— */ --radius-sm: 4px; /* 标签、小按钮 */ --radius: 8px; /* 卡片、按钮、输入框(主值) */ --radius-lg: 12px; /* 弹窗、大图 */ /* —— 阴影(卡片最多 2 级;弹层用组件库默认) —— */ --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.06); --shadow-2: 0 4px 12px rgba(0, 0, 0, 0.08); /* —— 层级(防 z-index 军备竞赛) —— */ --z-dropdown: 100; --z-drawer: 200; --z-modal: 300; --z-message: 400; } ``` 项目可从组件库变量派生(EP:`--bg-card: var(--el-bg-color)`),重点是**层次齐全、命名统一**。卡片风格全站二选一:**描边(border-base)或阴影(shadow-1)**,不许混用。 ### 4.3 字体与排版 | 用途 | 字号 / 行高 | 字重 | |------|------------|------| | 页面标题 | 18-20 / 28 | 600 | | 卡片标题 | 16 / 24 | 600 | | 正文 / 表格 | 14 / 22 | 400 | | 次要说明 | 13 / 20 | 400 | | 辅助 / 标签 | 12 / 20 | 400 | 规则:**正文最小 12px**(再小不可读);同一页面标题层级 ≤2 级;数字用 `font-variant-numeric: tabular-nums`;中英文与数字混排加空格(「共 12 条」);长文本省略必须带 title 提示;字体栈取系统栈 + 组件库默认,**禁止**引入额外 webfont 除非品牌需要。 ### 4.4 图标规范 - **一套图标集**(线性或面性统一,**禁止**混搭两套风格;推荐 Iconify 单集或组件库图标) - 尺寸四档:12(辅助)/ 16(按钮、菜单、表格行内 **主值**)/ 20(页头)/ 24(空态插图位) - 图标与文字并排间距 6px;按钮内图标与文字同色同级 - 纯图标按钮**必须**带 tooltip 与 aria-label ### 4.5 过渡与微交互 | 场景 | 时长 / 缓动 | |------|-------------| | 颜色 / hover 变化 | 150ms ease | | 展开 / 抽屉 / 弹层 | 200-300ms `cubic-bezier(0.4, 0, 0.2, 1)` | | 页面切换 | fade 150ms | 规则:同类交互全站**同时长同缓动**;**禁止**超过 400ms 的阻塞动画;按钮提交必须有 loading 态(防重复触发);自定义交互组件四态齐全——**hover / active / focus-visible / disabled** 缺一不可(focus 可见性是可访问性底线)。 ### 4.6 对齐与栅格 - 表格:数字列右对齐 + tabular-nums,文本列左对齐,**居中对齐只用于状态标签/操作列**(全列居中是典型反模式) - 表单:标签右对齐或上对齐全站统一一档;输入宽度按内容语义定(手机号 11 位就别 100% 宽) - 卡片间距统一 `--space-4`(16px);页面内边距 `--space-6`(24px) --- ## 五、好看反模式清单(出现即改) 1. 硬编码色值(`#333`、`rgba(0,0,0,.5)` 散落模板)——不走 Token 2. 一页混用多种圆角 / 多种阴影 / 非 4px 基数间距(8 和 10 混用) 3. 全列居中对齐的表格(数字该右对齐、文本该左对齐) 4. 图标混用两套风格(线性 + 面性、单色 + 彩色) 5. 一页多个 primary 按钮(主操作**只能有一个**,其余 default/text) 6. 只有两种灰的文字层次(该用四档文字 Token) 7. 弹窗套弹窗再套抽屉(层级超过两层改抽屉分流) 8. 满屏成功 toast(见 07 章反馈分级:成功静默) 9. 骨架屏与真实内容尺寸不符(loading 结束页面跳动) 10. 表格列被挤压换行(缺 min-width / 未开横向滚动) 11. 危险操作无确认且无 danger 语义色 12. 空页面白板(empty 必须带引导动作) 13. 阴影当装饰乱叠(卡片最多 shadow-2,层级感来自留白而非阴影堆叠) 14. 暗色模式直接套浅色阴影(暗色靠边框 + 亮度差分层) --- ## 六、死 CSS 治理 存量工程常见病灶:styles/ 下积累多代无入口引用的样式文件(曾有工程沉淀约 2000 行死 CSS,且「设计系统」存在 v1/v2 两代并存——换了两茬样式系统、旧的没删的考古层)。 ### 6.1 治理方案(四步) ``` 第 1 步 验死:对每个文件跑全仓引用检查(import/@import/url()), 确认零引用后直接删除(git 里有历史,不建备份目录) 第 2 步 收敛:保留三件套 = variables(语义 Token)+ 栅格 + 基础元素; 组件样式回归 scoped / 组件库 第 3 步 立规:CI 检查 styles/ 下每个 .css 必须被入口链引用, 新文件无引用直接 fail 第 4 步 单源:设计规范类文档只留一份(docs),代码里不留第二套实现 ``` **规则**:CSS 文件必须有入口引用链,如同 JS 文件必须有 import——「无引用样式」=「无主代码」,删除是唯一正确处理。 --- ## 七、样式写法优先级(adp 明文范式,直接抄用) ``` 1. 组件库自带能力(EP 的 type/size/token props) ← 优先 2. 语义工具类 / Tailwind 原子类(布局、间距) ← 其次 3. scoped 样式 + CSS 变量(主题相关) ← 业务组件 4. 深度选择器 :deep()(覆盖组件库,须注释原因) ← 最后手段 5. 全局覆盖(唯一收口文件,如 element 覆盖表) ← 禁散落 ``` gva 的补充范式:语义化页面类(`gva-search-box / gva-table-box / gva-btn-list`)。与「页面容器 + 列表组件」的组件路线二选一——**推荐组件路线**(类型提示与 props 约束更强),不要并行引入两套体系。 --- ## 八、最佳实践清单 ✅ **要做** 1. 新颜色/间距/圆角一律引用 Token 变量,组件里禁止现场发明数值 2. 透明度拼接用 `hsl(var(--x) / 0.5)` 裸值模式 3. 暗色切换在 app 挂载前完成(必要时 index.html 内联首帧脚本) 4. 图表颜色从主题变量读取(初始化注入当前主题色板,暗色切换重建实例) 5. 新全局覆盖样式集中一个文件并注释原因 ❌ **不要** 1. 不要保留 v1/v2 并存的设计系统文件 2. 不要用 `!important` 解决覆盖问题(用 CSS layer 或提高选择器语义) 3. 不要在组件里写 `@media (prefers-color-scheme)` 直连暗色(绕过用户选择) 4. 不要硬编码暗色背景色值(读组件库 bg 变量) 5. 不要双端各存一套主题 key(迁移期统一命名) --- ## 九、AI 检查清单(对比目标工程逐项核对) **P0(结构)** 1. styles/ 下是否有零入口引用的样式文件?列出并标记删除(统计行数) 2. 是否存在硬编码色值 / 非 Token 间距圆角出现在模板与 scoped 样式?抽样列出 3. 是否有多套并行样式系统 / 两代设计系统文件并存? **P1(视觉执行)** 4. 基础 Token 全表(背景四档/文字四档/边框三级/间距六档/圆角三档)是否建立且被引用? 5. 排版抽检:正文 <12px?数字列未右对齐/未 tabular-nums?长文本省略无 title? 6. 图标抽检:是否混用两套风格?纯图标按钮是否缺 tooltip/aria-label? 7. 过渡抽检:同类交互时长是否统一?提交按钮是否都有 loading?四态是否齐全? 8. 对照「好看反模式清单」14 条逐条扫描,命中项列出 9. 暗色抽检:分层是否靠边框/亮度差?图表色板是否随主题重建? **P2(体验升级)** 10. 对比度达标:正文 ≥ 4.5:1、大字 ≥ 3:1(WCAG AA)——用工具抽查文字层次 Token 11. Tailwind 是否与组件库变量打通(pure `@theme` 桥接)? 12. 是否需要 BUILD_TIME 发版覆盖用户旧主题缓存(soybean 方案)? 13. 灰度/色弱无障碍模式是否值得启用(值班场景)? --- > 下一篇:[07-组件设计与交互反馈](07-组件设计与交互反馈.md) —— 组件是设计的词汇表。