Skip to content

主题定制 ​

ClassIntra 的主题系统由 client/src/core/theme-engine.js 提供,基于 CSS 变量驱动 + 双写策略,支持运行时切换、扩展主题、动态色注入。

本页内容覆盖以下主题:

ThemeEngine 架构 ​

源码:client/src/core/theme-engine.js

ThemeEngine 是 ClassIntra 的主题引擎核心,负责管理主题注册、切换、CSS 变量注入、订阅通知。

核心特性 ​

  • 双写策略:同时写入 --ci-* 新变量(inline style)与 data-theme 属性(触发旧 CSS 切换),向后兼容
  • CSS 变量驱动:所有 token 展平为 --ci-color-* / --ci-shape-* / --ci-motion-* / --ci-elevation-* 注入 document.documentElement
  • 运行时切换:调用 setTheme() 立即生效,无需刷新页面
  • 持久化:用户选择自动保存到 localStorage['theme'] 与服务端 user_settings 表
  • 订阅机制:通过 subscribe 注册回调,主题变化时通知
  • 动画开关:setMotionEnabled 控制全局动画,响应 prefers-reduced-motion
  • 扩展主题:通过 theme-extension-loader 扫描 theme-extensions/ 目录加载第三方主题包
  • 动态色注入:setDynamicColor(seedColor) 重新生成扩展主题色板

获取实例 ​

javascript
import { getThemeEngine } from '@/core/theme-engine';

// 单例工厂:首次调用创建实例并应用保存的主题
var engine = getThemeEngine();

内置主题 ​

源码:themes/light/ + themes/dark/

主题 ID名称类型路径
light浅色lightthemes/light/manifest.json + tokens.js
dark深色darkthemes/dark/manifest.json + tokens.js

内置主题通过 theme-loader.js 在应用启动时动态加载,注册到 ThemeEngine。

设计动机

将所有视觉数值抽离为 token,让 UI 组件只消费 var(--ci-*) 而不写死颜色,从而实现「一处变更、全局生效」。这也是运行时切换主题无需刷新的根本原因。

Token 体系 ​

ClassIntra 的设计 Token 体系遵循 前缀分类 + 语义命名 原则,所有 token 注入为 CSS 变量。

Token 前缀 ​

前缀用途示例
--ci-color-*颜色(主色、表面、文字、边框、语义)--ci-color-primary、--ci-color-surface-base
--ci-shape-*形状(圆角、半径)--ci-shape-xs、--ci-shape-3xl
--ci-motion-*动画(时长、缓动、开关)--ci-motion-duration-fast、--ci-motion-easing-spring
--ci-elevation-*阴影层级--ci-elevation-sm、--ci-elevation-lg

圆角(Shape) ​

Token值说明
--ci-shape-xs4px极小圆角(徽章、标签)
--ci-shape-sm8px小圆角(按钮、输入框)
--ci-shape-md12px中圆角(卡片、面板)
--ci-shape-lg16px大圆角(应用图标、模态框)
--ci-shape-xl20px超大圆角(超能岛)
--ci-shape-2xl24px巨大圆角(窗口)
--ci-shape-3xl28px极大圆角(应用图标占位)
--ci-shape-full9999px全圆角(头像、胶囊按钮)

应用图标规范

应用图标固定 56×56px,圆角使用 --ci-shape-lg(16px)或 --ci-shape-3xl(28px),符合 iOS 圆角视觉。详见 图标系统。

间距(Spacing) ​

Token值说明
--ci-spacing-xs4px极小间距(图标内)
--ci-spacing-sm8px小间距(按钮内)
--ci-spacing-md12px中间距(卡片内)
--ci-spacing-lg16px大间距(面板内)
--ci-spacing-xl24px超大间距(区块间)
--ci-spacing-xxl48px巨大间距(页面级)

字号(Typography) ​

Token值说明
--ci-font-caption12px注释、辅助文字
--ci-font-body14px正文(默认)
--ci-font-title17px卡片标题
--ci-font-headline20px区块标题
--ci-font-large24px大标题
--ci-font-largeTitle40px超大标题(页面级)

字号阶梯

字号阶梯遵循 iOS 字体规范,从 12px 到 40px 共 6 级,覆盖从注释到大标题的所有场景。

缓动(Easing) ​

Token值说明
--ci-motion-easing-standardcubic-bezier(0.4, 0, 0.2, 1)标准缓动(默认)
--ci-motion-easing-springcubic-bezier(0.175, 0.885, 0.32, 1.275)弹性缓动(强调动画)
Token值说明
--ci-motion-duration-fast150ms快速动画(按钮、悬停)
--ci-motion-duration-normal250ms标准动画(卡片展开)
--ci-motion-duration-slow350ms慢速动画(模态框、超能岛)

颜色(Color) ​

颜色 token 按类别分组,每类有多个语义化子键:

类别子键示例 CSS 变量
主色primary / primaryLight / primaryDark--ci-color-primary
表面background / surface / surfaceRaised--ci-color-surface
文字textPrimary / textSecondary / textDisabled--ci-color-text-primary
边框border / borderSubtle--ci-color-border
语义success / warning / error / info--ci-color-success

