搭建全局主题与 Design Token

本文是《HarmonyOS NEXT 企业级开发实战:30篇打造智能记账APP》系列的第 03 篇,对应 Git Tag v0.0.3。承接第 02 篇的工程骨架,本篇设计 HarmonyLedger 的色彩体系、字号体系、间距体系,并封装深色模式支持,为后续所有页面提供统一的视觉基础。

前言

企业级应用的视觉一致性是 用户体验的基础。如果没有统一的主题规范,每个页面各写一套颜色、字号、间距,最终会演变成视觉灾难。Design Token 是业界主流的设计系统方案,通过将视觉要素抽象为可复用的 Token,实现主题的集中管理与一键切换。

本文将带你:

  1. 设计 HarmonyLedger 的色彩体系(收入绿/支出红/预算蓝/统计紫)
  2. 搭建字号体系与间距体系
  3. 用 ArkTS 常量类封装 Design Token
  4. 通过 AppStorage 实现深色模式动态切换

企业级核心原则:视觉要素必须 集中定义、统一引用、禁止硬编码。参考 Material Design Tokens 了解业界主流方案。


一、Design Token 概念

1.1 什么是 Design Token

Design Token 是设计系统的原子单元,用于存储视觉要素的值。一个 Token 通常包含:

Token 属性 说明 示例
name Token 命名 color.income.primary
value Token 取值 #34C759
description 用途描述 “收入主色,用于收入账单卡片”
platform 适用平台 harmonyos

1.2 Token 分层

HarmonyLedger 的 Token 分 三层

Layer 1: Base Token(基础色板)
  └ primary, secondary, accent, neutral 色系
Layer 2: Semantic Token(语义化 Token)
  └ income, expense, budget, statistic
Layer 3: Component Token(组件 Token)
  └ bill.card.background, bill.card.text.primary

设计要点:禁止组件直接引用 Base Token,必须通过 Semantic Token 中转。这样色板调整时只需改 Base Token,所有 Semantic Token 自动继承。


二、色彩体系设计

2.1 色彩语义规划

HarmonyLedger 采用 语义化色彩 方案,不同业务场景使用不同色系:

语义 色系 主色 浅色 应用场景
收入 绿色 #34C759 #E8F8EE 收入账单卡片、收入金额
支出 红色 #FF3B30 #FFEBE9 支出账单卡片、支出金额
预算 蓝色 #007AFF #E3F0FF 预算进度条、预算提醒
统计 紫色 #AF52DE #F5E8FB 图表图例、统计标题
中性 灰色 #8E8E93 #F2F2F7 背景、分割线、次要文字

2.2 颜色定义文件

// theme/Colors.ets - 色彩 Token 定义
export class AppColors {
  // ===== Base Token:基础色板 =====
  static readonly Green500: string = '#34C759';
  static readonly Green100: string = '#E8F8EE';
  static readonly Red500: string = '#FF3B30';
  static readonly Red100: string = '#FFEBE9';
  static readonly Blue500: string = '#007AFF';
  static readonly Blue100: string = '#E3F0FF';
  static readonly Purple500: string = '#AF52DE';
  static readonly Purple100: string = '#F5E8FB';

  // ===== Semantic Token:语义化色 =====
  static readonly Income: string = AppColors.Green500;
  static readonly IncomeLight: string = AppColors.Green100;
  static readonly Expense: string = AppColors.Red500;
  static readonly ExpenseLight: string = AppColors.Red100;
  static readonly Budget: string = AppColors.Blue500;
  static readonly BudgetLight: string = AppColors.Blue100;
  static readonly Statistic: string = AppColors.Purple500;
  static readonly StatisticLight: string = AppColors.Purple100;

  // ===== 中性色(浅色模式) =====
  static readonly Background: string = '#F2F2F7';
  static readonly CardBackground: string = '#FFFFFF';
  static readonly PrimaryText: string = '#1C1C1E';
  static readonly SecondaryText: string = '#8E8E93';
  static readonly Separator: string = '#E5E5EA';
  static readonly Border: string = '#D1D1D6';
}

