跳转至

@bams-app/theme

📄 创建: 刘泽伟 2026-08-04

https://doc.xpku.com/raw/tech/frontend/theme/theme-switch.md

文档说明

本文档基于 @bams-app/theme 包整理,说明主题换肤模块的功能边界、接入方式、运行机制,以及颜色变量的命名与分层规范,供前端项目统一接入和扩展。

适用目录:/Users/wei/bams-work/work/packages/theme

模块定位

@bams-app/theme 将主题相关能力统一封装为独立包,目标是让业务项目以较低成本获得一致的换肤能力,同时避免颜色变量散落在各业务模块中。

模块当前提供以下能力:

  • 主题模式管理:lightdarkauto
  • 主题预设管理:内置经典蓝、松石绿、暮光紫、青柠绿、活力橙
  • Pinia 主题 Store:统一管理模式、预设、解析后的明暗态和主题类名
  • CSS 变量注入:通过 theme-* 类挂载语义变量
  • 跟随系统主题:auto 模式下自动监听系统明暗变化
  • Ant Design Vue 主题适配:输出 theme 配置对象
  • DevExtreme 主题同步:切换 DOM 主题时同步 devextreme/ui/themesdevextreme/viz/themes
  • 局部主题变量能力:组件可按页面级、容器级单独挂载局部变量
  • 现成的切换 UI:ThemePanel
  • 环境开关控制:通过 VUE_APP_ENABLE_THEME 控制是否启用

导出能力

模块统一从 src/index.js 导出:

  • 常量:DEFAULT_THEME_MODEDEFAULT_THEME_PRESETTHEME_MODESTHEME_PRESETS
  • 运行时:resolveThemeMode()applyTheme()setupTheme()cleanupTheme()
  • Store:createThemeStore()useThemeStore()useThemeState()
  • UI 桥接:createAntdThemeConfig()useAntdThemeConfig()
  • 工具方法:颜色处理、系统主题识别、开关解析等
  • 局部变量 Hook:useLocalThemeVars()
  • UI 组件:ThemePanel

启用方式

主题功能默认关闭,只有显式配置环境变量时才开启。

环境变量:VUE_APP_ENABLE_THEME

启用值:

  • 1
  • true
  • on
  • yes

未设置或其他值时,模块退回默认主题:

  • 模式:light
  • 预设:material.blue

示例:

VUE_APP_ENABLE_THEME=true

标准接入方式

1. 安装 Pinia 和持久化插件

主题 Store 使用 Pinia,且默认只持久化以下字段:

  • mode
  • preset

默认本地存储 key 为:[bams-app]-theme

2. 在应用入口初始化

import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import { setupTheme, useThemeStore } from "@bams-app/theme";
import "@bams-app/theme/src/theme.less";

const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
app.use(pinia);

const themeStore = useThemeStore();
themeStore.init();
setupTheme();

推荐顺序:

  1. 注册 Pinia
  2. 挂载持久化插件
  3. 引入 theme.less
  4. 执行 themeStore.init(),校验持久化值是否合法
  5. 执行 setupTheme(),同步 DOM 类名、系统主题监听和第三方主题

运行机制

1. Store 维护主题状态

Store 中维护以下核心状态:

  • mode:用户选择的模式,值为 light | dark | auto
  • preset:当前主题预设,如 material.blue
  • resolvedMode:真正生效的模式,仅为 light | dark
  • modeClass:如 theme-light
  • presetClass:如 theme-material-blue

2. setupTheme 负责主题同步

setupTheme() 会完成以下事情:

  • 初次执行 applyTheme(),把主题类挂到根节点
  • 监听 Store 状态变化,自动同步主题
  • auto 模式下监听 prefers-color-scheme
  • 同步 DevExtreme 图表和组件主题

3. applyTheme 的核心逻辑

applyTheme() 不是直接写 CSS 变量,而是通过组合类名生效:

  • 明暗模式类:theme-light / theme-dark
  • 主题预设类:theme-material-blue / theme-material-teal

最终会把类挂到根元素,并保留兼容属性:

  • data-theme
  • data-color-mode

这样做的好处是:

  • 变量来源集中在样式层维护
  • DOM 上的主题状态明确可见
  • 便于组件、页面、第三方 UI 一起消费

主题切换面板

模块内置 ThemePanel 组件,适合直接挂到设置抽屉、个人偏好页等场景。

支持能力:

  • 切换主题模式:浅色 / 深色 / 跟随系统
  • 切换主题预设:5 套内置品牌色
  • 面板只在主题功能开启时显示

示例:

<template>
  <ThemePanel />
</template>

<script setup>
import { ThemePanel } from "@bams-app/theme";
</script>

状态读取与主题修改

只读场景

业务组件如果只消费主题状态,推荐使用 useThemeState()

<script setup>
import { useThemeState } from "@bams-app/theme";

const { mode, preset, resolvedMode, modeClass, presetClass } = useThemeState();
</script>

读写场景

如果组件既要展示当前状态,又要切换主题,推荐组合使用:

  • useThemeState():读取响应式状态
  • useThemeStore():调用 action
<script setup>
import { useThemeState, useThemeStore } from "@bams-app/theme";

const themeStore = useThemeStore();
const { preset, resolvedMode } = useThemeState();

const switchDark = () => {
  themeStore.setMode("dark");
};
</script>

与 Ant Design Vue 集成

如果项目使用 Ant Design Vue,推荐在根组件中把主题 Store 映射为 antd 的 theme 配置。

<script setup>
import { theme as antdTheme } from "ant-design-vue";
import { useAntdThemeConfig, useThemeStore } from "@bams-app/theme";

const themeStore = useThemeStore();
const antdThemeConfig = useAntdThemeConfig(themeStore, antdTheme);
</script>