优先使用语义化 token

应用 CSS 中应优先使用语义化 token(如 --ci-color-text-primary),而非具体的 --ci-color-primary。前者会在主题切换时自动适配深色模式。

双写策略详解 ​

ClassIntra 主题系统的核心是双写策略:同时维护两套 CSS 变量入口,确保新旧代码都能正常工作。

写入流程 ​

ThemeEngine.setTheme(id) 的执行流程:

  1. 校验主题:未注册则回退到 'light'
  2. 旧机制写入:setAttribute('data-theme', id) 触发旧 CSS 选择器(:root / [data-theme="dark"])切换
  3. 新变量写入:调用 flattenTokens 展平 token,通过 applyToElement(document.documentElement, flatMap) 写入 --ci-* inline style
  4. 清旧写新:先 removeFromElement(root) 清除旧的 --ci-* 变量,再写入新的
  5. 通知订阅者:遍历 _subscribers 调用回调(异常隔离)
  6. EventBus 广播:通过 getEventBus().emit('THEME_CHANGED', payload) 通知非直接订阅者

两套变量对照 ​

旧别名(向后兼容)新变量(推荐)用途
--primary-color--ci-color-primary主色
--bg-color--ci-color-background背景色
--text-color--ci-color-text-primary主文字色
--border-color--ci-color-border边框色

新代码请用 --ci-*

新代码应统一使用 --ci-* 前缀变量。旧别名(--primary-color 等)仅为向后兼容保留,未来版本可能移除。两套变量在主题切换时都会被同步更新。

CSS 中使用示例 ​

css
.my-card {
  /* 推荐使用新变量 */
  background: var(--ci-color-surface);
  color: var(--ci-color-text-primary);
  border: 1px solid var(--ci-color-border-subtle);
  border-radius: var(--ci-shape-md);
  padding: var(--ci-spacing-lg);
  box-shadow: var(--ci-elevation-sm);
  transition: all var(--ci-motion-duration-fast) var(--ci-motion-easing-standard);
}

.my-card:hover {
  box-shadow: var(--ci-elevation-md);
}

/* 旧代码兼容(不推荐新代码使用) */
.legacy-button {
  background: var(--primary-color);
  border-radius: var(--ci-shape-sm);
}

主题定制指南 ​

创建自定义主题(3 种方式) ​

方式一:通过 registerTheme 注册(推荐用于变体) ​

javascript
import { getThemeEngine } from '@/core/theme-engine';

var engine = getThemeEngine();

engine.registerTheme('ocean', {
  name: '海洋蓝',
  type: 'light',
  tokens: {
    color: {
      primary: '#0EA5E9',
      primaryLight: '#38BDF8',
      primaryDark: '#0284C7',
      background: '#F0F9FF',
      surface: '#FFFFFF'
    }
  }
});

engine.setTheme('ocean');

方式二:通过扩展主题包(推荐用于分发) ​

创建 theme-extensions/ocean/ 目录:

theme-extensions/ocean/
├── manifest.json
├── apply.js
├── tokens.js
└── README.md

启动时 theme-extension-loader.js 会自动扫描并加载,详见 扩展主题开发。

方式三:通过主题目录(推荐用于内置主题) ​

在 themes/ 顶级目录创建新主题子目录:

themes/ocean/
├── manifest.json
└── tokens.js