2.3 深色模式色板

// theme/DarkColors.ets - 深色模式色板
export class AppDarkColors {
  // 语义色保持一致(保证品牌识别)
  static readonly Income: string = '#30D158';
  static readonly IncomeLight: string = '#1B2A1E';
  static readonly Expense: string = '#FF453A';
  static readonly ExpenseLight: string = '#2B1C1C';
  static readonly Budget: string = '#0A84FF';
  static readonly BudgetLight: string = '#0A1F2B';
  static readonly Statistic: string = '#BF5AF2';
  static readonly StatisticLight: string = '#1F0F2B';

  // 中性色反转(深色模式)
  static readonly Background: string = '#000000';
  static readonly CardBackground: string = '#1C1C1E';
  static readonly PrimaryText: string = '#FFFFFF';
  static readonly SecondaryText: string = '#8E8E93';
  static readonly Separator: string = '#38383A';
  static readonly Border: string = '#545458';
}

深色模式原则:语义色保持品牌识别,中性色反转。参考 Apple Dark Mode Guidelines 了解业界主流实践。


三、字号与间距体系

3.1 字号体系

HarmonyLedger 采用 4 倍数 字号体系,共 6 个字号阶梯:

Token 字号 用途 示例
FontSizeXS 12 辅助文字 时间戳、标签
FontSizeSM 14 次要正文 备注、描述
FontSizeMD 16 默认正文 账单备注
FontSizeLG 18 标题 卡片标题
FontSizeXL 22 页面标题 页面 Header
FontSizeXXL 28 大数字 金额汇总
// theme/Typography.ets - 字号 Token
export class AppFontSize {
  static readonly XS: number = 12;
  static readonly SM: number = 14;
  static readonly MD: number = 16;
  static readonly LG: number = 18;
  static readonly XL: number = 22;
  static readonly XXL: number = 28;
  static readonly Display: number = 36;  // 首页大数字
}

3.2 间距体系

同样采用 4 倍数 间距体系:

Token 间距 用途
SpaceXS 4 细微间距(图标与文字)
SpaceSM 8 小间距(卡片内元素)
SpaceMD 16 中间距(卡片 Padding)
SpaceLG 20 大间距(卡片间)
SpaceXL 24 页面边距
SpaceXXL 32 区块间距
// theme/Spacing.ets - 间距 Token
export class AppSpace {
  static readonly XS: number = 4;
  static readonly SM: number = 8;
  static readonly MD: number = 16;
  static readonly LG: number = 20;
  static readonly XL: number = 24;
  static readonly XXL: number = 32;
  static readonly PagePadding: number = 20;  // 页面统一 Padding
  static readonly CardPadding: number = 16;  // 卡片统一 Padding
  static readonly CardRadius: number = 16;   // 卡片统一圆角
}

3.3 字重体系

// theme/Typography.ets - 字重 Token
export class AppFontWeight {
  static readonly Regular: FontWeight = FontWeight.Regular;
  static readonly Medium: FontWeight = FontWeight.Medium;
  static readonly Bold: FontWeight = FontWeight.Bold;
}

设计要点:字号、间距、字重统一用 Token 管理,禁止在页面里硬编码 fontSize(18) 这样的数字。


四、主题管理器封装

4.1 ThemeManager.ets

通过 AppStorage 实现深色模式动态切换:

// theme/ThemeManager.ets
import { AppStorage } from '@kit.ArkUI';
import { AppColors } from './Colors';
import { AppDarkColors } from './DarkColors';

export enum ThemeMode {
  LIGHT = 'light',
  DARK = 'dark',
  AUTO = 'auto'  // 跟随系统
}

export class ThemeManager {
  private static readonly KEY_THEME_MODE = 'theme_mode';
  private static currentMode: ThemeMode = ThemeMode.AUTO;

  /** 初始化主题(从 Preferences 读取) */
  static init(mode: ThemeMode): void {
    this.currentMode = mode;
    AppStorage.setOrCreate(this.KEY_THEME_MODE, mode);
    this.applyTheme(mode);
  }

