【寻迹校园 HarmonyOS NEXT 实战 36】深色模式不是反色:用语义 Token 管理品牌色与状态色
本章导读:这是“寻迹校园 HarmonyOS NEXT 实战”系列第 36 篇。本文以
AppTheme.ets、Light/Dark 颜色资源、首页状态卡片、发布表单和治理进度页为证据,拆解 HarmonyOS NEXT 项目如何用语义 Token 同时管理品牌色、丢失/拾得状态、警告、错误、文本、边框与禁用态,并说明静态资源检查、视觉验收、量化对比度和屏幕阅读器验证之间的证据边界。

上图为原创生成的主题架构插画,不是项目页面截图。Light 与 Dark 不是在组件层临时反色,而是共享同一组语义名称,由两套资源分别提供适合各自背景的值。
一、为什么深色模式不能用“整体反色”解决
整体反色只关心像素,却不知道颜色表达的业务含义。在校园失物招领中,品牌蓝、丢失珊瑚、拾得绿、待处理琥珀和危险红承担不同职责。如果把整个页面做颜色反转,常见问题包括:
- 品牌蓝变成不受控制的互补色,选中态失去统一识别;
LOST与FOUND的类型语义被交换或弱化;- 错误红在深色背景上过亮,长时间阅读刺眼;
- 白色卡片反成纯黑,层级与页面背景融在一起;
- 禁用按钮仍使用低透明度白字,文字难以识别;
- 边框、分割线和次级文字一起变暗,结构消失;
- 同一业务状态在首页、详情和消息页出现不同颜色。
因此真正需要适配的不是某个页面,而是“颜色角色”。
二、本章对应的真实项目文件
| 证据 | 项目路径 | 作用 |
|---|---|---|
| 主题入口 | common-ui/src/main/ets/theme/AppTheme.ets |
暴露 AppColors、间距、圆角、尺寸和字号 |
| 浅色资源 | common-ui/src/main/resources/base/element/color.json |
Light 模式的语义颜色值 |
| 深色资源 | common-ui/src/main/resources/dark/element/color.json |
Dark 模式的同名语义颜色值 |
| 首页卡片 | entry/src/main/ets/pages/HomePage.ets |
LOST/FOUND/CLAIMING/RESOLVED 状态映射 |
| 发布表单 | entry/src/main/ets/pages/PublishFormPage.ets |
错误、警告、禁用与主按钮 |
| 治理进度 | entry/src/main/ets/pages/ModerationProgressPage.ets |
PENDING/IN_REVIEW/RESOLVED/REJECTED 语义 |
| 视觉基准 | docs/design/all-pages-design-spec-v2.md |
深色板、对比度假设和语义规则 |
页面只引用 AppColors,不需要知道资源来自浅色目录还是深色目录。
三、AppColors 管理的是角色,不是具体色值
当前主题类没有把 #2563EB 写进页面,而是通过资源引用定义语义名称:
export class AppColors {
static readonly BRAND_PRIMARY: ResourceColor = $r('app.color.xunji_brand_primary');
static readonly PAGE_BACKGROUND: ResourceColor = $r('app.color.xunji_page_background');
static readonly CARD_BACKGROUND: ResourceColor = $r('app.color.xunji_card_background');
static readonly TEXT_PRIMARY: ResourceColor = $r('app.color.xunji_text_primary');
static readonly LOST_CONTAINER: ResourceColor = $r('app.color.xunji_lost_container');
static readonly FOUND_CONTAINER: ResourceColor = $r('app.color.xunji_found_container');
static readonly WARNING_CONTAINER: ResourceColor = $r('app.color.xunji_warning_container');
static readonly ERROR_CONTAINER: ResourceColor = $r('app.color.xunji_error_container');
}
组件只表达“这里是主文本”“这里是拾得状态容器”,系统再根据当前资源限定词选择实际颜色。
四、同名 Token 如何连接 Light 与 Dark
浅色资源中,页面背景为 #F3F6FA,卡片为 #FFFFFF;深色资源中,同名 Token 分别变为 #101318 和 #1A1F27。名称不变,值随主题切换:
| 语义 Token | Light | Dark | 用途 |
|---|---|---|---|
BRAND_PRIMARY |
#2563EB |
#7FA7FF |
主操作、选中、链接 |
PAGE_BACKGROUND |
#F3F6FA |
#101318 |
页面底色 |
CARD_BACKGROUND |
#FFFFFF |
#1A1F27 |
卡片与导航表面 |
TEXT_PRIMARY |
#182230 |
#F3F6FA |
标题与主要正文 |
TEXT_SECONDARY |
#596579 |
#B5BECC |
描述与辅助信息 |
BORDER |
#D7DEE9 |
#364050 |
边框与分割线 |
DISABLED |
#E7EAF0 |
#2A303A |
禁用表面 |
TEXT_DISABLED |
#7B8494 |
#8E98A8 |
禁用文字 |
这里没有“把白色变黑”的运算,只有两套经过设计的资源表。

