【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(二十五):【深色模式】一键切换暗色主题——让 App 在深夜也温柔

HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(二十五):【深色模式】一键切换暗色主题------让 App 在深夜也温柔

摘要 :从第1篇到现在,我们写了上万行代码,但有一个细节一直被忽略------用户可能在深夜打开 App,白色背景的刺眼光线会让体验大打折扣。本篇将利用 HarmonyOS 6.1.0 的 资源限定符(Resource Qualifier)setColorMode() API ,为《灵犀厨房》构建深色模式------只需在 resources/dark/ 目录添加一组颜色覆盖值,配合一个 Toggle 开关,App 就能在浅色和深色之间无感切换。


一、引言:被忽略的"夜间访客"

你是否有过这样的体验:深夜关灯准备刷一下手机,打开某个 App,瞬间被白色背景刺得眯起眼睛。《灵犀厨房》也存在同样的问题------从第1篇到现在,我们所有的代码都默认运行在浅色模式下,硬编码了 #FFF8F0#FFFFFF#333333 等颜色。这在白天没问题,但在深夜,这些颜色就是"光污染"。

深色模式不只是"把白底换成黑底"。它需要解决三个核心问题:

问题 浅色模式 深色模式需求
背景亮度 暖米色/纯白,柔和舒适 深灰/纯黑,减少眩光
文字对比度 深色文字在浅色背景上 浅色文字在深色背景上,但仍需保持足够对比度
主题色一致性 #FF6B35 橙红色醒目 需要调亮为 #FF8C5A,否则在深色背景上辨识度下降

🎯 本篇目标:用最少的工作量(28个颜色定义 + 1个Toggle开关 + 2处演示替换),为《灵犀厨房》装上深色模式,并为后续的全面迁移建立基础设施。


二、核心原理:HarmonyOS 的资源限定符机制

HarmonyOS 的资源系统支持限定符目录。当你写下:

typescript 复制代码
.backgroundColor($r('app.color.bg_page'))

ArkUI 会根据当前系统的颜色模式,自动去 resources/base/resources/dark/ 目录中查找同名资源。

复制代码
resources/
├── base/element/color.json    ← 默认值(浅色模式)
│   { "name": "bg_page", "value": "#FFF8F0" }
│
└── dark/element/color.json    ← 深色模式覆盖值
    { "name": "bg_page", "value": "#121212" }

