# 主题系统 **本文引用的文件** - [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 在现代浏览器广泛支持,如需兼容旧环境可在构建期生成降级色值。