Files
workspace/code/fms/.codebuddy/plans/主题色切换与深浅色模式_9791a922.md
T
2026-08-16 22:01:32 +08:00

9.8 KiB
Raw Blame History

name, overview, design, todos
name overview design todos
主题色切换与深浅色模式 为 fms-vue 增加主题色切换(预设色板点选主色)与深/浅色切换功能,采用纯手写 + CSS 变量驱动方案,参考 Soybean Admin 与 Vue Vben Admin 的 design tokens 设计,主题状态用 pinia 持久化。
architecture styleKeywords fontSystem colorSystem
framework
vue
现代简洁
中后台
克制专业
平滑过渡
fontFamily heading subheading body
Noto Sans SC
size weight
28px 600
size weight
18px 500
size weight
14px 400
primary background text functional
#1677ff
#0958d9
#003eb3
#f5f6f8
#ffffff
#0f1115
#1a1d24
#1f2329
#86909c
#e6e8eb
#16a34a
#f59e0b
#ef4444
#e5e6eb
id content status
theme-tokens-foundation 创建 theme/presets.js 预设色板、theme/tokens.css 设计变量(浅色/深色 + color-mix 派生)及 theme/index.js 应用函数 completed
id content status dependencies
theme-store-init 创建 stores/app.js(Pinia 持久化)并改造 main.js 与 index.html,完成初始化与防闪烁 completed
theme-tokens-foundation
id content status dependencies
theme-ui-components 使用 [skill:ui-ux-pro-max] 设计并实现 ThemeToggle 深浅切换按钮与 ColorThemePicker 色板组件 completed
theme-store-init
id content status dependencies
theme-showcase 为 button.vue 与 App.vue 补充基于 tokens 的样式并接入主题切换组件,验证联动效果 completed
theme-ui-components

产品概述

为 fms-vue 增加一套轻量主题系统,支持「主色切换」与「深/浅色切换」。方案参考 Soybean Admin 与 Vue Vben Admin 的 token 化设计:CSS 变量作为唯一运行时真相,JS 仅负责集中配置与动态写变量。保持纯手写组件、不引入任何 UI 组件库。

核心功能

  • 预设主色切换:提供一组精选预设色板,点击色块一键切换品牌主色(primary),success / warning / destructive 保持固定
  • 深/浅色切换:手动切换明暗模式,状态持久化到 pinia(localStorage),刷新后保持用户选择
  • 衍生色自动派生:主色的 hover、active、前景色等通过 color-mix 自动派生,仅维护一个主色值,避免多处重复维护
  • 变量驱动组件:按钮、页面背景、卡片、文字、边框等全部通过 CSS 变量取色,切换主题即时生效
  • 首屏防闪烁:深色模式下刷新页面不出现浅色闪白

视觉要求

  • 主题切换控件(深浅切换按钮 + 色板)置于页面顶部,精致、克制
  • 色板以圆形色块网格展示,选中态有清晰描边标识
  • 组件展示区能直观看到主色与深浅色的联动效果

技术选型

  • 前端框架:Vue 3(已有)+ Vite(已有)
  • 状态与持久化:Pinia + pinia-plugin-persistedstate(已在 package.json 中,无需新增依赖)
  • 样式方案:原生 CSS 变量(design tokens)+ color-mix() 派生色,不引入 UI 库、Tailwind/UnoCSS/Less
  • 无新增任何运行时依赖

实现方案

总体策略

借鉴 Soybean / Vben 的共同设计:CSS 变量是唯一运行时真相。主题切换本质上只做两件事:

  1. 切换 <html> 上的 .dark class(深浅色)
  2. 向 <html> 写入 --primary 变量(主色)

其余所有颜色(衍生色、中性色、固定语义色)都由 tokens.css 静态定义或派生,JS 不散落颜色值。

颜色单源设计

  • 固定值全部进 tokens.css:中性色(background/card/foreground/muted/border)与固定语义色(success/warning/destructive)在 :root(浅色)和 .dark(深色)两组中集中维护
  • 动态值只有 --primary:由 JS 从预设色板写入;--primary-hover、--primary-active、--primary-foreground 用 color-mix() 派生
  • 深色模式不再重复维护主色:浅色下 hover 加深(mix black)、深色下 hover 提亮(mix white),在 .dark 中仅覆盖派生规则

模块职责

  • theme/presets.js:预设主色色板(颜色值单源)
  • theme/tokens.css:全部 CSS 变量(浅色 + 深色 + 主色派生规则)
  • theme/index.js:纯函数 applyPrimaryColor(color)、applyDarkMode(isDark)
  • stores/app.js:Pinia store,state 为 darkMode + primaryColor,watch 变化时调用 theme/index.js 同步到 DOM,persist: true 持久化

初始化与防闪烁

  • main.js 注册 pinia + 持久化插件,mount 前实例化 app store(此时持久化状态已恢复并 watch 首次触发)
  • index.html 的 <head> 加 inline script:提前读取 localStorage 中持久化的 darkMode,在 CSS 加载前给 <html> 加 .dark class,避免刷新闪白

