【时光清单|17】HarmonyOS ArkTS 亮暗色与视觉令牌实战:集中颜色、间距和交互状态避免页面割裂
【时光清单|17】HarmonyOS ArkTS 亮暗色与视觉令牌实战:集中颜色、间距和交互状态避免页面割裂
**本轮证据边界:**本文基于
D:\huawei\one8当前生产源码、亮暗资源与项目错误记录做静态复核。本轮没有执行构建、安装、模拟器、真机、截图基线、服务卡片验证或 AGC 检查;文中的当前结论不替代产物与设备验收。阅读约定:“当前事实”来自文中点名的源码与资源;“历史证据”只复述
PROJECT_ERRORS.md已有记录;三态模式、主题持久化、完整ThemeTokens和交互状态矩阵均为建议实现,不代表当前工程已经落地。
亮暗色适配最难的部分不是把白色背景换成深色,而是保证同一个语义在所有页面、所有主题和所有交互状态下都一致。首页把主色写成 #2C5F7C,详情页从资源里读取 brand_primary,主题页又从 AppTheme.primaryColor 取值,服务卡片继续固定白底。每一段代码单独看都能显示,组合起来却可能出现主题切换只改了页面背景、卡片仍保持旧颜色,深色模式下状态栏图标看不清,选中态和禁用态没有统一对比度的情况。
时光清单 的真实源码同时包含三套与视觉有关的机制。第一套是资源型语义颜色:AppColors 通过 $r('app.color.xxx') 引用 base 与 dark 目录中的同名资源。第二套是用户可选主题:Theme.ets 定义国风山水、治愈日落、极简白、深夜星空、樱花树五组配色,AppStore.applyTheme() 根据系统明暗模式把背景、卡片、主色和文字色写入 AppStorage。第三套是尺寸令牌:AppSpacing、AppRadius、AppFontSize 与 AppFontWeight 集中管理间距、圆角和字号。
这三套机制已经构成视觉系统骨架,但也存在真实边界:部分页面读取动态主题字符串,部分组件只使用资源型 AppColors;ThemeService 维护自己的静态主题 ID,却没有在其他源码中被调用;服务卡片与少数组件仍保留硬编码颜色;当前主题选择在本文复核的代码中没有展示跨启动持久化。本文从这个真实 ArkTS 工程出发,说明怎样划清颜色所有权、统一亮暗色链路、补齐交互状态,并用对比度与页面矩阵验证结果。

本文将解决:
- 资源颜色、用户主题和运行时共享状态如何分工。
- base/dark 同名资源怎样自动解析。
- 系统明暗变化如何驱动 AppStorage 与系统栏更新。
- 为什么颜色、间距、圆角、字号都必须语义化。
- 选中、按下、禁用、错误和空状态如何纳入令牌。
- 怎样发现硬编码颜色、双主题真源和对比度风险。
本文唯一标记:
CSDN-SERIES:ALL-163209540
一、先把“主题”拆成三层
项目里“主题”至少有三种含义:
| 层次 | 真实实现 | 适合解决的问题 |
|---|---|---|
| 系统颜色模式 | base/dark 资源、DARK_MODE | 亮色与暗色可读性 |
| 用户风格主题 | AppTheme 五套配色 | 国风、日落、星空等品牌风格 |
| 组件视觉令牌 | AppColors、AppSpacing、AppRadius | 跨页面一致性 |
如果把三层都压进一个 isDark 布尔值,就无法表达“暗色模式下的樱花主题”;如果每个页面自己组合,又会形成大量分支。更清晰的规则是:
系统模式决定亮/暗表面
用户主题决定品牌色与风格
语义令牌决定组件应该使用哪类颜色
页面只消费最终令牌
页面不应该知道某个深色背景到底是 #1A1A2E 还是 #121212,只应知道这里需要 bgPage;按钮不应该判断当前是樱花还是星空,只应请求 actionPrimary、textOnPrimary 和 pressedOverlay。
二、AppColors:把资源名变成 ArkTS 语义入口
Colors.ets 没有直接保存十六进制字符串,而是引用资源:
export class AppColors {
static readonly brandPrimary: Resource =
$r('app.color.brand_primary');
static readonly bgPage: Resource =
$r('app.color.bg_page');
static readonly bgSurface: Resource =
$r('app.color.bg_surface');
static readonly textPrimary: Resource =
$r('app.color.text_primary');
static readonly divider: Resource =
$r('app.color.divider');
static readonly danger: Resource =
$r('app.color.danger');
}
这样的好处是调用点表达语义:
Text('主题切换')
.fontColor(AppColors.textPrimary)
Column()
.backgroundColor(AppColors.bgSurface)
.borderRadius(AppRadius.md)
开发者看到代码就能判断用途,不必记忆色值。设计调整时也不需要逐页替换。资源型令牌还能利用 HarmonyOS 资源限定目录,让系统模式变化后解析同名资源的另一组值。

