【化学实验室|17】HarmonyOS ArkTS 亮暗色与视觉令牌实战:集中颜色、间距和交互状态避免页面割裂 一个项目拥有Theme.ets不等于它已经拥有完整主题系统。真正可维护的主题需要回答四个问题颜色表达的是业务语义还是某个十六进制值浅色与深色是否使用同一组资源名称系统状态栏、普通 ArkUI 组件和 Canvas 是否能同步变化选中、按下、禁用和错误状态是否都经过对比度验证。本文基于化学实验室 1.0.5的真实 HarmonyOS ArkTS 工程展开。源码中已经有AppColors与AppFontsConstants.ets也集中保存了页面间距、卡片间距和圆角。然而EntryAbility.ets在创建阶段强制设置COLOR_MODE_LIGHTbase/element/color.json定义了多种颜色dark/element/color.json却只有启动窗口背景部分页面、弹窗、Canvas 和系统栏仍直接写入#F0F0F0、#FFFFFF、#CCCCCC等色值。这不是简单的“把白色改成黑色”问题。直接替换可能造成正文变暗、图表坐标线消失、未选中标签与卡片背景混在一起甚至状态栏图标和背景使用同一明暗方向。正确做法是先建立语义令牌契约再让资源限定词自动提供浅色和深色值。主题系统的核心不是集中保存十六进制颜色而是让页面、组件、系统栏和绘制层共同消费稳定的语义资源。CHEM-LAB-ONE12-SEMANTIC-THEME-TOKENS-20260730本文将完成以下工作核对工程当前颜色模式、主题常量和资源目录。区分颜色常量、语义令牌与资源限定词的职责。盘点AppColors已覆盖的区域和仍然散落的硬编码颜色。设计 base 与 dark 使用相同名称的颜色资源。让 ArkUI 页面从字符串色值迁移到$r(app.color.xxx)。统一间距、圆角、字号与交互状态。处理状态栏、导航栏、弹窗、阴影和 Canvas 的主题同步。给出分阶段迁移、构建检查、对比度计算与双模式回归表。一、先确认工程当前不是“跟随系统”EntryAbility.ets的onCreate()中存在以下真实代码this.context.getApplicationContext().setColorMode( ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT )这行代码把应用锁在浅色模式。它解释了为什么当前页面即使在系统开启深色模式时仍保持浅色也解释了为什么dark/element/color.json长期没有被补齐运行时通常不会进入深色资源匹配。因此本文的第一条结论是在迁移完成前锁定浅色可以避免半成品主题进入用户界面在 base、dark、系统栏和绘制层全部验证后才删除这条强制浅色设置让应用重新跟随系统。不能先解除锁定再边运行边寻找白底白字问题。二、Theme.ets 已经提供了一个起点当前Theme.ets把颜色分成品牌、状态、背景、文字、Tab 和实验分类几组export class AppColors { static readonly PRIMARY: string #0052D9 static readonly PRIMARY_BG: string #E8F1FF static readonly PAGE_BG: string #F5F7FA static readonly CARD_BG: string #FFFFFF static readonly DIVIDER: string #EEEEEE static readonly TEXT_PRIMARY: string #1A1A1A static readonly TEXT_SECONDARY: string #475467 static readonly TEXT_HINT: string #6B7280 }HomePage.ets、LearningPage.ets、LabPage.ets、MinePage.ets、ExperimentCard.ets、DataTable.ets等文件都在消费这些值。集中常量已经减少了主页面之间的随机色差也使品牌蓝和正文色可以统一修改。但static readonly string只是在代码层共享字符串。它不会根据资源限定词自动变化也不能让系统在颜色模式切换时重新匹配资源。这个结构适合迁移前盘点不适合作为最终的双模式资源层。三、资源文件与代码常量目前存在两套真源工程的base/element/color.json中已经存在primary_blue、page_background、card_background、text_primary等资源但它们与Theme.ets的值并不完全一致。例如资源文件中的主色是#2979FFTheme.ets中的主色是#0052D9资源里的辅助绿色是#00C853代码常量中则是#087443。当同一个语义有两套值时会出现三个后果使用$r()的启动页和使用AppColors的业务页看起来不是同一套品牌。修改Theme.ets无法覆盖资源引用修改color.json也无法覆盖字符串常量。深色资源只能替换$r()引用无法替换编译进代码的字符串。迁移时必须指定唯一真源。对需要根据配置自动变化的颜色、尺寸和媒体优先使用资源系统Theme.ets可以保留类型、状态映射和非视觉常量但不再保存两套重复色值。四、dark 目录只有一个资源意味着什么当前dark/element/color.json只有{ color: [ { name: start_window_background, value: #FFFFFF } ] }它不仅没有覆盖页面、卡片和文字颜色连启动背景仍是白色。若解除浅色锁定系统能匹配到的深色资源极少大多数业务组件继续使用浅色色值。这会形成“系统进入深色、应用局部仍为浅色”的割裂状态。HarmonyOS 资源系统支持颜色模式限定词。base提供默认值dark目录提供同名覆盖值组件仍引用同一个资源名称运行时根据当前配置匹配。重点是同名、同语义、不同取值而不是给深色版本再创建一套dark_text_primary名称。五、先设计语义名称再选择色值不建议直接把现有变量机械转换为blue_500、gray_900。页面真正关心的是用途这是页面背景、卡片背景、正文、弱提示、边界、主操作还是危险状态。推荐最小语义集合如下语义资源浅色职责深色职责surface_page页面底色深色页面底色surface_card卡片与弹窗抬高的深色表面surface_muted输入框、未选标签次级深色表面content_primary标题与正文高强调文字content_secondary说明文字中强调文字content_hint占位与弱提示低强调文字border_subtle分割线与边框可见但克制的边界action_primary主按钮与选中态深色环境下主操作action_on_primary主按钮上的文字主操作上的文字status_success成功与安全成功与安全status_warning警告与注意警告与注意status_danger错误与危险错误与危险命名不带具体颜色未来品牌蓝调整、对比度修复或高对比主题增加时业务组件都无需改名。六、建立 base 与 dark 的同名资源浅色资源可以先从当前Theme.ets的稳定值迁移{ color: [ { name: surface_page, value: #F5F7FA }, { name: surface_card, value: #FFFFFF }, { name: surface_muted, value: #F0F0F0 }, { name: content_primary, value: #1A1A1A }, { name: content_secondary, value: #475467 }, { name: content_hint, value: #6B7280 }, { name: border_subtle, value: #E5E7EB }, { name: action_primary, value: #0052D9 }, { name: action_on_primary, value: #FFFFFF }, { name: status_success, value: #087443 }, { name: status_warning, value: #B45309 }, { name: status_danger, value: #B42318 } ] }dark/element/color.json使用完全相同的名称但选择经过对比度验证的深色值{ color: [ { name: surface_page, value: #111418 }, { name: surface_card, value: #1B2026 }, { name: surface_muted, value: #252B33 }, { name: content_primary, value: #F5F7FA }, { name: content_secondary, value: #C4CBD4 }, { name: content_hint, value: #9AA4B2 }, { name: border_subtle, value: #353D47 }, { name: action_primary, value: #6EA8FF }, { name: action_on_primary, value: #0B1F3A }, { name: status_success, value: #5BD6A2 }, { name: status_warning, value: #F4B95E }, { name: status_danger, value: #FF8C82 } ] }这些值是迁移起点不是所有项目通用的固定答案。最终仍要用真实字体、字号、透明度和背景组合计算对比度并在设备上观察视觉层级。七、组件只引用语义资源原代码Text(实验室) .fontColor(AppColors.TEXT_PRIMARY) Column() .backgroundColor(AppColors.CARD_BG)迁移后Text(实验室) .fontColor($r(app.color.content_primary)) Column() .backgroundColor($r(app.color.surface_card))组件不需要读取isDark也不需要写三元表达式。资源匹配层负责提供颜色组件只表达“这里是主文字”“这里是卡片表面”。这样系统主题变化时页面结构不会出现大量条件分支。八、Theme.ets 应从颜色仓库变成主题契约迁移完成后Theme.ets不必被删除。它可以保存语义角色、组件状态映射或纯类型避免业务页面拼接资源名export interface ComponentTone { background: ResourceColor foreground: ResourceColor border: ResourceColor } export enum VisualState { Normal, Selected, Disabled, Danger }如果团队希望集中导出资源引用也要确保导出的仍是ResourceColor不是十六进制字符串。最重要的边界是主题契约描述角色资源文件提供配置相关的值业务组件消费角色。九、间距和圆角也属于视觉令牌Constants.ets已经集中保存static readonly PAGE_PADDING: number 16 static readonly CARD_GAP: number 12 static readonly CARD_RADIUS: number 16 static readonly SMALL_RADIUS: number 12这是正确方向但命名仍偏“某个数值放在哪里”。随着平板、2in1 和自由窗口加入同一语义可能需要不同密度。例如紧凑窗口页面边距 16vp宽屏内容区边距 24vp普通卡片圆角 12vp弹窗圆角 20vp。可以把数值升级为尺度space_xs、space_s、space_m、space_l再由组件规则决定使用哪一档。若尺寸需要随设备或配置匹配也可以放入 float 资源通过$r(app.float.space_m)引用。这样页面不会同时出现 15、16、18 三个几乎相同的边距。十、字号令牌不能替代动态字体测试当前AppFonts提供 20、16、14、12 四档字号和三档字重。集中字号能维持层级但不能证明固定高度容器支持系统字体放大。HomePage的 Banner、列表行、按钮和 Tab 都需要在较大字体下验证。建议用语义名表达用途title_page、title_section、body、caption并遵循以下规则普通正文优先允许换行而不是缩小到不可读。按钮文字设置合理maxLines容器高度保留弹性。卡片标题与说明使用不同语义字号不用单一字号加粗模拟所有层级。Canvas 内文字也要从同一套字号与颜色规则读取不能独立写死。十一、硬编码颜色需要分类迁移源码扫描可看到多类硬编码CategoryTabs.ets #F0F0F0 SearchBar.ets #F0F0F0 DataTable.ets #F5F5F5 / #CCCCCC ChartView.ets #FFFFFF / #EEEEEE EntryAbility.ets #F5F7FA / #1A1A1A / #FFFFFF LearningPage.ets #1A237E / #3949AB不能用一次全局替换处理因为它们职责不同#F0F0F0可能是未选标签、搜索框或数据表头#FFFFFF可能是卡片、按钮文字或 Canvas 背景渐变色可能是品牌 Banner而不是普通表面。正确顺序是先给每处硬编码标注语义再替换为对应资源。无法判断用途的颜色应保留到组件审查阶段不能因为颜色相同就认定语义相同。十二、交互状态需要独立令牌当前分类标签通常只区分 selected 和 normal.fontColor(selected ? AppColors.TEXT_WHITE : AppColors.TEXT_SECONDARY) .backgroundColor(selected ? AppColors.PRIMARY : #F0F0F0)真实交互还包括 pressed、focused、disabled 与 error。建议为组件建立状态表状态背景文字边框验收重点normalsurface_mutedcontent_secondary透明标签可辨识selectedaction_primaryaction_on_primary主色选中明显pressed主色压暗或叠层高对比文字主色有即时反馈focused原背景原文字焦点色键盘可定位disabled禁用表面禁用文字弱边界不像可点击error危险弱背景危险文字危险色不只依赖红色状态不能只靠色相区分。错误状态还应有文字或图标焦点状态要有轮廓禁用状态要同时停止点击行为。十三、系统栏必须和页面主题同步EntryAbility.ets当前把系统栏固定为浅色mainWindow.setWindowSystemBarProperties({ statusBarColor: #F5F7FA, statusBarContentColor: #1A1A1A, isStatusBarLightIcon: false, navigationBarColor: #FFFFFF, navigationBarContentColor: #1A1A1A, isNavigationBarLightIcon: false })解除浅色锁定后如果这里不变深色页面顶部仍会出现浅色条。应把系统栏配置封装成一个窗口级方法在窗口创建和颜色配置变化时调用。方法输入是当前主题画像输出是背景色、内容色以及图标明暗方向。interface SystemBarPalette { statusBackground: string statusContent: string navigationBackground: string navigationContent: string useLightIcon: boolean }系统栏属于窗口层不应散落在每个业务页面。验证时要同时观察普通页面、弹窗、返回后的页面和冷启动窗口避免只修主页面截图。十四、Canvas 不会自动理解组件主题ChartView.ets使用 Canvas 绘制背景、网格、文字和折线其中既有AppColors也有#FFFFFF、#CCCCCC、#EEEEEE。ArkUI 的Text可以直接消费资源颜色但 Canvas 上下文最终需要明确的绘制色值而且主题变化后还要重新绘制。可以把图表调色板作为输入interface ChartPalette { background: string axis: string grid: string label: string line: string point: string } private drawChart(palette: ChartPalette): void { this.ctx.fillStyle palette.background this.ctx.strokeStyle palette.grid // 绘制网格、标签、折线和数据点 }窗口或主题层解析当前资源后生成ChartPaletteCanvas 组件在调色板变化时清屏并重绘。只修改页面背景而不重绘 Canvas会留下白色绘图区或不可见坐标线。十五、实验模拟 Canvas 要区分物体色与界面色ExperimentSimPage.ets中存在大量烧杯、液体、导线、刻度和背景色。它们不能全部迁移成页面主题色。应拆成两类界面色画布背景、说明文字、网格、控制面板、边框随主题变化。内容色化学液体、火焰、金属、指示剂、实验器材用于表达实验对象通常保持稳定。如果把指示剂红色替换成深色主题的危险色实验含义就会被破坏如果让画布背景始终白色深色页面又会出现刺眼亮块。因此 Canvas 迁移必须按语义而不是按色值数量批量替换。十六、阴影在深色模式下不能照搬源码多处使用半透明黑色阴影.shadow({ radius: 2, color: #08000000, offsetY: 1 })在浅色背景中轻微黑影能表达卡片层级在深色背景中同样阴影几乎不可见。深色层级更适合使用表面亮度差、细边框或更克制的环境光效果。建议把卡片层级拆成surface_page、surface_card、border_subtle阴影仅作为增强而不是唯一边界。回归时关闭阴影观察卡片是否仍可识别。如果完全依赖阴影说明背景与卡片表面缺少足够层级差。十七、渐变与品牌色要验证文字对比度HomePage和LearningPage使用蓝色渐变 Banner文字通常是白色。深色模式不一定需要把品牌渐变反转但要检查渐变最亮区域与白字的对比度尤其是小字号说明和透明图标。建议把 Banner 定义成一组配对令牌而不是单个背景色interface BannerTone { start: ResourceColor end: ResourceColor title: ResourceColor description: ResourceColor buttonBackground: ResourceColor buttonText: ResourceColor }这样修改渐变时会同时审查其上的标题、说明和按钮不会只调整背景后留下不可读文字。十八、计算对比度而不是凭肉眼判断对比度需要按前景与背景的相对亮度计算。以下 TypeScript 工具可用于开发期校验十六进制颜色function channel(value: number): number { const s value / 255 return s 0.03928 ? s / 12.92 : Math.pow((s 0.055) / 1.055, 2.4) } function luminance(hex: string): number { const value hex.replace(#, ) const r parseInt(value.slice(0, 2), 16) const g parseInt(value.slice(2, 4), 16) const b parseInt(value.slice(4, 6), 16) return 0.2126 * channel(r) 0.7152 * channel(g) 0.0722 * channel(b) } function contrast(foreground: string, background: string): number { const a luminance(foreground) const b luminance(background) const light Math.max(a, b) const dark Math.min(a, b) return (light 0.05) / (dark 0.05) }工程发布检查可把正文与背景目标设为不低于 4.5:1关键图标、标题和按钮等至少检查 3:1。透明色需要先与实际背景合成再计算最终颜色不能直接拿八位十六进制的 RGB 部分计算。十九、迁移要按可回滚批次进行一次改完几十个页面风险很高。建议分六批建立 base/dark 同名资源不解除浅色锁定。迁移Index、首页、实验室、学习和“我的”五个主页面。迁移共用卡片、搜索、分类标签、表格与弹窗。迁移二级页面和交互状态清理普通 UI 硬编码色。处理系统栏、ChartView 与实验模拟 Canvas。补齐深色启动资源解除浅色锁定并执行完整回归。每批结束都运行构建和核心页面检查。若发现颜色问题可以回滚单个批次不会在整个工程中追查一次性替换造成的连锁错误。二十、用脚本持续发现新增硬编码迁移完成后仍需要阻止新代码重新引入散落色值。开发期可以执行rg -n --glob *.ets #[0-9A-Fa-f]{6,8} entry/src/main/ets白名单可以保留实验内容色、需要固定的品牌素材或确有依据的 Canvas 物体色普通页面背景、文字、边框、按钮和状态色不应继续新增。审查记录要写明文件、行号、语义和保留原因。资源层也需要检查同名覆盖。一个简单的校验脚本可以读取 base 与 dark 的color.json比较资源名称集合const missingInDark baseNames.filter((name) !darkNames.includes(name)) if (missingInDark.length 0) { throw new Error(dark resources missing: ${missingInDark.join(, )}) }这样新增浅色语义资源却忘记补深色值时构建或检查阶段就能发现。二十一、构建验证不能省略资源名迁移最容易产生拼写错误、资源类型错误和漏文件。工程根目录使用项目既有 hvigor 命令构建$env:DEVECO_SDK_HOMED:\huawei\DevEco Studio\sdk $env:PATHD:\huawei\DevEco Studio\jbr\bin; $env:PATH D:\huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat --no-daemon assembleHap构建通过只说明资源可解析、ArkTS 可编译不说明视觉正确。仍要在浅色、深色、冷启动、前后台切换、横竖屏和自由窗口下走完整页面。二十二、主页面双模式验收表页面场景预期结果通过标准首页浅色启动页面与卡片层级清楚正文可读Banner 按钮清晰首页切到深色页面、卡片、标签同步无突兀白块未选标签可辨实验室分类切换selected/normal 明确两种模式都能识别当前分类学习渐变 Banner白字与按钮可读最亮区域仍保持清晰我的卡片与分割线层级稳定分割线不消失也不过亮Tabs选中与未选中状态区别明确颜色外仍有位置或图标反馈测试时先进入页面再切换系统颜色模式并返回上一级。若只有重新启动后才更新说明页面或绘制层缓存了旧主题值。二十三、组件与绘制层验收表对象操作预期结果失败信号SearchBar输入、清空、失焦输入区与占位符可辨背景和卡片融为一体CategoryTabs点击、按压、键盘焦点四类状态可区分只在浅色可见焦点DataTable查看表头、行、分割线数据行扫描顺畅深色中网格消失NoteEditorDialog打开、编辑、关闭浮层高于页面且文字清晰弹窗仍是纯白或输入框发黑ChartView切换主题背景、轴、标签、折线重绘留下旧主题画布ExperimentSim运行实验器材含义不变、界面适配内容色被误替换成主题色通过标准必须包含操作结果不只记录“截图看起来正常”。例如 ChartView 需要在主题切换后重新绘制分类标签需要能继续点击弹窗关闭后背景恢复正确。二十四、系统级场景验收表场景操作步骤预期结果冷启动深色系统下杀进程后启动启动背景、状态栏、首屏连续无白色闪屏热切换应用前台切换系统主题当前页与系统栏同步更新前后台切后台改主题再返回页面不保留旧主题缓存横竖屏两种主题下旋转无固定背景条与文字裁切平板分屏缩放窗口后切主题响应式布局与颜色同时正确2in1鼠标悬停与键盘焦点深浅色都能看见反馈轮廓字体放大较大系统字体下切主题文字不被固定高度遮挡二十五、常见失败与定位方式页面切换后局部恢复浅色检查二级页面是否仍引用AppColors字符串或硬编码色同时检查页面是否缓存了主题解析结果。优先搜索#色值和AppColors使用点。深色模式出现白色启动闪屏检查dark/element/color.json中的start_window_background并核对启动图、应用图标背景和首屏页面背景是否连续。状态栏图标看不清检查系统栏背景、内容色和isStatusBarLightIcon是否成套更新。不能只改背景不改图标明暗方向。Canvas 仍显示旧颜色检查主题变化后是否调用清屏和重绘确认调色板是新对象或可观察状态避免仅修改普通 ArkUI 背景。深色卡片没有边界检查surface_page与surface_card的亮度差、border_subtle和阴影策略。不要只增加更重黑影。二十六、源码级完成清单EntryAbility.ets不再永久强制浅色且系统栏可跟随主题。base 与 dark 对所有语义资源使用相同名称。Theme.ets不再与color.json维护重复色值。五个主页面的背景、文字、边界和状态均使用语义资源。SearchBar、CategoryTabs、DataTable、弹窗等共用组件已迁移。Canvas 界面色与实验内容色已分类主题变化会重绘。启动窗口在深色模式下没有白色闪屏。浅色与深色正文对比度经过计算和设备观察。pressed、selected、disabled、focused、error 均有清晰反馈。资源扫描与 base/dark 名称校验已进入日常检查。二十七、源码事实与建议实现的边界本文同时包含现状审计和迁移方案。为避免把示例资源、目标架构或验收标准误当成当前版本已经完成的功能下面按源码可复核项逐一划清边界。维度当前源码事实本文建议实现颜色模式EntryAbility.onCreate()调用COLOR_MODE_LIGHT应用固定浅色先完成资源迁移和双模式回归再评估移除强制浅色代码主题Theme.ets用static readonly string保存品牌、背景、文字和分类色保留类型与契约把需随配置变化的颜色迁移为语义资源字体令牌AppFonts集中 20/16/14/12 等字号与字重继续复用字号令牌同时执行系统字体放大测试间距与圆角Constants.ets定义页面边距、卡片间距和两级圆角将其纳入视觉契约避免页面自行新增魔法数字资源真源base/element/color.json与Theme.ets存在同义不同值指定资源系统为配置响应颜色的唯一真源深色资源dark/element/color.json只有白色启动背景为所有语义颜色提供同名 dark 覆盖并修正启动连续性系统栏EntryAbility直接写浅色背景与深色内容色主题变化时成套更新背景、内容色和图标明暗策略CanvasChartView.ets同时使用AppColors与#FFFFFF、#CCCCCC等硬编码将界面色注入绘制调色板主题变化后清屏并重绘实验内容色部分颜色表达试剂、火焰或分类含义与界面主题色分离只有确有语义依据时保持固定交互状态当前主题类未定义完整 pressed、focused、disabled、error 令牌按交互语义补齐状态并在浅色和深色分别验证对比度源码没有自动对比度校验流程对正文、图标、按钮、透明叠色和 Canvas 标签建立检查构建结果本文未声称迁移方案已在该工程完成构建或真机验证每个迁移批次执行构建并按页面与系统场景表回归其中强制浅色、两套颜色值、dark 目录内容和 Canvas 硬编码都来自当前工程surface_page、content_primary等命名以及六批迁移顺序是建议方案。实施时应保留浅色基线截图和构建产物对照不能把文章示例直接视为已经交付的源码。二十八、结语化学实验室已经拥有AppColors、AppFonts、间距与圆角常量这说明项目具备统一视觉的基础但强制浅色、双重颜色真源、dark 资源缺失和 Canvas 硬编码表明它尚未形成完整的配置响应主题。最稳妥的升级路线不是立刻切换系统主题而是先用真实源码盘点颜色来源把“蓝色、白色、灰色”改写成“操作、表面、内容、边界和状态”等语义再建立 base/dark 同名资源。随后按主页面、共用组件、二级页面、系统栏和 Canvas 分批迁移每批都构建并执行明确的操作验收。当业务页面不再判断深浅色、不再保存十六进制颜色系统栏和 Canvas 也能消费同一主题契约时视觉一致性才真正从设计约定变成工程能力。参考资料Huawei Developer深浅色颜色适配https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ui-dark-light-color-adaptationHuawei Developer资源分类与访问https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/resource-categories-and-accessHuawei DeveloperArkUI 概述https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkui-overview项目源码entry/src/main/ets/common/Theme.ets项目源码entry/src/main/ets/common/Constants.ets项目源码entry/src/main/ets/entryability/EntryAbility.ets项目资源entry/src/main/resources/base/element/color.json项目资源entry/src/main/resources/dark/element/color.json项目源码entry/src/main/ets/components/ChartView.etsAI 辅助声明本文由 AI 辅助完成源码梳理、结构校对与配图设计技术结论均以文中列出的本地 ArkTS 源码和资源文件为依据并由作者复核后发布。