时光胶囊 App 支持四套主题——暖金、亮色、暗色、护眼。切换主题后,整个 App 的背景色、文字色、主色、边框色全部同步变化。这种"一个变量驱动全局颜色"的机制,核心是一个 ThemeColors 接口和一个 getThemeColors 函数。设计得好,新增主题只需要加一个分支;设计得差,改一个颜色需要改几十个文件。

完整效果
在这里插入图片描述

一、主题系统的整体架构

ThemeType 枚举         定义四种主题
       ↓
getThemeColors()       根据 ThemeType 返回 ThemeColors 对象
       ↓
this.getColors()       Index 页面的快捷方法
       ↓
所有 UI 组件            通过 colors 参数接收颜色

四层架构——枚举→工厂函数→快捷方法→组件消费。 每一层只做一件事,职责清晰。

二、ThemeType 枚举

// 从 utils/Theme.ets 导入
enum ThemeType {
  CAPSULE = 'CAPSULE',   // 暖金主题(默认)
  LIGHT = 'LIGHT',       // 亮色主题
  DARK = 'DARK',         // 暗色主题
  EYE_CARE = 'EYE_CARE'  // 护眼主题
}

四种主题的定位

主题 键值 目标场景
CAPSULE ‘CAPSULE’ 默认主题,温暖金色调
LIGHT ‘LIGHT’ 标准亮色,高对比度
DARK ‘DARK’ 暗色背景,低亮度
EYE_CARE ‘EYE_CARE’ 低蓝光,长时间阅读

CAPSULE 是默认主题——和其他 App 的默认主题不同。 知识库 App 默认紫色,旅行 App 默认橙色,时光胶囊默认暖金色。每个 App 的默认主题和它的产品调性匹配——时光胶囊的"暖金"传递温暖和期待。

三、ThemeColors 接口

从所有页面的使用方式推断,ThemeColors 包含这些字段:

interface ThemeColors {
  primary: string        // 主色(按钮、标签、进度条)
  background: string     // 页面背景色
  surface: string        // 卡片/容器背景色
  text: string           // 主文字色(标题、正文)
  textSecondary: string  // 辅助文字色(说明、标签)
  textTertiary: string   // 最弱文字色(时间、占位符)
  border: string         // 边框色
  error: string          // 错误/危险色
}

在这里插入图片描述

颜色字段的使用频率

字段 使用位置 频率
primary 按钮、选中标签、进度条、链接 最高
background 页面背景
surface 卡片、输入框、导航栏背景 最高
text 标题、正文 最高
textSecondary 说明文字、辅助信息
textTertiary 时间戳、占位符
border 边框、分隔线
error 清空按钮、危险操作

primary 和 surface 是使用频率最高的——几乎所有交互元素都用到。 这两个字段的配色决定了整个主题的"性格"。

四、四套配色方案

暖金主题(CAPSULE)

字段 说明
primary #D4A574 暖金色
background #FBF8F3 米白色
surface #FFFFFF 纯白
text #2C2417 深棕
textSecondary #8C7B6B 浅棕
textTertiary #B8A99A 更浅棕
border #E8E0D5 暖灰
error #E74C3C 红色

整个色系围绕"棕色+金色"——温暖、复古、有时间感。 和"时光胶囊"的产品主题高度契合。

亮色主题(LIGHT)

字段 说明
primary #3B82F6 标准蓝
background #F9FAFB 浅灰白
surface #FFFFFF 纯白
text #111827 近黑
textSecondary #6B7280 中灰
textTertiary #9CA3AF 浅灰
border #E5E7EB 边框灰
error #EF4444 红色

标准亮色主题——高对比度,适合白天使用。 蓝色主色是最"安全"的选择——不暖不冷,大多数用户接受度高。

暗色主题(DARK)

字段 说明
primary #60A5FA 亮蓝
background #111827 深灰黑
surface #1F2937 稍浅灰
text #F9FAFB 近白
textSecondary #9CA3AF 中灰
textTertiary #6B7280 暗灰
border #374151 深灰
error #F87171 浅红

暗色主题的文字和背景完全反转——背景变深,文字变浅。 主色从标准蓝变为亮蓝——在深色背景上需要更亮的颜色才能保持可读性。

