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

寻迹校园深色语义 Token 原创封面图

上图为原创生成的主题架构插画,不是项目页面截图。Light 与 Dark 不是在组件层临时反色,而是共享同一组语义名称,由两套资源分别提供适合各自背景的值。

一、为什么深色模式不能用“整体反色”解决

整体反色只关心像素,却不知道颜色表达的业务含义。在校园失物招领中,品牌蓝、丢失珊瑚、拾得绿、待处理琥珀和危险红承担不同职责。如果把整个页面做颜色反转,常见问题包括:

  • 品牌蓝变成不受控制的互补色,选中态失去统一识别;
  • LOSTFOUND 的类型语义被交换或弱化;
  • 错误红在深色背景上过亮,长时间阅读刺眼;
  • 白色卡片反成纯黑,层级与页面背景融在一起;
  • 禁用按钮仍使用低透明度白字,文字难以识别;
  • 边框、分割线和次级文字一起变暗,结构消失;
  • 同一业务状态在首页、详情和消息页出现不同颜色。

因此真正需要适配的不是某个页面,而是“颜色角色”。

二、本章对应的真实项目文件

证据 项目路径 作用
主题入口 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 禁用文字

这里没有“把白色变黑”的运算,只有两套经过设计的资源表。

Light 与 Dark 语义 Token 矩阵原创图

上图把品牌、表面、文字和四组业务状态放在同一矩阵中。每一行保持语义不变,每一列针对背景重新选择明度、饱和度和容器色。

五、品牌蓝只负责交互,不负责所有状态

BRAND_PRIMARY 的职责是可操作与已选中,例如:

  • 当前 Tab;
  • 筛选选中项;
  • 主按钮;
  • 文本链接;
  • 匹配页“信息相似分”;
  • 当前治理步骤;
  • 键盘焦点强调。

如果 LOSTFOUND、警告和错误也全部使用品牌蓝,用户只能看到“很多蓝色标签”,无法从颜色和文案共同识别业务状态。

六、LOST 与 FOUND 为什么需要容器色

项目为丢失和拾得分别准备基础色、容器色和容器上的前景色:

LOST / LOST_CONTAINER / ON_LOST
FOUND / FOUND_CONTAINER / ON_FOUND

浅色模式下,丢失容器是 #FFF1ED,前景为 #9E321F;拾得容器是 #EAF8F2,前景为 #0B6B4B。深色模式不是交换红绿,而是分别使用 #4A241D#143D32 作为容器,并提高前景亮度。

容器色适合标签和信息块,基础色不应直接承担大段小字号正文。

七、状态映射必须来自业务字段

HomePage.ets 的卡片不会根据标题文字猜颜色,而是读取 report.statusreport.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_CONTAINERON_WARNING,而不是主操作蓝或危险红。

这一区分帮助用户理解:

  • 蓝色:可以操作或正在选择;
  • 琥珀:需要留意、等待或核验;
  • 红色:错误、危险或破坏性动作;
  • 绿色:拾得类型或真正完成的业务结果。

颜色必须与状态文案一起出现,不能只靠颜色传递结论。

九、ERROR 不等于所有失败结果

发布表单的字段错误、Repository 异常和“举报不成立”是不同概念。当前 UI 中:

  • 输入错误使用 ERROR_CONTAINER/ON_ERROR
  • 重试入口仍使用品牌蓝;
  • REJECTED 在治理语境中表示核查结论,不应被描述成系统崩溃;
  • Photo Picker 取消是中性提示,不使用错误红;
  • 没有匹配候选是空态,不是错误态。

语义颜色首先要求业务分类正确,其次才是色值选择正确。

十、深色表面需要层级,而不是纯黑

Dark 资源把页面设为 #101318,卡片设为 #1A1F27,边框设为 #364050。三者之间形成稳定层级:

  1. 页面背景承载全局区域;
  2. 卡片比页面略亮,用于列表、表单和导航;
  3. 边框继续比卡片可辨,但不抢主内容;
  4. 品牌容器 #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 验收至少包含:

  1. 首页正常、空、加载和错误状态;
  2. LOST、FOUND、CLAIMING、RESOLVED 卡片;
  3. 发布表单正常、必填错误和 disabled;
  4. 详情页主操作与危险操作;
  5. 消息、举报与治理状态;
  6. Phone 与大屏导航的 selected/focus;
  7. 系统字体放大与长文本;
  8. 系统组件往返;
  9. 关键前景/背景组合的量化对比度;
  10. 屏幕阅读器不依赖颜色播报状态。

每项记录 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》。

Logo

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

更多推荐