羽球联盟 HarmonyOS NEXT 实战系列 (06/20):主题Token、Resource颜色与深色模式准备
文章导读
- 颜色、字号、间距、圆角先抽成主题 token,页面层只引用语义名称。
- 会随系统外观变化的颜色,交给
Resource和base / dark资源目录处理。 - 标签色、徽章色这类业务色保留为字符串,避免把数据模型和资源系统绑死。
页面效果
主题方案最怕只在一个页面里看起来成立,切到别的入口就失真。羽球联盟里,首页搜索框、Banner、功能宫格、列表卡片和底部导航都在复用同一组主色、文字层级、卡片背景和圆角;这意味着主题 token 已经不只是样式表,而是页面结构的一部分。

实战拆解
如果页面里直接散落 #0F7B5F、16、8 这类裸值,短期开发会很快,后面改主色、补暗色或统一卡片节奏时就会很痛。羽球联盟把这层公共样式拆到 AppColors 和 AppSizes,页面只表达“这是主色”“这是卡片背景”“这里用 16 的间距”,而不再关心底层具体值。
这里最值得单独讲清楚的是颜色分层。AppColors.primary、textPrimary、bgPage 这类会随着系统外观变化的颜色,全部走 Resource;而赛事状态徽章、标签强调色这类更偏业务语义、常常需要从模型字段直接拼接到组件里的颜色,则继续保留为字符串。两者都叫“颜色”,但生命周期完全不同,混在一起后续维护会非常吃力。
另一个关键点是深色模式不是在页面末端补一层 if dark then ...。这个项目在 EntryAbility 启动时从 PrefStore 读取用户偏好,再调用 setColorMode(),让整个应用从资源解析层开始进入对应色表。这样首页、详情页、个人中心和设置页用到的 Resource 色会一起切换,而不是每页各补一套判断。
关键代码
export class AppColors {
static readonly primary: Resource = $r('app.color.primary');
static readonly primaryLight: Resource = $r('app.color.primary_light');
static readonly primaryDark: Resource = $r('app.color.primary_dark');
static readonly textPrimary: Resource = $r('app.color.text_primary');
static readonly textSecondary: Resource = $r('app.color.text_secondary');
static readonly textTertiary: Resource = $r('app.color.text_tertiary');
static readonly bgPage: Resource = $r('app.color.bg_page');
static readonly bgCard: Resource = $r('app.color.bg_card');
static readonly bgGray: Resource = $r('app.color.bg_gray');
static readonly divider: Resource = $r('app.color.divider');
static readonly badgeOngoing: string = '#0F7B5F';
static readonly badgeFinished: string = '#999999';
static readonly badgePending: string = '#FF9F2D';
}
export class AppSizes {
static readonly s8: number = 8;
static readonly s12: number = 12;
static readonly s16: number = 16;
static readonly s24: number = 24;
static readonly r8: number = 8;
static readonly r12: number = 12;
static readonly r16: number = 16;
static readonly f14: number = 14;
static readonly f16: number = 16;
static readonly f20: number = 20;
}
PrefStore.getInstance().init(this.context).then(() => {
const mode: string = AppStorage.get<string>('setting_darkMode') ?? 'system';
let cm: ConfigurationConstant.ColorMode = ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET;
if (mode === 'dark') {
cm = ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
} else if (mode === 'light') {
cm = ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
}
this.context.getApplicationContext().setColorMode(cm);
});
这两段放在一起看,主题体系才完整:前者定义了“页面该用哪些语义 token”,后者保证这些 token 会在应用启动阶段解析到正确的浅色或深色资源值。
取舍分析
把 token 抽得太细,容易让页面代码读起来很绕;抽得太粗,又会回到满地散值。这个项目当前停在一个比较实用的层级:颜色、间距、圆角、字号统一收进主题类,页面只用语义名;而业务上波动更大的标签色、徽章色保留为字符串。这样既能吃到资源系统带来的深浅色适配,又不至于让所有颜色都必须先去资源表里登记一遍。
深色模式也没有做成一套完全独立的页面主题,而是沿用同样的 token 名称,只替换 base 和 dark 目录下的资源值。这样新增页面时,不需要先考虑两套样式分支,只要遵守已有 token 体系,大部分适配会自动发生。
设计落点
- 颜色 token 优先表达语义,不直接暴露裸值。
- 会跟系统外观变化的颜色走
Resource,业务标签色保留为字符串。 - 间距、圆角、字号和颜色一起收敛,避免“颜色统一了,布局节奏还是散的”。
- 深色模式在应用入口设置,页面不重复写判断逻辑。
易踩坑
- 不要把
Resource色和字符串色混着往同一个组件接口里塞,后面很难分清谁应该跟随深色模式变化。 - 不要只抽主色,不抽背景、分割线、文字层级。主题体系一旦只有一个绿色常量,实际维护价值会很弱。
- 深色模式如果靠页面内部分支临时覆盖,页面一多就会失控,最好从资源层统一处理。
验证方式
- 切换“跟随系统 / 浅色 / 深色”,确认首页和详情页的背景、文字和按钮都能一起变化。
- 调整
primary后检查 Banner 指示器、底部导航高亮、按钮和标签是否同步变化。 - 打开设置页和个人中心,确认卡片背景、分割线和正文层级没有出现暗色下发灰或发脏的问题。
参考资料
- [深浅色模式适配(官方文档)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ui-dark-light-color-adaptation-V5)
- [资源分类与访问(官方文档)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/resource-categories-and-access)
- [UIAbilityContext.setColorMode(官方文档)](https://developer.huawei.com/consumer/en/doc/harmonyos-references/js-apis-inner-application-uiabilitycontext)
小结
这篇的核心不是“把颜色常量搬个家”,而是先把语义层建立起来,再让资源系统和入口生命周期共同接管深浅色切换。这样后续继续扩页面时,样式一致性和暗色适配都能跟着走,不需要每次重做一遍主题整理。
更多推荐

所有评论(0)