  /** 切换主题 */
  static switch(mode: ThemeMode): void {
    this.currentMode = mode;
    AppStorage.set(this.KEY_THEME_MODE, mode);
    this.applyTheme(mode);
  }

  /** 应用主题到全局 */
  private static applyTheme(mode: ThemeMode): void {
    if (mode === ThemeMode.DARK) {
      this.applyDarkColors();
    } else {
      this.applyLightColors();
    }
  }

  private static applyLightColors(): void {
    AppStorage.setOrCreate('color.background', AppColors.Background);
    AppStorage.setOrCreate('color.card', AppColors.CardBackground);
    AppStorage.setOrCreate('color.text.primary', AppColors.PrimaryText);
    AppStorage.setOrCreate('color.text.secondary', AppColors.SecondaryText);
    AppStorage.setOrCreate('color.separator', AppColors.Separator);
  }

  private static applyDarkColors(): void {
    AppStorage.setOrCreate('color.background', AppDarkColors.Background);
    AppStorage.setOrCreate('color.card', AppDarkColors.CardBackground);
    AppStorage.setOrCreate('color.text.primary', AppDarkColors.PrimaryText);
    AppStorage.setOrCreate('color.text.secondary', AppDarkColors.SecondaryText);
    AppStorage.setOrCreate('color.separator', AppDarkColors.Separator);
  }

  static getCurrentMode(): ThemeMode {
    return this.currentMode;
  }
}

4.2 在 Ability 中初始化主题

// entryability/EntryAbility.ets
import { ThemeManager } from '../theme/ThemeManager';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 初始化主题(默认跟随系统)
    ThemeManager.init(ThemeMode.AUTO);
  }
}

关键技术AppStorage.setOrCreate 用于写入全局响应式数据,任何 @StorageLink 绑定会自动刷新 UI。这是 ArkUI 状态管理的核心机制。


五、主题使用示例

5.1 在组件中引用主题色

// components/BillCard.ets
import { AppColors } from '../theme/Colors';
import { AppFontSize } from '../theme/Typography';
import { AppSpace } from '../theme/Spacing';

@Component
export struct BillCard {
  @Prop money: number;
  @Prop type: string;  // 'income' | 'expense'

  build() {
    Column() {
      Text(`¥${this.money}`)
        .fontSize(AppFontSize.XL)
        .fontColor(this.type === 'income' ? AppColors.Income : AppColors.Expense)
        .fontWeight(FontWeight.Bold)
    }
    .padding(AppSpace.CardPadding)
    .backgroundColor(AppColors.CardBackground)
    .borderRadius(AppSpace.CardRadius)
  }
}

5.2 响应式深色模式

// pages/HomeView.ets
@Entry
@Component
struct HomeView {
  @StorageLink('color.background') bgColor: string = '#F2F2F7';
  @StorageLink('color.text.primary') textColor: string = '#1C1C1E';

  build() {
    Column() {
      Text('HarmonyLedger 鸿蒙记账')
        .fontColor(this.textColor)
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.bgColor)
  }
}

深色模式切换:调用 ThemeManager.switch(ThemeMode.DARK) 后,所有 @StorageLink 绑定的颜色会自动刷新,UI 即时响应。


六、主题常量集中管理

6.1 Theme Index 文件

// theme/index.ets - 主题统一导出
export { AppColors } from './Colors';
export { AppDarkColors } from './DarkColors';
export { AppFontSize } from './Typography';
export { AppFontWeight } from './Typography';
export { AppSpace } from './Spacing';
export { ThemeManager, ThemeMode } from './ThemeManager';

6.2 资源文件中的颜色定义

除了 ArkTS 常量,还需在 resources/base/element/color.json 定义静态色:

{
  "color": [
    { "name": "start_window_background", "value": "#F2F2F7" },
    { "name": "icon_background", "value": "#FFFFFF" }
  ]
}
资源色 用途 定义位置
start_window_background 启动屏背景 resources/base/element/color.json
icon_background 应用图标背景 resources/base/element/color.json
动态主题色 UI 组件 theme/Colors.ets(ArkTS 常量)

七、最佳实践