护眼主题(EYE_CARE)

字段 说明
primary #059669 绿色
background #ECFDF5 浅绿白
surface #FFFFFF 纯白
text #065F46 深绿
textSecondary #6B7280 中灰
textTertiary #9CA3AF 浅灰
border #D1FAE5 浅绿
error #EF4444 红色

护眼主题用绿色为主色——绿色对眼睛的刺激最小。 背景带微弱绿色调,减少蓝光辐射。

五、getThemeColors 工厂函数

export function getThemeColors(theme: ThemeType): ThemeColors {
  switch (theme) {
    case ThemeType.CAPSULE:
      return { primary: '#D4A574', background: '#FBF8F3', ... }
    case ThemeType.LIGHT:
      return { primary: '#3B82F6', background: '#F9FAFB', ... }
    case ThemeType.DARK:
      return { primary: '#60A5FA', background: '#111827', ... }
    case ThemeType.EYE_CARE:
      return { primary: '#059669', background: '#ECFDF5', ... }
    default:
      return { primary: '#D4A574', background: '#FBF8F3', ... }
  }
}

在这里插入图片描述

switch-case 返回完整的 ThemeColors 对象——每个主题返回 8 个颜色值。 default 回退到暖金主题——保证即使传入无效主题值,也不会崩溃。

为什么用工厂函数而不是对象映射

// 方案 A(当前):工厂函数
function getThemeColors(theme: ThemeType): ThemeColors {
  switch (theme) { ... }
}

// 方案 B:对象映射
const THEME_MAP: Record<ThemeType, ThemeColors> = {
  [ThemeType.CAPSULE]: { ... },
  [ThemeType.LIGHT]: { ... },
  ...
}

方案 A 更好——因为工厂函数可以加逻辑。 未来如果需要根据时间自动切换(白天亮色/晚上暗色),工厂函数里加 new Date().getHours() 判断即可。对象映射是纯数据,没有扩展空间。

六、主题切换的持久化

写入 Preferences

private async saveTheme(theme: ThemeType): Promise<void> {
  try {
    const prefs = await preferences.getPreferences(this.context!, 'settings')
    await prefs.put('theme', theme)
    await prefs.flush()
    this.currentTheme = theme
  } catch (e) {
    // 静默失败——使用默认主题
  }
}

先 flush 再更新 @State——保证数据持久化成功后才切换 UI。 如果顺序反过来(先更新 @State 再 flush),用户看到主题切换了但 Preferences 写入失败——下次重启又回到默认主题。

读取 Preferences

private async loadTheme(): Promise<void> {
  try {
    const prefs = await preferences.getPreferences(this.context!, 'settings')
    const theme = await prefs.get('theme', ThemeType.CAPSULE)
    this.currentTheme = theme as ThemeType
  } catch (e) {
    // 使用默认主题
  }
}

默认值 ThemeType.CAPSULE——第一次打开 App 时是暖金主题。 as ThemeType 类型断言——Preferences 返回的是 PreferencesDataValue,需要断言为 ThemeType。

主题切换的刷新链路

用户点击主题标签
  → saveTheme()
  → prefs.put('theme', theme)     持久化
  → prefs.flush()                 写入文件
  → this.currentTheme = theme     更新 @State
  → build 重新执行                触发重渲染
  → getColors() 返回新主题        所有颜色更新
  → 所有 UI 元素刷新              背景/文字/按钮全部变色

从用户点击到 UI 刷新——四步。 关键是 this.currentTheme = theme 这一步——它是触发 build 重新执行的开关。没有这步,即使 Preferences 写入成功,UI 也不会变。

七、颜色传递的架构模式

模式一:@State + getColors()(Index 页面)

@State currentTheme: ThemeType = ThemeType.CAPSULE

private getColors(): ThemeColors {
  return getThemeColors(this.currentTheme)
}

build() {
  Column() {
    // 所有颜色通过 this.getColors() 获取
  }
}

Index 页面直接持有 currentTheme @State——切换主题后 build 自动刷新。 getColors() 是快捷方法,避免每次写 getThemeColors(this.currentTheme)