theme-loader.js 启动时扫描 themes/*/manifest.json 加载。

主题定制步骤 ​

完整创建自定义主题的步骤:

  1. 设计 token:根据设计稿定义颜色、圆角、间距、字号、缓动等 token
  2. 选择注册方式:变体用 registerTheme,分发用扩展主题包,内置用 themes/ 目录
  3. 编写 tokens.js:导出 token 对象(结构需符合 flattenTokens 展平规则)
  4. 编写 manifest.json(仅扩展主题与内置主题):声明主题元数据
  5. 注册到引擎:通过 registerTheme 或自动加载
  6. 测试切换:调用 setTheme(id) 验证视觉效果
  7. 持久化验证:刷新页面确认主题被正确恢复

flattenTokens 展平规则

token 对象的嵌套结构会被展平为 --ci-{category}-{key}-{subkey} 格式(驼峰转 kebab-case):

  • color.primary → --ci-color-primary
  • color.surfaceRaised → --ci-color-surface-raised
  • motion.durationFast → --ci-motion-duration-fast

示例:考试模式主题 ​

javascript
import { getThemeEngine } from '@/core/theme-engine';

var engine = getThemeEngine();

// 极简、低干扰、无动画的考试模式
engine.registerTheme('exam', {
  name: '考试模式',
  type: 'light',
  tokens: {
    color: {
      primary: '#6B7280',     // 中性灰主色
      background: '#FAFAFA',
      surface: '#FFFFFF'
    }
  }
});

function startExamMode() {
  engine.setTheme('exam');
  engine.setMotionEnabled(false);  // 关闭所有动画
}

扩展主题开发 ​

源码:client/src/core/theme-extension-loader.js + theme-extensions/

扩展主题是 ClassIntra 的核心扩展能力,第三方可以创建独立的主题包,通过 theme-extensions/ 目录加载。

目录结构 ​

theme-extensions/material-you/
├── manifest.json       # 主题元数据
├── apply.js            # 应用逻辑(注册到 ThemeEngine)
├── tokens.js           # Token 定义
└── README.md           # 说明文档

manifest.json ​

json
{
  "id": "material-you",
  "name": "Material You",
  "version": "1.0.0",
  "description": "Material You 动态色主题",
  "author": "ClassIntra Community",
  "type": "extension",
  "light": {
    "id": "material-you-light",
    "name": "Material You Light"
  },
  "dark": {
    "id": "material-you-dark",
    "name": "Material You Dark"
  }
}
字段类型必填说明
idstring是扩展唯一 ID
namestring是显示名称
versionstring是版本号
descriptionstring否描述
authorstring否作者
type'extension'是固定为 extension
lightobject是浅色主题配置(id、name)
darkobject是深色主题配置(id、name)

apply.js ​

扩展主题的入口,由 theme-extension-loader.js 在加载时调用。

javascript
// theme-extensions/material-you/apply.js
import tokens from './tokens.js';

export default function apply(engine, manifest) {
  // 注册浅色主题
  engine.registerTheme(manifest.light.id, {
    name: manifest.light.name,
    type: 'light',
    tokens: tokens.light
  });

  // 注册深色主题
  engine.registerTheme(manifest.dark.id, {
    name: manifest.dark.name,
    type: 'dark',
    tokens: tokens.dark
  });

  // 可选:注册扩展状态(用于动态色注入)
  engine.registerExtensionState({
    extensionId: manifest.id,
    lightId: manifest.light.id,
    darkId: manifest.dark.id,
    manifest: manifest,
    applyFn: apply,
    buildTokensFn: function(seedColor, type) {
      // 基于种子色动态生成色板
      return generatePaletteFromSeed(seedColor, type);
    }
  });
}

function generatePaletteFromSeed(seedColor, type) {
  // 实现动态色生成逻辑(HSL 明度调整)
  return {
    color: {
      primary: seedColor,
      primaryLight: lighten(seedColor, 0.15),
      primaryDark: darken(seedColor, 0.15)
    }
  };
}

tokens.js ​

javascript
// theme-extensions/material-you/tokens.js
export default {
  light: {
    color: {
      primary: '#6750A4',
      primaryLight: '#7E5DCC',
      primaryDark: '#4F3D7E',
      background: '#FEF7FF',
      surface: '#FFFFFF',
      surfaceRaised: '#F7F2FA'
    }
  },
  dark: {
    color: {
      primary: '#D0BCFF',
      primaryLight: '#E5DDFF',
      primaryDark: '#A794E6',
      background: '#141218',
      surface: '#211F26',
      surfaceRaised: '#2C2A33'
    }
  }
};

加载流程 ​

theme-extension-loader.js 在应用启动时的执行流程:

  1. 扫描 theme-extensions/*/manifest.json 文件
  2. 解析 manifest,校验 type === 'extension'
  3. 动态 import() 扩展的 apply.js
  4. 调用 apply(engine, manifest) 注册主题到 ThemeEngine
  5. 可选:注册扩展状态,用于后续 setDynamicColor 动态色注入

异步加载

扩展主题使用动态 import() 异步加载,加载完成后才会注册到 ThemeEngine。若用户在加载完成前切换到该主题,ThemeEngine 会回退到 light 并在加载完成后自动切换。

动态色注入 ​

扩展主题支持动态色注入:通过 ThemeEngine.setDynamicColor(seedColor) 重新生成色板。

javascript
import { getThemeEngine } from '@/core/theme-engine';

var engine = getThemeEngine();

// 假设当前主题为 material-you-light
engine.setDynamicColor('#FF6B6B');
// 内部流程:
// 1. 读取当前主题对应的扩展状态
// 2. 调用 buildTokensFn('#FF6B6B', 'light') 生成新 token
// 3. 重新调用 registerTheme 更新 token
// 4. 若当前主题是该扩展主题,立即重新写入 --ci-color-* 变量

动态色应用场景

  • 用户从相册选取壁纸颜色,自动生成匹配的主题色
  • 季节性主题(春节红、圣诞绿)
  • 班级品牌色定制

完整示例:Material You 扩展 ​

参考 theme-extensions/material-you/,已实现:

  • 浅色 / 深色双主题注册
  • 基于种子色的动态色板生成
  • README 说明文档
javascript
// 使用示例
import { getThemeEngine } from '@/core/theme-engine';

var engine = getThemeEngine();

// 切换到 Material You 浅色主题
engine.setTheme('material-you-light');

// 动态注入种子色
engine.setDynamicColor('#FF9800');

// 切换到深色变体
engine.setTheme('material-you-dark');

相关文档 ​