上图把品牌、表面、文字和四组业务状态放在同一矩阵中。每一行保持语义不变,每一列针对背景重新选择明度、饱和度和容器色。
五、品牌蓝只负责交互,不负责所有状态
BRAND_PRIMARY 的职责是可操作与已选中,例如:
- 当前 Tab;
- 筛选选中项;
- 主按钮;
- 文本链接;
- 匹配页“信息相似分”;
- 当前治理步骤;
- 键盘焦点强调。
如果 LOST、FOUND、警告和错误也全部使用品牌蓝,用户只能看到“很多蓝色标签”,无法从颜色和文案共同识别业务状态。
六、LOST 与 FOUND 为什么需要容器色
项目为丢失和拾得分别准备基础色、容器色和容器上的前景色:
LOST / LOST_CONTAINER / ON_LOST
FOUND / FOUND_CONTAINER / ON_FOUND
浅色模式下,丢失容器是 #FFF1ED,前景为 #9E321F;拾得容器是 #EAF8F2,前景为 #0B6B4B。深色模式不是交换红绿,而是分别使用 #4A241D 与 #143D32 作为容器,并提高前景亮度。
容器色适合标签和信息块,基础色不应直接承担大段小字号正文。
七、状态映射必须来自业务字段
HomePage.ets 的卡片不会根据标题文字猜颜色,而是读取 report.status 与 report.reportType:
private statusFillColor(): ResourceColor {
if (this.report.status === ReportStatus.CLAIMING) return AppColors.WARNING_CONTAINER;
if (this.report.status === ReportStatus.RESOLVED) return AppColors.FOUND_CONTAINER;
return this.report.reportType === ReportType.LOST ?
AppColors.LOST_CONTAINER : AppColors.FOUND_CONTAINER;
}
业务状态是权威来源,Token 只是视觉映射。这样修改深色值不会改变状态机,修改状态机也不会把颜色规则散落到多个页面。
八、CLAIMING 为什么不是品牌蓝
CLAIMING 表示存在待核验的认领流程,需要注意但不是错误。项目使用 WARNING_CONTAINER 和 ON_WARNING,而不是主操作蓝或危险红。
这一区分帮助用户理解:
- 蓝色:可以操作或正在选择;
- 琥珀:需要留意、等待或核验;
- 红色:错误、危险或破坏性动作;
- 绿色:拾得类型或真正完成的业务结果。
颜色必须与状态文案一起出现,不能只靠颜色传递结论。
九、ERROR 不等于所有失败结果
发布表单的字段错误、Repository 异常和“举报不成立”是不同概念。当前 UI 中:
- 输入错误使用
ERROR_CONTAINER/ON_ERROR; - 重试入口仍使用品牌蓝;
REJECTED在治理语境中表示核查结论,不应被描述成系统崩溃;- Photo Picker 取消是中性提示,不使用错误红;
- 没有匹配候选是空态,不是错误态。
语义颜色首先要求业务分类正确,其次才是色值选择正确。
十、深色表面需要层级,而不是纯黑
Dark 资源把页面设为 #101318,卡片设为 #1A1F27,边框设为 #364050。三者之间形成稳定层级:
- 页面背景承载全局区域;
- 卡片比页面略亮,用于列表、表单和导航;
- 边框继续比卡片可辨,但不抢主内容;
- 品牌容器
#20325F只用于选中与强调。
如果页面和卡片都使用纯黑,列表项只能依赖阴影;而深色界面中的阴影常常不够可靠。
十一、主文字与次文字不能共用透明度
当前资源为主文字和次文字提供独立颜色,而不是对主文字统一设置透明度。这样可以在不同背景上分别调整:
- 主标题优先保证阅读;
- 描述文字降低层级但保持可辨;
- placeholder 与 disabled 再单独处理;
- 错误与警告使用各自前景 Token。
透明度叠加会受到父容器、动画和嵌套组件影响,不适合作为全项目文字层级的唯一方案。
十二、disabled 需要一对表面与文字 Token
发布表单的“下一步”和“提交”按钮会根据校验结果切换:
.fontColor(this.canSubmit() ? AppColors.ON_PRIMARY : AppColors.TEXT_DISABLED)
.backgroundColor(this.canSubmit() ? AppColors.BRAND_PRIMARY : AppColors.DISABLED)
禁用态并不是主按钮乘以低透明度,而是 DISABLED + TEXT_DISABLED 的组合。Light 与 Dark 各自拥有可读的禁用对,避免白字落在过浅灰色上。
十三、页面禁止散写颜色的工程收益
如果页面中大量出现 '#FFFFFF'、'#333333' 和 '#999999',深色改造需要逐文件查找,并且很难判断每个灰色的角色。语义 Token 带来三项直接收益:
- 主题变更集中在资源层;
- 代码审查可以发现新页面是否绕过 Token;
- 同一状态跨页面共享含义;
- 视觉规范可以直接映射到代码名称;
- 后续高对比度模式有明确扩展点。
这也是把主题放进 common-ui HAR,而不是放在某个页面旁边的原因。
十四、Token 不能替代组件状态设计
有了颜色表,组件仍要正确处理:
selected:背景、前景、边框和可访问名称一起变化;disabled:不仅变色,还要.enabled(false);pressed:需要系统按压反馈;error:需要文字说明和重试动作;loading:不能用灰色页面假装不可用;focus:键盘焦点必须可见且顺序合理。
Token 解决“用什么颜色”,组件状态解决“什么时候使用”。
十五、为什么状态不能只靠颜色
红绿色觉差异、灰阶显示和高亮环境都可能削弱颜色信息。项目卡片同时提供状态文字、标题、地点、日期和描述;导航项同时提供图标、文字与“已选中”可访问名称。
例如 BottomNavigation 使用:
.accessibilityText(`${label}${this.selectedTab === tab ? ',已选中' : ''}`)
.accessibilityDescription(`切换到${label}页面`)
颜色只是冗余线索,不能成为唯一证明。
十六、Light/Dark 资源存在不等于运行验收完成
静态读取当前项目可以证明:
AppColors使用资源引用;- base 与 dark 目录存在同名资源;
- 页面广泛引用语义 Token;
- 设计文档包含深色专项板和预期色值。
这些证据不能自动证明所有页面已经在真机深色模式下无闪烁、无遗漏、无系统栏冲突。当前文章没有重新完成全页面深色真机回归,因此不把“资源齐全”写成“深色验收全部通过”。
十七、视觉检查与量化对比度是两种证据
视觉检查适合发现层级、语义混用和明显不可读;量化对比度需要对具体前景/背景组合计算,并考虑字号、字重和实际渲染。
建议建立组合清单:
| 前景 | 背景 | 场景 | 验证方式 |
|---|---|---|---|
TEXT_PRIMARY |
PAGE_BACKGROUND |
页面正文 | 自动计算 + 真机观察 |
TEXT_SECONDARY |
CARD_BACKGROUND |
卡片描述 | 自动计算 + 字体放大 |
ON_PRIMARY |
BRAND_PRIMARY |
主按钮 | 自动计算 + disabled 对照 |
ON_WARNING |
WARNING_CONTAINER |
待处理提示 | Light/Dark 分别计算 |
ON_ERROR |
ERROR_CONTAINER |
错误提示 | Light/Dark 分别计算 |
设计板通过不能代替对比度报告,对比度数值也不能代替真实设备可读性观察。
十八、深色切换还要检查系统组件
项目页面之外,日期选择器、Photo Picker、小艺系统界面、键盘和系统弹窗由平台管理。应用 Token 只能控制自己的 ArkUI 表面,不能假设系统界面会沿用同一品牌色。
验收时应分别检查:
- 应用页面切换时是否重建或丢状态;
- 系统组件返回后页面颜色是否恢复;
- 状态栏与导航区域是否可读;
- 图片、插画在深色背景上是否出现白边;
- 第三方或系统页面是否存在短暂亮屏。
十九、主题变更不应进入 Service 与 Repository
深色模式是展示偏好,不应改变 ReportService 的校验、ReportRepository 的数据或匹配分数。页面消费同一份业务实体,只改变视觉 Token。
如果需要保存主题偏好,可以由设置 Service/Preferences 管理轻量配置;业务数据不应因为颜色模式产生两套副本。主题切换后重新查询 Repository,通常是错误的职责耦合。
二十、推荐的主题验收清单
一次可追踪的 Light/Dark 验收至少包含:
- 首页正常、空、加载和错误状态;
- LOST、FOUND、CLAIMING、RESOLVED 卡片;
- 发布表单正常、必填错误和 disabled;
- 详情页主操作与危险操作;
- 消息、举报与治理状态;
- Phone 与大屏导航的 selected/focus;
- 系统字体放大与长文本;
- 系统组件往返;
- 关键前景/背景组合的量化对比度;
- 屏幕阅读器不依赖颜色播报状态。
每项记录 passed / failed / not run,不要用一张深色首页图代表全应用。
二十一、工程复盘:Token 是跨页面视觉合约
语义 Token 把设计语言变成稳定接口。设计稿说“品牌蓝只负责操作与选中”,代码就使用 BRAND_PRIMARY;设计稿说“丢失与拾得独立”,代码就使用 LOST_* 与 FOUND_*;设计稿说“禁用文字可读”,资源就提供 TEXT_DISABLED。
以后新增“申诉中”状态,不应在页面临时挑一种紫色,而应先定义业务语义、决定是否复用 WARNING,再更新资源、组件、设计稿和测试。
二十二、本文小结
“寻迹校园”的深色策略不是对浅色页面做反色,而是让 Light/Dark 资源共同实现一组稳定语义 Token。品牌、表面、文字、边框、禁用、丢失、拾得、警告和错误各自拥有清晰职责,页面通过 AppColors 统一消费。
当前代码和资源能证明语义主题结构已经建立,不能替代全页面深色真机、量化对比度或屏幕阅读器验收。视觉、运行和无障碍证据仍需分别记录。
系列导航:第 36 篇 / 共 50 篇。上一篇:《Design Spec 到 ArkUI 的设计稿门禁》;下一篇:《sm、md、lg、xl 四档响应式 Shell》。
更多推荐


所有评论(0)