模式二:colors 参数传递(子组件)

// Index 传 colors 给子组件
MoodPicker({
  colors: this.getColors(),
  ...
})

// 子组件用 this.colors 渲染
@Component
struct MoodPicker {
  colors: ThemeColors = getThemeColors(ThemeType.CAPSULE)
  build() {
    Column() {
      // 用 this.colors.primary 等
    }
  }
}

子组件不持有 currentTheme——只接收 colors 参数。 这保证了子组件的独立性——它不需要知道当前是什么主题,只用传进来的颜色渲染。

两种模式的对比

特性 模式一(Index) 模式二(子组件)
持有 currentTheme
切换主题后自动刷新 是(父组件传新 colors)
依赖关系 依赖 ThemeType 依赖 ThemeColors
可复用性 低(和 ThemeType 耦合) 高(只依赖颜色对象)

子组件用模式二——高可复用性。 MoodPicker 在封印 Tab 和详情页都能用——只要传不同的 colors 参数即可。

八、透明度后缀的系统化

所有使用透明度的位置

位置 后缀 透明度 用途
统计卡片背景 100% surface 色本身
笔记本图标背景 ‘15’ 15% 轻度背景
MoodPicker 选中 ‘20’ 20% 中度背景
快捷操作图标 ‘12’ 12% 轻度背景
标签输入背景 ‘15’ 15% 轻度背景
可开启卡片 ‘#FFF8E1’ 固定 暖黄背景

透明度的设计规则

透明度 适用场景 视觉效果
10%~15% 图标背景、输入框 几乎不可见,微弱底色
15%~20% 选中状态、活跃状态 可感知,但不抢内容
20%~30% 强调、特殊状态 明显,引导注意力
30%+ 不使用 太深,影响内容可读性

透明度是主题系统里最容易被忽视的细节。 它控制"颜色的强度"——同一个 primary 色,10% 透明度是微弱底色,20% 是选中状态,100% 是按钮实色。三个层次让同一颜色在不同场景下有不同的视觉重量。

九、Spacing 和 BorderRadius 常量

Spacing 间距系统

export const Spacing = {
  xs: 4,    // 最小间距
  sm: 8,    // 小间距
  md: 16,   // 标准间距
  lg: 24,   // 大间距
  xl: 32    // 最大间距
}

BorderRadius 圆角系统

export const BorderRadius = {
  sm: 6,    // 小圆角(进度条、标签)
  md: 12,   // 中圆角(卡片、输入框)
  lg: 16,   // 大圆角(按钮、区块)
  full: 999 // 全圆角(胶囊形)
}

FontSize 字号系统

export const FontSize = {
  xs: 10,   // 辅助文字
  sm: 12,   // 说明文字
  md: 14,   // 正文
  lg: 16,   // 小标题
  xl: 20,   // 标题
  xxl: 24,  // 大标题
  hero: 32  // 数字展示
}

三套常量(Spacing/BorderRadius/FontSize)和颜色系统平行——共同构成 App 的设计语言。 颜色控制"看起来是什么感觉",间距控制"元素之间多远",圆角控制"多圆润",字号控制"多大"。

十、主题系统的设计原则

1. 颜色不硬编码

所有颜色通过 this.getColors()colors.xxx 获取——不在组件里写死 #6C5CE7#FFFFFF。这让主题切换成为可能——改一个变量,全局变色。

2. 透明度可预测

所有浅色背景都用 主色 + 透明度后缀——不随机选色。用户看到 primary + '15' 就知道这是"15% 透明度的主色",不需要看渲染结果。

3. 持久化优先

主题选择写入 Preferences——重启后保留。这是用户个性化设置的基本要求——没有人想每次打开 App 都重新选主题。

4. 默认值兜底

所有主题相关的方法都有 default/fallback——getThemeColors 的 default 返回暖金主题,loadTheme 的 catch 使用默认值。任何异常都不会导致 App 崩溃或显示异常。

5. 单一数据源

currentTheme 是唯一的主题状态——所有组件通过它(或它派生的 colors)获取颜色。不存在"这个组件用 currentTheme,那个组件用 localStorage"的混乱情况。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