三、base 与 dark 必须同名,语义必须相同
项目在 base 和 dark 下定义了相同名称:
{ "name": "bg_page", "value": "#F5F0E8" }
{ "name": "bg_surface", "value": "#FFFFFF" }
{ "name": "text_primary", "value": "#1A1A1A" }
{ "name": "text_secondary", "value": "#4A4A4A" }
对应 dark:
{ "name": "bg_page", "value": "#1A1A2E" }
{ "name": "bg_surface", "value": "#2D2D44" }
{ "name": "text_primary", "value": "#E8E8F0" }
{ "name": "text_secondary", "value": "#D0D0E4" }
关键不只是“名字一致”,还要保证语义一致。text_secondary 在两套资源中都应表示次要正文,而不是亮色里表示说明文字、暗色里突然变成禁用文字。否则组件虽然自动换色,视觉层级却会变化。
新增令牌时应成对提交:
base/element/color.json
dark/element/color.json
theme/Colors.ets
组件使用点
对比度验证记录
漏掉 dark 同名资源通常不会在亮色开发阶段暴露,却可能在系统切换后产生不可读文本或退回错误默认值。
本轮还对资源中几组代表性前景色和背景色做了独立计算。亮色的 text_primary / bg_page 为 15.34:1,text_secondary / bg_surface 为 8.86:1,text_tertiary / bg_surface 为 8.19:1;暗色对应组合分别为 14.00:1、8.80:1 和 7.79:1。它们说明当前抽样组合在数值上有较充足余量,但只能证明所列色值组合,不能外推到透明度叠加、图片背景、按钮所有状态或设备最终像素。历史记录里的“约 6:1 到 11:1”与本轮计算口径也不同,不能混成同一批结果。
四、AppTheme:五套风格配色不是资源暗色的替代品
Theme.ets 定义了五个 ThemeId:
export type ThemeId =
'chinese_ink' |
'sunset' |
'minimal_white' |
'starry_night' |
'sakura';
每个主题包含亮色主色、背景、卡片和文字,以及暗色主色、背景和卡片:
export interface AppTheme {
id: ThemeId;
name: string;
primaryColor: string;
secondaryColor: string;
backgroundColor: string;
cardColor: string;
textPrimary: string;
textSecondary: string;
darkPrimaryColor: string;
darkBackgroundColor: string;
darkCardColor: string;
}
这是一套用户风格数据。它允许“日落亮色”和“日落暗色”共享品牌语义。资源型 AppColors 则负责通用表面、状态色和组件默认值。二者应该有明确合并点,而不是让页面随机选择。
当前合并点主要在 AppStore.applyTheme()。这正是值得保留并继续收敛的架构位置。
五、AppStore.applyTheme 是运行时令牌解析器
真实实现先读取系统暗色状态,再从 AppTheme 中选择对应字段:
static applyTheme(themeId: ThemeId): void {
const theme = getThemeById(themeId);
const isDark =
AppStorage.get<boolean>(StateKeys.DARK_MODE) ?? false;
const bg = isDark
? theme.darkBackgroundColor
: theme.backgroundColor;
const card = isDark
? theme.darkCardColor
: theme.cardColor;
const primary = isDark
? theme.darkPrimaryColor
: theme.primaryColor;
const textPrimary =
isDark ? '#E8E8F0' : theme.textPrimary;
const textSecondary =
isDark ? '#D0D0E4' : theme.textSecondary;
AppStore.setStorageString(StateKeys.CURRENT_THEME, themeId);
AppStore.setStorageString(StateKeys.THEME_BG, bg);
AppStore.setStorageString(StateKeys.THEME_CARD, card);
AppStore.setStorageString(StateKeys.THEME_PRIMARY, primary);
AppStore.setStorageString(
StateKeys.THEME_TEXT_PRIMARY,
textPrimary
);
AppStore.setStorageString(
StateKeys.THEME_TEXT_SECONDARY,
textSecondary
);
AppStore.updateStatusBarStyle(bg);
}
它做了两件事:把“主题 ID + 明暗模式”解析成最终色值,并把这些值分发到 AppStorage。页面通过 @StorageLink 响应变化,不必逐个接收回调。
不过当前动态令牌只覆盖背景、卡片、主色和两级文字。边框、分割线、成功、警告、危险、禁用、按下蒙层仍主要来自资源型 AppColors。这就形成了混合系统,需要进一步定义优先级。
六、混合令牌的真实风险:背景变了,卡片未必跟着变
根页面和部分壳层读取:
@StorageLink(StateKeys.THEME_BG)
themeBg: string = '#F5F0E8';
@StorageLink(StateKeys.THEME_PRIMARY)
themePrimary: string = '#2C5F7C';
而很多业务卡片使用:
.backgroundColor(AppColors.bgSurface)
.fontColor(AppColors.textPrimary)
用户从国风切到樱花时,themeBg 和 themePrimary 会变化,但 AppColors.bgSurface 仍由 base/dark 资源决定,不一定等于樱花主题的 cardColor。页面可能因此出现“主题背景已切换,卡片仍是默认体系”的割裂。
解决方法不是删掉资源令牌,而是统一最终消费路径。可以选择:
- 用户主题只控制品牌色与页面背景,通用表面始终由资源决定。
- 用户主题完整控制背景、表面、文字和品牌色,资源只做启动默认。
- 建立 ThemeTokens 对象,AppStore 每次计算完整语义集合。
无论选哪一种,都要写成明确规则。当前源码更接近第二种与第一种混用,优先工作是梳理所有消费点。