模块当前映射了两个关键 token:

  • colorPrimary
  • colorInfo

算法切换规则:

  • 浅色模式:defaultAlgorithm
  • 深色模式:darkAlgorithm

局部主题变量

如果某个页面或容器想使用自己的主题变量,而不依赖全局根节点注入,可以用 useLocalThemeVars() 或手动计算 :style

适用场景:

  • 单个页面想局部定制视觉
  • 某个容器要与全局主题保持一致,但又要补充私有变量
  • 弹出层希望继承当前容器变量,而不是 document.body

使用原则:

  • 局部变量仍然建议沿用 --bams-* 语义命名
  • 模式差异放在 themeModeVars
  • 品牌差异放在 themePresetVars
  • 弹出层组件通过 getPopupContainer 绑定到当前容器

内置主题预设

当前预设定义在 THEME_PRESETS 中:

名称 value 主色
经典蓝 material.blue #0d6cbd
松石绿 material.teal #13c2c2
暮光紫 material.purple #722ed1
青柠绿 material.lime #aeea00
活力橙 material.orange #ff6d00

新增预设时,需要同时维护:

  • constants.js 中的 THEME_PRESETS
  • theme-presets.less 中对应的 .theme-*

颜色变量架构规范

主题变量采用分层架构,避免把品牌色、模式色、状态色混在一起。

1. 架构分层

样式入口顺序如下:

  1. architecture.less
  2. root-tokens.less
  3. theme-presets.less
  4. theme-light.less
  5. theme-dark.less
  6. global.less

各层职责:

:root

全局默认语义变量,提供兜底值,保证即使主题功能关闭,页面仍有可用颜色系统。

.theme-material-*

主题预设层,只覆盖品牌相关变量,不负责背景、文本、边框等中性色。

.theme-light / .theme-dark

模式层,负责页面背景、文本层级、边框、阴影、浮层、交互态等中性色系统。

组件样式层

组件内部只消费语义变量,不直接写死颜色值。

2. 命名规范

统一使用 --bams-* 前缀,按语义而不是按业务组件命名。

推荐命名族:

  • 品牌主色:--bams-primary-*
  • 链接/信息:--bams-link-*--bams-info-*
  • 语义状态:--bams-success-*--bams-warning-*--bams-error-*
  • 文本层级:--bams-text-*
  • 背景层级:--bams-*-bg--bams-surface-*
  • 边框层级:--bams-border-*
  • 阴影/焦点:--bams-shadow-*--bams-focus-*

后缀含义:

  • color:实体色值,适合文本、图标、描边
  • bg:背景色
  • soft:弱化填充态
  • hover / active / selected / disabled:交互态
  • text:文本使用色
  • border:边框使用色
  • outline:外发光、聚焦描边等

3. 变量职责规范

品牌色变量

品牌预设层负责这类变量,例如:

  • --bams-primary
  • --bams-primary-hover
  • --bams-primary-active
  • --bams-primary-border
  • --bams-primary-soft
  • --bams-primary-bg
  • --bams-primary-outline

这类变量应只表达品牌色及其衍生态,不应混入明暗模式判断。

模式层变量

明暗模式层负责中性色和结构层级,例如:

  • 页面和容器背景:--bams-page-bg--bams-surface-bg
  • 文本层级:--bams-text-color--bams-text-secondary
  • 边框层级:--bams-border-color--bams-border-strong
  • 浮层和遮罩:--bams-overlay-bg--bams-mask-bg
  • 组件交互:--bams-hover-bg--bams-selected-bg

这类变量应只表达浅色和深色差异,不应写死具体品牌色方案。

状态色变量

成功、警告、错误等状态色定义在根变量层,通常跨预设共用:

  • --bams-success-*
  • --bams-warning-*
  • --bams-error-*

除非确有品牌策略要求,不建议把状态色跟随主题预设一起变化。

推荐实践

  • 组件尽量消费语义变量,不直接写 #xxxxxx
  • 品牌色变化放到预设层,不要散落在业务组件里
  • 明暗差异放到 theme-light.less / theme-dark.less
  • 业务组件新增颜色时,优先补语义变量,再在组件里使用
  • 新增主题预设时,同时补充常量定义和 .theme-* 样式类
  • 主题关闭时也要保证默认主题可用,因此 :root 必须有兜底值
  • 对接第三方组件库时,优先由主题 Store 派生其配置对象,不要在页面里重复映射

不推荐做法

  • 在业务组件里直接写死品牌色
  • theme-light.less 中定义某个预设专属颜色
  • theme-presets.less 中覆盖大面积背景、边框、文本层级
  • 让组件直接依赖 material.blue 这类预设名判断逻辑
  • 新增变量时使用无语义命名,如 --blue-1--card-color-2

维护建议

如果后续继续扩展主题体系,建议遵循以下顺序:

  1. 先明确变量属于品牌层、模式层还是状态层
  2. 再决定应该加到 root-tokens.lesstheme-presets.lesstheme-light.lesstheme-dark.less
  3. 最后再让业务组件消费这个变量

这样可以保证:

  • 变量职责清晰
  • 新主题扩展成本低
  • 主题切换时不容易出现视觉串色
  • 第三方组件与业务组件的换肤规则保持一致

总结

@bams-app/theme 的核心思想不是“切换几组颜色”,而是通过 Store + DOM 类名 + 分层语义变量,建立一个可维护、可扩展、可复用的前端主题系统。

对于业务接入方,最关键的原则是:

  • 入口统一初始化
  • 组件统一消费语义变量
  • 品牌色、模式色、状态色分层维护
  • 扩展预设时同时维护常量与样式映射

按这个规范执行后,模块可以稳定支撑全局换肤、局部换肤,以及 Ant Design Vue / DevExtreme 等第三方库的主题同步。