性能说明

  • 切换操作仅执行 1 次 setProperty + 1 次 classList.toggle,无组件重渲染开销
  • color-mix() 由浏览器一次性计算,无运行时 JS 计算成本;目标为现代浏览器(Vite 8 默认目标,Chrome 111+ 已支持)

架构设计

flowchart LR
    A[app store<br/>darkMode + primaryColor] -->|watch| B[theme/index.js]
    B -->|classList.toggle dark| C[html.dark]
    B -->|setProperty --primary| D[html 变量]
    C --> E[tokens.css 深色覆盖]
    D --> F[color-mix 派生 hover/active]
    E --> G[组件样式 var 取值]
    F --> G
    H[index.html inline script] -->|提前恢复| C

目录结构

fms-vue/
├── index.html                          # [MODIFY] head 加 inline script 提前恢复 .dark,防刷新闪烁
└── src/
    ├── main.js                         # [MODIFY] 注册 pinia + 持久化插件,mount 前初始化 app store
    ├── styles/
    │   └── index.css                   # [MODIFY] 顶部 @import '../theme/tokens.css',保留字体配置
    ├── theme/
    │   ├── presets.js                  # [NEW] 预设主色色板(约 6-8 个精选主色,含默认蓝/青/绿/橙/紫/玫红)
    │   ├── tokens.css                  # [NEW] 浅色 :root + 深色 .dark 全部 CSS 变量 + 主色 color-mix 派生规则
    │   └── index.js                    # [NEW] applyPrimaryColor / applyDarkMode 纯函数,操作 documentElement
    ├── stores/
    │   └── app.js                    # [NEW] Pinia app store:darkMode/primaryColor + setDark/toggleDark/setPrimary + persist
    ├── components/
    │   └── ui/
    │       ├── ThemeToggle.vue         # [NEW] 深/浅色切换按钮(太阳/月亮图标 + 动画过渡)
    │       ├── ColorThemePicker.vue    # [NEW] 预设色板点选组件(圆形色块网格 + 选中描边)
    │       └── button/
    │           └── button.vue          # [MODIFY] 补充基于 tokens 的按钮样式(<style scoped> 引用 var)
    └── App.vue                         # [MODIFY] header 接入 ThemeToggle + ColorThemePicker,补充展示区样式

关键代码结构

presets.js

export const PRESETS = [{ name: '拂晓蓝', color: '#1677ff' }, ...],第一个为默认主色。

stores/app.js(state 与 actions 契约)

  • state:{ darkMode: false, primaryColor: PRESETS[0].color }
  • actions:setDark(v)、toggleDark()、setPrimary(color)
  • persist: true(key 默认即 store id app,与 index.html 读取的 key 一致)

theme/index.js

  • applyPrimaryColor(color):documentElement.style.setProperty('--primary', color)
  • applyDarkMode(isDark):documentElement.classList.toggle('dark', isDark)

tokens.css 变量骨架

  • 中性色:--background、--card、--foreground、--muted、--muted-foreground、--border
  • 固定语义色:--success、--warning、--destructive(浅/深各一组)
  • 主色系列:--primary(JS 写入)、--primary-foreground、--primary-hover、--primary-active(color-mix 派生,.dark 下覆盖派生方向)

设计风格

采用现代简洁的中后台风格,参考 Soybean / Vben 的克制专业气质。整体轻量、留白充足,主题控件精致不喧宾夺主,切换过程有平滑过渡。

页面结构(App.vue 展示页)

顶部导航栏

  • 左侧:品牌标识「FMS UI」+ 页面标题,使用较大的字重形成层次
  • 右侧:主题控件区,依次为 ColorThemePicker 色板与 ThemeToggle 深浅切换按钮,间距适度

组件展示区

  • 卡片式容器承载 Button 组件展示,卡片背景、边框、文字均随深浅色变化
  • 主色按钮(default 类型)作为主题色最直观的反馈载体,切换色板时按钮颜色即时变化

控件设计

ColorThemePicker 色板

  • 圆形色块网格排列,每个色块展示预设主色,hover 轻微放大并显示阴影
  • 当前选中的色块带外圈描边环 + 内部对勾标识,与浅/深背景均有足够对比度

ThemeToggle 深浅切换

  • 胶囊形或圆形按钮,内含太阳/月亮图标,点击在浅色与深色间切换
  • 切换采用 icon 旋转/淡入淡出的微动画,状态持久化后刷新不丢失

响应式与可访问性

  • 色板与切换按钮在窄屏下自动换行或收缩,保持可点击区域不小于 36px
  • 选中态不仅靠颜色区分,还依赖描边与图标双重标识,兼顾色弱用户

Agent Extensions

Skill

  • ui-ux-pro-max
  • 用途:设计主题切换控件(ColorThemePicker 色板与 ThemeToggle 深浅切换按钮)的视觉方案,确定预设主色色板配色与选中态、hover 态、微动画细节
  • 预期产出:一套与中后台风格匹配的控件视觉规范,包含色板颜色选择、控件尺寸/圆角/描边、深浅色下的对比度与可访问性建议