关键机制同名覆盖 。dark 目录中只放需要改变的颜色。不需要覆盖的颜色(如 text_white: #FFFFFF)不出现在 dark 目录中,系统自动沿用 base 的值。这极大减少了维护成本------你只需要定义"变化的部分"。
#mermaid-svg-5ULxmpIwQtopjCK8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5ULxmpIwQtopjCK8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5ULxmpIwQtopjCK8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5ULxmpIwQtopjCK8 .error-icon{fill:#552222;}#mermaid-svg-5ULxmpIwQtopjCK8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5ULxmpIwQtopjCK8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5ULxmpIwQtopjCK8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5ULxmpIwQtopjCK8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5ULxmpIwQtopjCK8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5ULxmpIwQtopjCK8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5ULxmpIwQtopjCK8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5ULxmpIwQtopjCK8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5ULxmpIwQtopjCK8 .marker.cross{stroke:#333333;}#mermaid-svg-5ULxmpIwQtopjCK8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5ULxmpIwQtopjCK8 p{margin:0;}#mermaid-svg-5ULxmpIwQtopjCK8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5ULxmpIwQtopjCK8 .cluster-label text{fill:#333;}#mermaid-svg-5ULxmpIwQtopjCK8 .cluster-label span{color:#333;}#mermaid-svg-5ULxmpIwQtopjCK8 .cluster-label span p{background-color:transparent;}#mermaid-svg-5ULxmpIwQtopjCK8 .label text,#mermaid-svg-5ULxmpIwQtopjCK8 span{fill:#333;color:#333;}#mermaid-svg-5ULxmpIwQtopjCK8 .node rect,#mermaid-svg-5ULxmpIwQtopjCK8 .node circle,#mermaid-svg-5ULxmpIwQtopjCK8 .node ellipse,#mermaid-svg-5ULxmpIwQtopjCK8 .node polygon,#mermaid-svg-5ULxmpIwQtopjCK8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5ULxmpIwQtopjCK8 .rough-node .label text,#mermaid-svg-5ULxmpIwQtopjCK8 .node .label text,#mermaid-svg-5ULxmpIwQtopjCK8 .image-shape .label,#mermaid-svg-5ULxmpIwQtopjCK8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-5ULxmpIwQtopjCK8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5ULxmpIwQtopjCK8 .rough-node .label,#mermaid-svg-5ULxmpIwQtopjCK8 .node .label,#mermaid-svg-5ULxmpIwQtopjCK8 .image-shape .label,#mermaid-svg-5ULxmpIwQtopjCK8 .icon-shape .label{text-align:center;}#mermaid-svg-5ULxmpIwQtopjCK8 .node.clickable{cursor:pointer;}#mermaid-svg-5ULxmpIwQtopjCK8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5ULxmpIwQtopjCK8 .arrowheadPath{fill:#333333;}#mermaid-svg-5ULxmpIwQtopjCK8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5ULxmpIwQtopjCK8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5ULxmpIwQtopjCK8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5ULxmpIwQtopjCK8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5ULxmpIwQtopjCK8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5ULxmpIwQtopjCK8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5ULxmpIwQtopjCK8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5ULxmpIwQtopjCK8 .cluster text{fill:#333;}#mermaid-svg-5ULxmpIwQtopjCK8 .cluster span{color:#333;}#mermaid-svg-5ULxmpIwQtopjCK8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5ULxmpIwQtopjCK8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5ULxmpIwQtopjCK8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-5ULxmpIwQtopjCK8 .icon-shape,#mermaid-svg-5ULxmpIwQtopjCK8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5ULxmpIwQtopjCK8 .icon-shape p,#mermaid-svg-5ULxmpIwQtopjCK8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5ULxmpIwQtopjCK8 .icon-shape .label rect,#mermaid-svg-5ULxmpIwQtopjCK8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5ULxmpIwQtopjCK8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5ULxmpIwQtopjCK8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5ULxmpIwQtopjCK8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} LIGHT
DARK
backgroundColor

($r('app.color.bg_page'))
资源解析器
当前 ColorMode?
base/element/color.json

bg_page: #FFF8F0
dark/element/color.json

bg_page: #121212

图一解读 :资源解析器是 HarmonyOS 的资源查找引擎。它根据系统当前的 ColorMode 自动选择对应限定符目录下的资源。开发者只需在代码中写 $r('app.color.bg_page'),不需要写任何 if (isDark) 判断。这种声明式的资源管理是 HarmonyOS 相比传统 Android 开发的一大优势。


三、颜色语义化设计:从"颜色值"到"设计意图"

直接写 #FF6B35 有两个问题:一是无法被深色模式覆盖(硬编码颜色不受资源系统管理),二是含义模糊------三个月后你自己都忘了这个颜色是干嘛用的。我们需要定义一套语义化颜色名 ,让颜色名表达用途 而非色值

语义名 浅色值 暗色值 用途
primary #FF6B35 #FF8C5A 主题色(按钮、标签、强调文字)
primary_light #FFF0E6 #3D2010 主题色浅底(卡片内强调区域)
bg_page #FFF8F0 #121212 页面背景
bg_card #FFFFFF #1E1E1E 卡片背景
bg_smart_screen #1A1A2E #0D0D0D 智慧屏深色背景
bg_secondary #F8F9FA #1A1A1A 次级背景(列表、分组头)
text_primary #333333 #E0E0E0 主文字
text_secondary #666666 #B0B0B0 次要文字
text_hint #999999 #808080 提示文字
text_white #FFFFFF #FFFFFF 白字(深色底上使用,无需覆盖)
divider #F0F0F0 #2A2A2A 分割线
success #4CAF50 #66BB6A 成功/在线
warn #FF9800 #FFB74D 警告/待机
error #F44336 #EF5350 错误/离线

命名原则

  • 用途 命名(bg_page),而非颜色值(orange)------方便后续换主题色
  • 暗色值调亮主色#FF6B35#FF8C5A),保证深色背景上的对比度
  • 暗色值降低背景亮度#FFFFFF#1E1E1E),减少眩光
  • text_white 不覆盖------白色在深色背景上不变,缺省即自动沿用 base

四、实战步骤

Step 1:定义颜色资源文件

resources/base/element/color.json(新增 14 个颜色):

json 复制代码
{
  "color": [
    { "name": "primary",         "value": "#FF6B35" },
    { "name": "primary_light",   "value": "#FFF0E6" },
    { "name": "bg_page",         "value": "#FFF8F0" },
    { "name": "bg_card",         "value": "#FFFFFF" },
    { "name": "bg_smart_screen", "value": "#1A1A2E" },
    { "name": "bg_secondary",    "value": "#F8F9FA" },
    { "name": "text_primary",    "value": "#333333" },
    { "name": "text_secondary",  "value": "#666666" },
    { "name": "text_hint",       "value": "#999999" },
    { "name": "text_white",      "value": "#FFFFFF" },
    { "name": "divider",         "value": "#F0F0F0" },
    { "name": "success",         "value": "#4CAF50" },
    { "name": "warn",            "value": "#FF9800" },
    { "name": "error",           "value": "#F44336" }
  ]
}

resources/dark/element/color.json (新增 13 个暗色覆盖,不含 text_white):

json 复制代码
{
  "color": [
    { "name": "primary",         "value": "#FF8C5A" },
    { "name": "primary_light",   "value": "#3D2010" },
    { "name": "bg_page",         "value": "#121212" },
    { "name": "bg_card",         "value": "#1E1E1E" },
    { "name": "bg_smart_screen", "value": "#0D0D0D" },
    { "name": "bg_secondary",    "value": "#1A1A1A" },
    { "name": "text_primary",    "value": "#E0E0E0" },
    { "name": "text_secondary",  "value": "#B0B0B0" },
    { "name": "text_hint",       "value": "#808080" },
    { "name": "divider",         "value": "#2A2A2A" },
    { "name": "success",         "value": "#66BB6A" },
    { "name": "warn",            "value": "#FFB74D" },
    { "name": "error",           "value": "#EF5350" }
  ]
}

注意text_white 不出现在 dark 目录中------白色在深色背景上不需要变化,系统自动沿用 base 的值。

Step 2:添加 Toggle 切换开关

ProfilePage 中添加一个 Switch 开关:

typescript 复制代码
import { common, ConfigurationConstant } from '@kit.AbilityKit';

@Local isDark: boolean = false;

aboutToAppear(): void {
  const ctx = this.getUIContext().getHostContext() as common.UIAbilityContext;
  this.isDark = ctx.config.colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
}

// UI
Row() {
  Text(this.isDark ? '🌙 深色模式' : '☀️ 浅色模式').fontSize(14).fontColor('#333')
  Blank()
  Toggle({ type: ToggleType.Switch, isOn: this.isDark })
    .selectedColor('#FF6B35')
    .onChange((on: boolean) => {
      this.isDark = on;
      const ctx = this.getUIContext().getHostContext() as common.UIAbilityContext;
      const mode = on ? ConfigurationConstant.ColorMode.COLOR_MODE_DARK
        : ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
      ctx.getApplicationContext().setColorMode(mode);
    })
}

所有页面 资源解析器 ApplicationContext Toggle 开关 👤 用户 所有页面 资源解析器 ApplicationContext Toggle 开关 👤 用户 #mermaid-svg-EKBdVRoqeVqSpKOZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EKBdVRoqeVqSpKOZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EKBdVRoqeVqSpKOZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EKBdVRoqeVqSpKOZ .error-icon{fill:#552222;}#mermaid-svg-EKBdVRoqeVqSpKOZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EKBdVRoqeVqSpKOZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EKBdVRoqeVqSpKOZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EKBdVRoqeVqSpKOZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EKBdVRoqeVqSpKOZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EKBdVRoqeVqSpKOZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EKBdVRoqeVqSpKOZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EKBdVRoqeVqSpKOZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EKBdVRoqeVqSpKOZ .marker.cross{stroke:#333333;}#mermaid-svg-EKBdVRoqeVqSpKOZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EKBdVRoqeVqSpKOZ p{margin:0;}#mermaid-svg-EKBdVRoqeVqSpKOZ .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-EKBdVRoqeVqSpKOZ text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-EKBdVRoqeVqSpKOZ .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-EKBdVRoqeVqSpKOZ .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-EKBdVRoqeVqSpKOZ .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-EKBdVRoqeVqSpKOZ .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-EKBdVRoqeVqSpKOZ #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-EKBdVRoqeVqSpKOZ .sequenceNumber{fill:white;}#mermaid-svg-EKBdVRoqeVqSpKOZ #sequencenumber{fill:#333;}#mermaid-svg-EKBdVRoqeVqSpKOZ #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-EKBdVRoqeVqSpKOZ .messageText{fill:#333;stroke:none;}#mermaid-svg-EKBdVRoqeVqSpKOZ .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-EKBdVRoqeVqSpKOZ .labelText,#mermaid-svg-EKBdVRoqeVqSpKOZ .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-EKBdVRoqeVqSpKOZ .loopText,#mermaid-svg-EKBdVRoqeVqSpKOZ .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-EKBdVRoqeVqSpKOZ .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-EKBdVRoqeVqSpKOZ .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-EKBdVRoqeVqSpKOZ .noteText,#mermaid-svg-EKBdVRoqeVqSpKOZ .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-EKBdVRoqeVqSpKOZ .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-EKBdVRoqeVqSpKOZ .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-EKBdVRoqeVqSpKOZ .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-EKBdVRoqeVqSpKOZ .actorPopupMenu{position:absolute;}#mermaid-svg-EKBdVRoqeVqSpKOZ .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-EKBdVRoqeVqSpKOZ .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-EKBdVRoqeVqSpKOZ .actor-man circle,#mermaid-svg-EKBdVRoqeVqSpKOZ line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-EKBdVRoqeVqSpKOZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 点击切换 setColorMode(DARK) 通知颜色模式变更 $r('app.color.xxx') 全部重新解析 所有页面立即切换为深色

图二解读setColorMode() 是全局生效的------一旦调用,所有使用了 $r('app.color.xxx') 的组件会立刻重新解析为 dark 目录的值。无需手动刷新任何页面,无需遍历组件树。这是 HarmonyOS 声明式资源系统最大的优势:状态变更 → 自动响应。

Step 3:渐进式迁移------只改两行演示

当前代码中大量使用了硬编码颜色(如 '#FF6B35''#333''#FFF')。全部替换成本高且风险大。本篇采用「渐进式迁移」策略:
#mermaid-svg-rhsjz0tSl6KjOAZe{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rhsjz0tSl6KjOAZe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rhsjz0tSl6KjOAZe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rhsjz0tSl6KjOAZe .error-icon{fill:#552222;}#mermaid-svg-rhsjz0tSl6KjOAZe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rhsjz0tSl6KjOAZe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rhsjz0tSl6KjOAZe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rhsjz0tSl6KjOAZe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rhsjz0tSl6KjOAZe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rhsjz0tSl6KjOAZe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rhsjz0tSl6KjOAZe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rhsjz0tSl6KjOAZe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rhsjz0tSl6KjOAZe .marker.cross{stroke:#333333;}#mermaid-svg-rhsjz0tSl6KjOAZe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rhsjz0tSl6KjOAZe p{margin:0;}#mermaid-svg-rhsjz0tSl6KjOAZe .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rhsjz0tSl6KjOAZe .cluster-label text{fill:#333;}#mermaid-svg-rhsjz0tSl6KjOAZe .cluster-label span{color:#333;}#mermaid-svg-rhsjz0tSl6KjOAZe .cluster-label span p{background-color:transparent;}#mermaid-svg-rhsjz0tSl6KjOAZe .label text,#mermaid-svg-rhsjz0tSl6KjOAZe span{fill:#333;color:#333;}#mermaid-svg-rhsjz0tSl6KjOAZe .node rect,#mermaid-svg-rhsjz0tSl6KjOAZe .node circle,#mermaid-svg-rhsjz0tSl6KjOAZe .node ellipse,#mermaid-svg-rhsjz0tSl6KjOAZe .node polygon,#mermaid-svg-rhsjz0tSl6KjOAZe .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rhsjz0tSl6KjOAZe .rough-node .label text,#mermaid-svg-rhsjz0tSl6KjOAZe .node .label text,#mermaid-svg-rhsjz0tSl6KjOAZe .image-shape .label,#mermaid-svg-rhsjz0tSl6KjOAZe .icon-shape .label{text-anchor:middle;}#mermaid-svg-rhsjz0tSl6KjOAZe .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rhsjz0tSl6KjOAZe .rough-node .label,#mermaid-svg-rhsjz0tSl6KjOAZe .node .label,#mermaid-svg-rhsjz0tSl6KjOAZe .image-shape .label,#mermaid-svg-rhsjz0tSl6KjOAZe .icon-shape .label{text-align:center;}#mermaid-svg-rhsjz0tSl6KjOAZe .node.clickable{cursor:pointer;}#mermaid-svg-rhsjz0tSl6KjOAZe .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rhsjz0tSl6KjOAZe .arrowheadPath{fill:#333333;}#mermaid-svg-rhsjz0tSl6KjOAZe .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rhsjz0tSl6KjOAZe .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rhsjz0tSl6KjOAZe .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rhsjz0tSl6KjOAZe .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rhsjz0tSl6KjOAZe .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rhsjz0tSl6KjOAZe .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rhsjz0tSl6KjOAZe .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rhsjz0tSl6KjOAZe .cluster text{fill:#333;}#mermaid-svg-rhsjz0tSl6KjOAZe .cluster span{color:#333;}#mermaid-svg-rhsjz0tSl6KjOAZe div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rhsjz0tSl6KjOAZe .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rhsjz0tSl6KjOAZe rect.text{fill:none;stroke-width:0;}#mermaid-svg-rhsjz0tSl6KjOAZe .icon-shape,#mermaid-svg-rhsjz0tSl6KjOAZe .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rhsjz0tSl6KjOAZe .icon-shape p,#mermaid-svg-rhsjz0tSl6KjOAZe .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rhsjz0tSl6KjOAZe .icon-shape .label rect,#mermaid-svg-rhsjz0tSl6KjOAZe .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rhsjz0tSl6KjOAZe .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rhsjz0tSl6KjOAZe .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rhsjz0tSl6KjOAZe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 当前状态

100% 硬编码颜色
第25篇

  • 14个颜色资源

  • Toggle 开关

  • 2处演示替换
    后续篇章

每次改文件顺带替换

硬编码 → $r()
目标状态

核心路径100%资源化

老旧代码逐步替换

图三解读 :本篇文章只完成第一步------建立颜色基础设施,并替换两处关键页面作为验证。后续每改一个文件就顺手把硬编码颜色替换为 $r(),最终实现全量迁移。这种策略避免了"一次性大重构"的风险,也与开发节奏自然融合。

本篇只替换了两处作为演示:

typescript 复制代码
// Index.ets ------ 页面背景
.backgroundColor($r('app.color.bg_page'))   // 原 '#FFF8F0'

// DeviceControlCard.ets ------ 卡片背景
.backgroundColor($r('app.color.bg_card'))    // 原 Color.White

验证效果

  • ✅ 浅色模式:Index 背景 #FFF8F0(暖米色),设备卡片 #FFFFFF(纯白)
  • ✅ 深色模式:Index 背景 #121212(深灰),设备卡片 #1E1E1E(浅黑)
  • ✅ 切换 Toggle 后,两处颜色立即变化,其余硬编码颜色保持不变

运行截图


五、代码增删改清单

文件 新增/修改 说明
resources/base/element/color.json 修改 新增 14 个语义化颜色值(浅色默认)
resources/dark/element/color.json 修改 新增 13 个暗色覆盖值
pages/ProfilePage.ets 修改 新增 Toggle 深色模式开关(~25行)
pages/Index.ets 修改 页面背景改为 $r('app.color.bg_page')(2行)
components/DeviceControlCard.ets 修改 卡片背景改为 $r('app.color.bg_card')(1行)

六、设计决策

决策 选择 理由
颜色命名方式 语义化(bg_page)而非色值(orange 换主题色时只需改资源文件,不碰代码
暗色主色 #FF8C5A(比浅色 #FF6B35 更亮) 深色背景上需要更高对比度的颜色才醒目
迁移策略 渐进式(只替换2处演示,其余逐步) 全量替换 300+ 处硬编码风险大,后续每改一个文件顺带替换
text_white 是否覆盖 否(dark 目录不含此色) 白色在深色背景上不需要变化,缺省即沿用 base
切换入口位置 ProfilePage(设置页) 符合用户预期------"设置"中切换主题
aboutToAppear 读取当前模式 ctx.config.colorMode 读取 保证 Toggle 初始状态与系统当前模式一致

七、本阶段总结与下篇预告

本篇是《灵犀厨房》系列中"代码量最少、视觉冲击最大"的一篇:

  • 14 个颜色资源:覆盖主题色、背景、文字、分割线、状态色五大类
  • 1 个 Toggle 开关:浅色/深色一键切换,全局即时生效
  • 渐进式迁移:不追求一次性全替换,2 处演示 + 后续逐文件迁移
  • 零破坏:硬编码颜色继续工作,与新资源引用共存

深色模式不是"把白底换成黑底"------主色要调亮以保持对比度,分割线要降低到仅可见,背景层级要用不同深度的灰色区分。一个好的深色模式,用户切换后感觉"本该如此"。

下篇预告:第 26 篇《响应式布局:折叠屏与平板完美适配》。让《灵犀厨房》在手机、折叠屏、平板上都展现最佳布局,让同一个 App 在不同屏幕尺寸上都有量身定做的体验。我们下一篇见!


📚 本系列持续更新中 :下一篇将实现跨设备响应式布局,让 App 自由穿梭于手机、平板、折叠屏。

🔗 专栏入口《HarmonyOS6.1全场景实战》合集
📦 获取基线版本源码包包括第1-15篇所有代码 + 架构文档 + Flask 后端
如果你觉得这篇文章对您有所帮助,麻烦您动动发财之手点赞 👍、收藏 ⭐ 和评论 💬。谢谢大家!!

相关推荐
若兰幽竹1 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(二十二) | 多媒体 | AVPlayer嵌入教学视频——让智慧屏真正“活”起来
音视频·华为鸿蒙系统·harmonyos6.1.0·灵犀厨房·harmonyos6.1
若兰幽竹1 天前
HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(二十三):【交互动效】转场、列表动画与趣味反馈——让每一次点击都有温度
交互·华为鸿蒙系统·harmonyos6.1
若兰幽竹2 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(二十一):【服务卡片】在桌面查看烹饪进度——主进程强推与跨进程桥接
服务卡片·华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹2 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(排错指南):【服务卡片跳转】页面栈“迷航”——从“回不去的主页”到精准 Tab 唤醒的全链路修复
华为鸿蒙系统·灵犀厨房·harmonyos6.1·排错指南
若兰幽竹4 天前
HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(番外篇):【深度排查】24小时死磕服务卡片不刷新,我踩平了 API 23 的所有底坑
华为鸿蒙系统·harmonyos6.1.0·灵犀厨房
若兰幽竹5 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(二十)扩展:【工程集成】主应用 + 元服务 + HSP 共享库——三模块一体化架构
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹5 天前
HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(二十点五):【排错指南】元服务跳转主应用——Want 参数传递的五个陷阱与架构修复
元服务·华为鸿蒙系统·harmonyos6.1·排除指南
若兰幽竹6 天前
HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(二十):【元服务】一键烹饪推荐原子化服务——免安装直达美味
华为鸿蒙系统·harmonyos6.1.0·灵犀厨房
若兰幽竹6 天前
HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(十九):【通知系统】延时烹饪提醒——让通知不再错过关键步骤
华为鸿蒙系统·harmonyos6.1.0·灵犀厨房