七、建议把最终视觉值收敛成完整 ThemeTokens
可以为运行时主题建立语义对象:
export interface ThemeTokens {
bgPage: string;
bgSurface: string;
bgSurfaceAlt: string;
textPrimary: string;
textSecondary: string;
textTertiary: string;
actionPrimary: string;
textOnPrimary: string;
borderDefault: string;
divider: string;
success: string;
warning: string;
danger: string;
pressedOverlay: string;
disabledFill: string;
disabledText: string;
}
随后只有主题解析器负责生成它:
function resolveThemeTokens(
theme: AppTheme,
dark: boolean
): ThemeTokens {
return {
bgPage: dark
? theme.darkBackgroundColor
: theme.backgroundColor,
bgSurface: dark
? theme.darkCardColor
: theme.cardColor,
bgSurfaceAlt: dark ? '#3A3A52' : '#F0EDE5',
textPrimary: dark ? '#E8E8F0' : theme.textPrimary,
textSecondary: dark ? '#D0D0E4' : theme.textSecondary,
textTertiary: dark ? '#C4C4D8' : '#4F4F4F',
actionPrimary: dark
? theme.darkPrimaryColor
: theme.primaryColor,
textOnPrimary: dark ? '#101018' : '#FFFFFF',
borderDefault: dark ? '#3A3A52' : '#E5E0D8',
divider: dark ? '#4A4A63' : '#D8D2C8',
success: dark ? '#5CD07A' : '#2E7D32',
warning: dark ? '#F0C040' : '#7A5A00',
danger: dark ? '#FF79A8' : '#B61B64',
pressedOverlay: dark ? '#24FFFFFF' : '#14000000',
disabledFill: dark ? '#3A3A52' : '#E8E8E8',
disabledText: dark ? '#8E8EA0' : '#777777'
};
}
这是演进示例,不声称当前工程已经存在。它的价值是让所有页面消费同一套最终语义,避免一半走资源、一半走 AppStorage 字符串。
八、系统明暗变化的主链路已连接,但消费端仍需验收
EntryAbility.onConfigurationUpdate() 读取新的颜色模式:
onConfigurationUpdate(newConfig: Configuration): void {
const isDark =
newConfig.colorMode ===
ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
const currentDark =
AppStorage.get<boolean>(StateKeys.DARK_MODE) ?? false;
if (isDark !== currentDark) {
AppStore.onDarkModeChanged(isDark);
}
}
onDarkModeChanged() 写入共享状态并重新应用当前主题:
static onDarkModeChanged(isDark: boolean): void {
AppStorage.set<boolean>(StateKeys.DARK_MODE, isDark);
const themeId = (
AppStorage.get<string>(StateKeys.CURRENT_THEME)
?? 'chinese_ink'
) as ThemeId;
AppStore.applyTheme(themeId);
}
这条链保证系统从亮切暗时,不只是资源目录变化,运行时用户主题也会重新计算。应用启动时还调用 setColorMode(COLOR_MODE_NOT_SET),默认跟随系统。
需要验证的细节是:每个组件到底使用资源型颜色还是动态颜色,以及硬编码颜色是否绕开了两条链。
九、当前“跟随系统、浅色、深色”三态入口仍有断点
ProfileView 同时展示“跟随系统”“浅色”“深色”三个模式项,但当前 ModeChip 的点击分支只处理 dark 和 light:
if (mode === 'dark') {
AppStore.applyColorMode(true);
} else if (mode === 'light') {
AppStore.applyColorMode(false);
}
当参数为 auto 时没有执行任何操作,因此“跟随系统”现在只是一个可见标签,不能从强制亮色或强制暗色恢复到 COLOR_MODE_NOT_SET。三个 Chip 也使用相同背景、文字和圆角,没有根据当前模式渲染选中标记。DARK_MODE 又是布尔值,只能表示当前亮或暗,无法区分“正在跟随系统且系统为暗色”与“用户强制暗色”。这三种状态在产品语义上并不相同。
建议将用户偏好定义为 auto | light | dark,把“偏好模式”和“系统当前解析结果”分成两个状态。点击 auto 时显式调用 setColorMode(COLOR_MODE_NOT_SET);配置变化时只在偏好为 auto 的前提下跟随;界面使用勾选、边框或图标展示当前偏好。以上是建议实现,当前源码尚未具备这条完整三态链路。
十、历史证据只能解释修复背景,不能替代本轮验证
PROJECT_ERRORS.md 记录了 2026 年 5 月 20 日两类相关问题。第一类是审核指出 Text 控件对比度为 1.59,记录中的修复包括加深亮色次级、三级文字,提亮暗色对应文字,并调整底部 Tab 与设置页弱文本。第二类是“深夜星空”导致 Tab 字体看不清,记录把原因归到主题传播不完整、setOrCreate 未覆盖已有状态和 Tab 外壳没有消费主题文字色,随后改为显式覆盖并让 MainTabShell 绑定主题次级文字。
记录还写有当时 assembleHap 成功,但那是历史修复批次的结果。本轮没有重新构建,也没有在 Mate X5、其他设备、深浅色切换和五套主题组合上复测。因此本文可以引用这些记录解释为什么源码出现显式覆盖与 Tab 绑定,不能写成“当前所有主题已经通过构建和真机审核”。
十一、主题设置页展示了正确的单向更新
ThemeSettingsView 读取共享主题 ID:
@StorageLink(StateKeys.CURRENT_THEME)
currentThemeId: string = 'chinese_ink';
@State
selectedTheme: ThemeId = 'chinese_ink';
用户点击某个主题后:
.onClick(() => {
this.selectedTheme = theme.id;
AppStore.applyTheme(theme.id);
})
页面本地 selectedTheme 负责立即更新选中态,AppStore.applyTheme() 更新跨页面颜色。列表通过主题数组展示预览色块,选中项有背景、边框和“使用中”文本,不只依赖色差。
这里也暴露两个后续点。第一,当前选择没有在本文复核的代码中写入 Preferences,重新启动时 AppStore.bootstrap() 仍调用 applyTheme('chinese_ink');第二,预览色块总是使用主题亮色字段,系统暗色下没有展示暗色预览。若产品要求跨启动记住主题,应把 ThemeId 持久化,并在 bootstrap 读取后再应用。
十二、ThemeService、ThemeManager 与 ThemeConfig 是潜在平行真源
ThemeService 内部还有:
private static currentThemeId: ThemeId = 'chinese_ink';
static setTheme(id: ThemeId): void {
ThemeService.currentThemeId = id;
}
static getTheme(): AppTheme {
return getThemeById(ThemeService.currentThemeId);
}
实际源码搜索没有发现其他模块调用 ThemeService。主题设置页直接调用 AppStore.applyTheme(),因此 ThemeService.currentThemeId 不会随 UI 选择变化。
同一目录中还存在 ThemeManager 与 ThemeConfig:它们各自保存一份当前主题对象,并把字符串写入 currentThemeId。本轮聚焦扫描同样没有发现其他生产模块调用它们。它们现在更像未接入的历史方案或候选方案,而不是已运行的主题链路。文章不能把三个类都描述为协同工作;更稳的事实是,当前可见页面、Ability 和 Tab 壳主要围绕 AppStore 与 StateKeys 运转。
未使用代码本身不一定造成运行错误,但它代表潜在双真源。后续开发者若调用 ThemeService.getTheme(),可能得到国风主题,而页面正在显示樱花主题。建议二选一:
- 将 ThemeService 改为无状态解析工具,只接收 ThemeId。
- 让 AppStore 成为唯一主题状态所有者,移除 ThemeService 的当前 ID。
主题 ID 只能有一个权威来源。
十三、间距、圆角与字号也需要令牌
视觉一致性不只靠颜色。项目已经定义:
export class AppSpacing {
static readonly xs: number = 4;
static readonly sm: number = 8;
static readonly md: number = 12;
static readonly lg: number = 16;
static readonly xl: number = 20;
static readonly xxl: number = 24;
static readonly xxxl: number = 32;
static readonly pagePadding: number = 16;
static readonly cardPadding: number = 16;
static readonly sectionGap: number = 28;
}
export class AppRadius {
static readonly sm: number = 8;
static readonly md: number = 12;
static readonly lg: number = 16;
static readonly round: number = 9999;
static readonly card: number = 16;
static readonly button: number = 12;
static readonly chip: number = 20;
}
除了按数值分级,还提供 card、button、chip 这样的角色名。角色名更稳定:设计调整卡片圆角时只改 card,不会误伤所有 16vp 圆角元素。
字号也集中在 AppFontSize,从 28vp 的 h1 到 10vp 的 footnote。发布前要特别关注小字号:手机、折叠屏、平板常规正文应尽量不低于 12vp;PC 常规正文应尽量不低于 14vp。footnote 只能用于非关键辅助信息,不能承载主要操作或错误原因。
十四、交互状态必须成为一等令牌
当前颜色表已经有 success、warning、danger,但对 pressed、disabled、focused、selected 的定义还不完整。页面经常通过条件表达式直接选择颜色:
.backgroundColor(
this.selectedTheme === theme.id
? AppColors.bgSurfaceAlt
: AppColors.bgSurface
)
这能表达选中态,但项目级设计系统还应统一:
| 状态 | 必须定义 |
|---|---|
| normal | 背景、文字、边框 |
| pressed | 蒙层或亮度变化 |
| selected | 背景、边框、图标与文字 |
| disabled | 填充、文字、透明度与不可点击 |
| focused | 焦点环颜色与宽度 |
| error | 错误文字、边框、提示 |
| success | 成功反馈,不只靠绿色 |
选中态不能只依赖颜色。ThemeSettingsView 同时显示边框与“使用中”文字,这是可复用模式。错误态也应配合文本或图标,避免色觉差异用户无法判断。
十五、硬编码颜色扫描能找到真实断点
工程中仍有多处十六进制色值:
- 三种服务卡片固定
#FFFFFF背景和深色文字。 CountdownCard与ShareCard内部按类型返回固定颜色。- 首页心情背景按时段返回固定浅色。
- 阴影与图片遮罩使用带 Alpha 的黑色。
- 若干组件 Prop 提供固定色默认值。
不是所有硬编码都必须删除。图片遮罩、阴影和外部分享图可能确实需要稳定输出;但每一处都应分类:
必须随模式变化 -> 资源或 ThemeTokens
必须随用户主题变化 -> 动态 ThemeTokens
品牌固定色 -> 品牌资源并验证两种背景
图片遮罩/阴影 -> 专用效果 token
导出图片固定设计 -> 与应用 UI 令牌分开
服务卡片的 colorMode 声明为 auto,但页面仍固定白底,这是一处需要实机复核的高优先级风险。声明自动模式不等于组件内部颜色会自动适配。
十六、状态栏与导航栏也属于主题
AppStore.updateStatusBarStyle() 根据背景亮度决定系统栏内容颜色:
const isLight = AppStore.isLightColor(bgColor);
const contentColor = isLight ? '#000000' : '#FFFFFF';
AppStore.mainWindow.setWindowSystemBarProperties({
statusBarContentColor: contentColor,
navigationBarContentColor: contentColor,
});
亮度计算使用 RGB 加权:
return (
r * 0.299 +
g * 0.587 +
b * 0.114
) > 128;
这比永远使用白色或黑色图标可靠,但仍是简化判断。半透明背景、图片背景和渐变区域不一定能由单个十六进制色值代表。主题测试必须目视检查状态栏实际背景,尤其是沉浸式页面和心情图片背景。
系统栏不是页面外的附属物。它与页面背景割裂、图标不可读,同样会成为审核问题。
十七、对比度是发布门槛,不是审美偏好
每组主题都应计算并实测主要组合:
textPrimary / bgPage
textPrimary / bgSurface
textSecondary / bgSurface
textOnPrimary / actionPrimary
danger / bgSurface
disabledText / disabledFill
divider / bgSurface
正文与背景的对比度应大于 4.5:1,关键图标、标题和按钮文字应大于 3:1。禁用态可以减弱,但不能弱到用户无法辨认控件存在。
五套主题乘以亮暗两种模式就是十套组合。如果状态色也随主题自由变化,组合数量会继续扩大。因此 success、warning、danger 更适合保持稳定语义,只在明暗模式下提供经过验证的成对值。
不要只验证色值表。透明度、图片背景、阴影和组件叠加后,最终像素对比度可能和设计稿不同。
十八、深色模式不能只看首页
建议按组件族验证:
| 组件族 | 重点 |
|---|---|
| 页面根背景 | 无亮色闪屏、无白边 |
| 卡片 | 表面层级可辨,边框与阴影不过重 |
| 输入框 | 文本、占位符、光标、错误边框 |
| Tab | 选中、未选中、按下与底部安全区 |
| 列表 | 主副文本、分割线、空状态 |
| 对话框/Sheet | 遮罩、标题、按钮和滚动区域 |
| 图片卡片 | 遮罩文字在明暗图片上均可读 |
| 服务卡片 | 桌面亮暗模式与三种尺寸 |
| 系统栏 | 状态栏与导航栏图标清晰 |
项目的业务页面很多:日记、习惯、情侣空间、相册、愿望清单、组件设置、隐私政策。只看首页通过,不能证明整套令牌没有遗漏。
十九、主题切换的状态一致性测试
主题与系统模式有两个独立输入,测试顺序要覆盖交叉组合:
- 亮色模式选择樱花。
- 保持页面不退出,切到暗色模式。
- 进入详情、设置和日记页,检查所有表面。
- 返回主题页,选中态仍指向樱花。
- 切回亮色,检查状态栏与导航栏。
- 结束应用并重新启动,核对是否按产品预期恢复主题。
当前代码最后一步会回到 chinese_ink,因为 bootstrap 明确调用该主题,未展示 ThemeId 持久化。如果产品希望记住用户选择,应在 DataStore 中新增专用 Key,并在首帧阶段读取:
const savedTheme = DataStore.getInstance()
.getJsonSync<ThemeId>(
DataKeys.THEME_ID,
'chinese_ink'
);
AppStore.applyTheme(savedTheme);
这属于建议实现。落地时还要对白名单校验,防止旧版本或损坏字符串被直接断言成 ThemeId。
二十、常见问题与修复
| 现象 | 根因 | 修复 |
|---|---|---|
| 切主题只改页面背景 | 动态主题与 AppColors 混用 | 明确最终令牌消费路径 |
| 深色下服务卡片仍白底 | 组件硬编码颜色 | 使用模式资源或运行时令牌 |
| 重启后恢复默认主题 | ThemeId 未持久化 | 存储并在 bootstrap 校验读取 |
| 某服务读到旧主题 | ThemeService 是第二真源 | 合并到 AppStore 或改无状态 |
| 选中态看不出来 | 只用相近背景色 | 增加边框、图标或文字 |
| 状态栏图标不清晰 | 背景是图片或透明层 | 按真实最终背景设置系统栏 |
| 暗色分割线太亮 | 直接复用亮色透明度 | dark 资源单独校准 |
| 禁用按钮文字不可读 | 只降低整体 opacity | 定义 disabledFill/Text |
| 页面间圆角不一致 | 使用多个裸数值 | 改用 card/button/chip 角色令牌 |
二十一、上线前核对
- base 与 dark 颜色资源名称一一对应。
- 用户主题和系统明暗模式由单一解析器合并。
- 页面不同时消费两套相互冲突的背景或文字真源。
- ThemeService、ThemeManager 与 ThemeConfig 不再维护彼此独立的旧主题状态。
- “跟随系统”能够从强制模式恢复,且三态选择有明确选中反馈。
- ThemeId 若需跨启动恢复,已经持久化并做白名单校验。
- 所有业务页面、Dialog、Sheet 和服务卡片都完成亮暗测试。
- 主正文对比度大于 4.5:1,关键图标与按钮大于 3:1。
- selected、pressed、disabled、focused、error、success 状态完整。
- 状态栏与导航栏在图片背景、主题背景上都可读。
- 间距、圆角、字号使用语义令牌,避免页面各写一套。
- 10 组“5 主题 × 2 模式”完成截图或自动化基线检查。
二十二、总结:主题系统的核心是所有权
时光清单 已经具备一套可用的视觉系统骨架:base/dark 同名资源提供系统模式适配,AppColors 把资源转成 ArkTS 语义入口,五套 AppTheme 提供用户风格,AppStore.applyTheme() 把主题与明暗模式合并并驱动页面,AppSpacing、AppRadius、AppFontSize 则统一空间和层级。
真正需要继续解决的是所有权:资源色与动态主题谁负责最终背景,ThemeService 是否仍应保存状态,硬编码色哪些是视觉效果、哪些是遗漏,服务卡片是否真的跟随 colorMode,主题选择是否需要跨启动恢复。只要这些边界不清晰,再多色值也只是分散配置。
高质量亮暗色适配的结果不是“页面变黑了”,而是任何主题、任何模式、任何页面和任何交互状态都能保持同样的语义层级。把颜色、间距、圆角、字号和状态统一为可验证的视觉令牌,才能避免 HarmonyOS 应用在功能扩展后逐渐变成一组彼此割裂的页面。
本文基于 D:\huawei\one8 中 Theme.ets、ThemeService.ets、AppStore.ets、ThemeSettingsView.ets、StateKeys.ets、Colors.ets、Spacing.ets、Radius.ets、Typography.ets 以及 base/dark 颜色资源的真实源码复核。文中完整 ThemeTokens、主题持久化和交互状态令牌均明确作为演进建议,不声称已经落地。
AI 辅助声明:本文由 AI 辅助整理与润色,所有当前事实、历史证据和建议实现均按文中列出的真实项目文件重新核对;发布前仍应以当前构建产物、设备表现与 AppGallery Connect 实际页面为准。
更多推荐




所有评论(0)