# 主题系统
**本文引用的文件**
- [fms-vue/src/theme/index.js](file://fms-vue/src/theme/index.js)
- [fms-vue/src/theme/tokens.css](file://fms-vue/src/theme/tokens.css)
- [fms-vue/src/theme/presets.js](file://fms-vue/src/theme/presets.js)
- [fms-vue/src/stores/app.js](file://fms-vue/src/stores/app.js)
- [fms-vue/src/components/ui/button/Button.vue](file://fms-vue/src/components/ui/button/Button.vue)
- [fms-vue/src/components/ui/input/Input.vue](file://fms-vue/src/components/ui/input/Input.vue)
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件为 FMS 前端主题系统的完整技术文档,围绕 CSS 变量体系、主题 token 管理、预设主题配置、深浅色模式切换与状态持久化、主题自定义扩展方式、组件集成策略、性能优化以及测试与兼容性处理进行系统化说明。目标是帮助开发者快速理解并安全地扩展主题能力,同时保证所有 UI 组件在主题切换时表现一致且稳定。
## 项目结构
FMS 的主题系统位于 fms-vue 子项目中,采用“CSS 变量 + JS 运行时写入 + Store 驱动”的轻量方案:
- tokens.css:集中定义设计令牌(Design Tokens),浅色在 :root,深色通过 .dark 覆盖同一批变量;主色 --fms-primary 由 JS 动态写入,衍生色使用 color-mix 派生。
- index.js:提供 applyPrimaryColor 与 applyDarkMode 两个纯函数,操作 上的 class 与 CSS 变量。
- presets.js:集中维护预设主色列表与默认主色。
- stores/app.js:Pinia store 负责主题状态(darkMode、primaryColor)及其持久化,并通过 watch 将状态同步到 DOM。
- 组件层:各 UI 组件通过 scoped SCSS 引用统一样式,间接消费 tokens.css 中的变量,实现主题自适应。
```mermaid
graph TB
A["应用入口
初始化 Pinia"] --> B["Store: app.js
darkMode / primaryColor"]
B --> C["主题同步器
initThemeSync()"]
C --> D["DOM 写入
applyDarkMode()"]
C --> E["CSS 变量写入
applyPrimaryColor()"]
E --> F["tokens.css
:root 与 .dark 变量"]
G["UI 组件
Button / Input 等"] --> F
```
图表来源
- [fms-vue/src/stores/app.js:104-122](file://fms-vue/src/stores/app.js#L104-L122)
- [fms-vue/src/theme/index.js:1-16](file://fms-vue/src/theme/index.js#L1-L16)
- [fms-vue/src/theme/tokens.css:1-152](file://fms-vue/src/theme/tokens.css#L1-L152)
章节来源
- [fms-vue/src/theme/tokens.css:1-152](file://fms-vue/src/theme/tokens.css#L1-L152)
- [fms-vue/src/theme/index.js:1-16](file://fms-vue/src/theme/index.js#L1-L16)
- [fms-vue/src/theme/presets.js:1-16](file://fms-vue/src/theme/presets.js#L1-L16)
- [fms-vue/src/stores/app.js:1-123](file://fms-vue/src/stores/app.js#L1-L123)
## 核心组件
- 主题令牌(tokens.css)
- 以 CSS 变量形式集中管理背景、卡片、文字、边框、语义色、主色系列、字体、控件尺寸、阴影、浮层层级、弹窗/抽屉间距、分割线、栅格、树节点等。
- 浅色定义于 :root,深色通过 .dark 覆盖同一批变量;主色 --fms-primary 由 JS 注入,hover/active 等衍生色通过 color-mix 自动计算。
- 主题应用(index.js)
- applyPrimaryColor(color):向 根元素写入 --fms-primary。
- applyDarkMode(isDark):在 上切换 dark class。
- 预设主题(presets.js)
- 集中维护预设主色名称与色值,并提供默认主色常量。
- 状态与持久化(stores/app.js)
- 使用 Pinia 管理 darkMode 与 primaryColor,并通过 persist.pick 仅持久化主题与语言相关字段。
- initThemeSync(pinia) 在 store 激活后建立 watch,立即将状态同步到 DOM。
章节来源
- [fms-vue/src/theme/tokens.css:1-152](file://fms-vue/src/theme/tokens.css#L1-L152)
- [fms-vue/src/theme/index.js:1-16](file://fms-vue/src/theme/index.js#L1-L16)
- [fms-vue/src/theme/presets.js:1-16](file://fms-vue/src/theme/presets.js#L1-L16)
- [fms-vue/src/stores/app.js:1-123](file://fms-vue/src/stores/app.js#L1-L123)
## 架构总览
主题系统遵循“单一事实源 + 运行时最小干预”的设计原则:
- 单一事实源:tokens.css 是唯一主题真相,所有颜色、尺寸、阴影等均以 CSS 变量暴露。
- 运行时最小干预:JS 仅在必要时写入 --fms-primary 或切换 .dark class,避免频繁重排。
- 状态驱动:Pinia store 作为主题状态中心,watch 监听变化并推送到 DOM。
- 组件解耦:组件不直接感知主题逻辑,只消费 CSS 变量,天然支持深浅色与主色切换。
```mermaid
sequenceDiagram
participant U as "用户"
participant S as "Store : app.js"
participant W as "watch : initThemeSync"
participant T as "主题工具 : index.js"
participant D as "DOM : "
participant C as "CSS : tokens.css"
participant V as "组件 : Button/Input"
U->>S : 切换主题/主色
S-->>W : 状态变更
W->>T : applyDarkMode()/applyPrimaryColor()
T->>D : 写入 class / CSS 变量
D-->>C : 选择器匹配 ( : root/.dark)
C-->>V : 变量生效,组件样式更新
```
图表来源
- [fms-vue/src/stores/app.js:104-122](file://fms-vue/src/stores/app.js#L104-L122)
- [fms-vue/src/theme/index.js:1-16](file://fms-vue/src/theme/index.js#L1-L16)
- [fms-vue/src/theme/tokens.css:1-152](file://fms-vue/src/theme/tokens.css#L1-L152)
## 详细组件分析
### 主题令牌与深浅色机制(tokens.css)
- 变量分层清晰:中性色、语义色、主色系列、字体、控件尺寸、阴影、浮层层级、弹窗/抽屉、分割线、栅格、树节点等均有独立命名空间。
- 深浅色覆盖策略:.dark 下覆盖中性色、语义色、主色衍生方向(hover/active 提亮)、阴影强度,确保对比度与可读性。
- 主色派生:通过 color-mix 基于 --fms-primary 计算 hover/active,减少硬编码,提升可维护性。
```mermaid
flowchart TD
Start(["页面加载"]) --> Root[":root 定义浅色变量"]
Root --> CheckDark{"是否存在 .dark ?"}
CheckDark --> |是| Dark[".dark 覆盖变量"]
CheckDark --> |否| Light["保持浅色变量"]
Dark --> Apply["组件消费 CSS 变量"]
Light --> Apply
```
图表来源
- [fms-vue/src/theme/tokens.css:1-152](file://fms-vue/src/theme/tokens.css#L1-L152)
章节来源
- [fms-vue/src/theme/tokens.css:1-152](file://fms-vue/src/theme/tokens.css#L1-L152)
### 主题应用与状态同步(index.js + stores/app.js)
- 应用层职责分离:
- index.js 仅提供写 DOM 的纯函数,无副作用之外的逻辑。
- stores/app.js 负责状态管理与持久化,并通过 watch 驱动 DOM 更新。
- 持久化策略:仅持久化 darkMode、primaryColor、locale,避免无关状态污染。
- 即时同步:watch 使用 immediate: true,确保首次渲染即与应用状态一致。
```mermaid
classDiagram
class AppStore {
+darkMode : boolean
+primaryColor : string
+setDark(value)
+toggleDark()
+setPrimary(color)
}
class ThemeUtils {
+applyDarkMode(isDark)
+applyPrimaryColor(color)
}
AppStore --> ThemeUtils : "watch 调用"
```
图表来源
- [fms-vue/src/stores/app.js:1-123](file://fms-vue/src/stores/app.js#L1-L123)
- [fms-vue/src/theme/index.js:1-16](file://fms-vue/src/theme/index.js#L1-L16)
章节来源
- [fms-vue/src/stores/app.js:1-123](file://fms-vue/src/stores/app.js#L1-L123)
- [fms-vue/src/theme/index.js:1-16](file://fms-vue/src/theme/index.js#L1-L16)
### 预设主题配置(presets.js)
- 单源维护:所有预设主色集中在 presets.js,新增主题只需追加条目。
- 默认主色:DEFAULT_PRIMARY 与 store 初始值保持一致,确保首屏一致性。
章节来源
- [fms-vue/src/theme/presets.js:1-16](file://fms-vue/src/theme/presets.js#L1-L16)
### 组件集成与主题消费(Button.vue / Input.vue)
- 组件通过 scoped SCSS 引入样式,间接消费 tokens.css 中的 CSS 变量,无需感知主题逻辑。
- 按钮与输入框等基础组件均具备禁用态、聚焦态、状态提示等视觉反馈,这些状态均可通过 tokens.css 中的变量控制,从而自然适配深浅色与主色切换。
```mermaid
graph LR
T["tokens.css
CSS 变量"] --> B["Button.vue
scoped SCSS"]
T --> I["Input.vue
scoped SCSS"]
B --> U["用户界面"]
I --> U
```
图表来源
- [fms-vue/src/components/ui/button/Button.vue:1-69](file://fms-vue/src/components/ui/button/Button.vue#L1-L69)
- [fms-vue/src/components/ui/input/Input.vue:1-370](file://fms-vue/src/components/ui/input/Input.vue#L1-L370)
- [fms-vue/src/theme/tokens.css:1-152](file://fms-vue/src/theme/tokens.css#L1-L152)
章节来源
- [fms-vue/src/components/ui/button/Button.vue:1-69](file://fms-vue/src/components/ui/button/Button.vue#L1-L69)
- [fms-vue/src/components/ui/input/Input.vue:1-370](file://fms-vue/src/components/ui/input/Input.vue#L1-L370)
- [fms-vue/src/theme/tokens.css:1-152](file://fms-vue/src/theme/tokens.css#L1-L152)
## 依赖关系分析
- 低耦合:组件不直接依赖主题逻辑,仅消费 CSS 变量;主题逻辑集中在 theme 目录与 store。
- 单向数据流:Store -> Watch -> DOM/CSS 变量 -> 组件样式。
- 外部依赖:@lucide/vue 图标库用于按钮加载态等,不影响主题体系。
```mermaid
graph TB
Presets["presets.js"] --> Store["stores/app.js"]
Store --> ThemeIndex["theme/index.js"]
ThemeIndex --> Tokens["theme/tokens.css"]
Tokens --> Components["ui 组件样式"]
```
图表来源
- [fms-vue/src/theme/presets.js:1-16](file://fms-vue/src/theme/presets.js#L1-L16)
- [fms-vue/src/stores/app.js:1-123](file://fms-vue/src/stores/app.js#L1-L123)
- [fms-vue/src/theme/index.js:1-16](file://fms-vue/src/theme/index.js#L1-L16)
- [fms-vue/src/theme/tokens.css:1-152](file://fms-vue/src/theme/tokens.css#L1-L152)
章节来源
- [fms-vue/src/theme/presets.js:1-16](file://fms-vue/src/theme/presets.js#L1-L16)
- [fms-vue/src/stores/app.js:1-123](file://fms-vue/src/stores/app.js#L1-L123)
- [fms-vue/src/theme/index.js:1-16](file://fms-vue/src/theme/index.js#L1-L16)
- [fms-vue/src/theme/tokens.css:1-152](file://fms-vue/src/theme/tokens.css#L1-L152)
## 性能考虑
- CSS 变量预编译与派生:
- 使用 color-mix 在主色基础上派生 hover/active,减少重复定义与运行时计算开销。
- 将尺寸、间距、阴影等稳定值放入 tokens.css,构建期即可确定大部分样式。
- 运行时切换优化:
- 仅切换 的 class 与写入一个 CSS 变量,避免大规模重绘。
- 使用 Pinia watch 的 immediate: true 确保首屏一致,减少二次渲染。
- 组件级优化:
- 组件样式使用 scoped SCSS,避免全局污染与额外选择器开销。
- 复杂交互(如 Input 的 textarea 拖拽高度)使用 requestAnimationFrame 节流,降低重排频率。
## 故障排查指南
- 主题未生效
- 检查 是否包含 .dark class(深浅色切换)。
- 检查 --fms-primary 是否已写入根元素(主色切换)。
- 确认 store 的 persist.pick 是否包含 darkMode 与 primaryColor。
- 深浅色对比度异常
- 检查 .dark 下的语义色与文本色是否合理,必要时调整 tokens.css 中的对应变量。
- 组件样式错乱
- 确认组件样式通过 scoped SCSS 引用 tokens.css 变量,而非硬编码颜色。
- 检查是否有第三方样式覆盖了 CSS 变量优先级。
- 状态不同步
- 确认 initThemeSync 已在 pinia 实例激活后调用,且 watch 的 immediate 为 true。
章节来源
- [fms-vue/src/stores/app.js:98-122](file://fms-vue/src/stores/app.js#L98-L122)
- [fms-vue/src/theme/index.js:1-16](file://fms-vue/src/theme/index.js#L1-L16)
- [fms-vue/src/theme/tokens.css:121-152](file://fms-vue/src/theme/tokens.css#L121-L152)
## 结论
FMS 主题系统以 CSS 变量为核心,结合 Pinia 状态管理与极简的 DOM 写入,实现了高性能、易扩展、可维护的主题能力。通过 tokens.css 统一管理设计令牌,presets.js 集中管理预设主题,stores/app.js 负责状态与持久化,组件层仅消费变量,天然支持深浅色与主色切换。该架构便于后续扩展更多主题变体与定制能力,同时保持稳定的用户体验与性能表现。
## 附录
- 自定义主题扩展建议
- 新增主色:在 presets.js 中添加条目,并在 UI 中提供选择器绑定到 store 的 setPrimary。
- 新增语义色:在 tokens.css 中定义新变量,并在组件样式中引用。
- 新增尺寸/间距:在 tokens.css 中新增变量,组件样式按需消费。
- 测试与兼容性
- 单元测试:验证 store 的 setDark、toggleDark、setPrimary 行为及持久化 pick 字段。
- 视觉回归:在不同浏览器下校验深浅色切换后的对比度与布局稳定性。
- 兼容性:color-mix 在现代浏览器广泛支持,如需兼容旧环境可在构建期生成降级色值。