7.1 禁止硬编码颜色

// ❌ 错误:硬编码颜色
Text('¥100')
  .fontColor('#FF3B30')

// ✅ 正确:引用 Token
Text('¥100')
  .fontColor(AppColors.Expense)

为什么禁止硬编码?

  1. 深色模式失效:硬编码颜色无法响应主题切换
  2. 维护困难:色板调整需全局搜索替换
  3. 品牌不一致:多人协作时颜色可能跑偏

7.2 语义色与组件色分层

组件禁止直接引用 Base Token:
  ❌ Text().fontColor(AppColors.Red500)
  ✅ Text().fontColor(AppColors.Expense)

只有 Semantic Token 可以引用 Base Token:
  static readonly Expense = AppColors.Red500;

7.3 深色模式测试矩阵

测试场景 浅色模式 深色模式
首页背景 #F2F2F7 #000000
卡片背景 #FFFFFF #1C1C1E
主文字 #1C1C1E #FFFFFF
收入金额 #34C759 #30D158
支出金额 #FF3B30 #FF453A

八、运行验证

8.1 编译检查

hvigorw assembleHap --mode module -p product=default

8.2 视觉验证

  1. 运行应用,首页背景应为 #F2F2F7(浅灰)
  2. 调用 ThemeManager.switch(ThemeMode.DARK),首页应即时变为 #000000(纯黑)
  3. 文字颜色应同步切换为白色

在这里插入图片描述
在这里插入图片描述


九、常见问题

9.1 AppStorage 不刷新

现象 原因 解决
@StorageLink 不响应 未调用 setOrCreate 初始化 Ability onCreate 中先 ThemeManager.init
切换无效果 ThemeManager.switch 未调用 AppStorage.set 检查 applyTheme 方法调用链

9.2 颜色显示错误

// 错误:颜色字符串格式不对
.backgroundColor('rgb(255, 0, 0)')  // ❌ ArkUI 不支持 rgb()

// 正确:统一用 hex
.backgroundColor('#FF0000')  // ✅

9.3 深色模式跟随系统

// 通过 ConfigurationConstant 读取系统主题
import { ConfigurationConstant } from '@kit.AbilityKit';

onConfigurationUpdate(config: ConfigurationConstant): void {
  const colorMode = config.colorMode;
  if (colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK) {
    ThemeManager.switch(ThemeMode.DARK);
  } else {
    ThemeManager.switch(ThemeMode.LIGHT);
  }
}

十、Git 提交

10.1 Commit Message

git add .
git commit -m "feat(theme): 搭建全局主题与 Design Token 系统

- 设计色彩体系(收入绿/支出红/预算蓝/统计紫)
- 定义字号、间距、字重 Token
- 封装 ThemeManager 支持深浅模式切换
- 通过 AppStorage 实现响应式主题
- 新增 theme/ 目录集中管理视觉 Token"

10.2 CHANGELOG

## [v0.0.3] - 2026-07-27
### Added
- theme/Colors.ets:浅色模式色板(Base + Semantic Token)
- theme/DarkColors.ets:深色模式色板
- theme/Typography.ets:字号、字重 Token
- theme/Spacing.ets:间距 Token
- theme/ThemeManager.ets:主题管理器,支持 AppStorage 响应式切换
- EntryAbility 初始化主题

总结

本文完整介绍了 HarmonyLedger 的 Design Token 体系搭建,涵盖色彩、字号、间距、字重四大视觉要素。通过本篇你可以:

  • 理解 Design Token 的三层架构(Base / Semantic / Component)
  • 设计语义化色彩体系(收入绿/支出红/预算蓝/统计紫)
  • 用 ArkTS 常量类封装 Token,禁止硬编码
  • 通过 AppStorage + ThemeManager 实现深色模式动态切换
  • 在组件中正确引用 Token 并响应主题变化

下一篇预告:《实现底部 Tab 导航》将使用 ArkUI 的 Tabs 组件搭建首页、统计、预算、我的四个底导航 Tab,并封装 AppTabBar 通用组件。


如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源

Logo

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

更多推荐