房间详情页是从首页的房间卡片或底部导航栏点进去的——用户选择某个房间后,这个页面列出该房间的所有设备,并提供开关控制和参数调节。从代码看,这个页面有两个核心设计:一是按设备类型条件渲染不同的控制组件(灯显示亮度滑块,空调显示温度滑块),二是每次操作后重新查询房间设备列表来触发 UI 刷新。
完整效果
在这里插入图片描述
在这里插入图片描述

页面结构

从上到下三个区域:

  1. 导航栏:返回按钮 + 房间名称 + 设备数量
  2. 设备列表:纵向滚动,每个设备一张卡片
  3. 底部留白:为可能的底部操作预留空间

每张设备卡片内部有两种形态——普通设备只显示名称和开关,可调节设备(灯、空调)在开启状态下多一个 Slider 滑块。

参数接收与数据查询

@State room: string = '客厅'
@State devices: Device[] = []

aboutToAppear(): void {
  const p = router.getParams() as Record<string, Object>
  if (p && p['room']) { this.room = p['room'] as string }
  this.devices = getDevicesByRoom(this.room)
}

从路由参数里取出房间名称,默认值是"客厅"。然后调用 getDevicesByRoom(this.room) 获取该房间的所有设备。

getDevicesByRoom 的实现是遍历 DEVICES 数组,把 room 属性匹配的设备收集到新数组里返回。这个查询是浅拷贝——返回的数组是新的,但里面的设备对象仍然是原始 DEVICES 数组里的引用。修改返回数组里某个设备的属性,会直接影响原始数据。

设备卡片的信息展示

ForEach(this.devices, (d: Device) => {
  Column() {
    Row() {
      Text(d.icon).fontSize(28).width(50).height(50).borderRadius(14)
        .backgroundColor('#F5F5FA').textAlign(TextAlign.Center)
      Column() {
        Text(d.name).fontSize(15).fontWeight(FontWeight.Bold).fontColor(T1)
        Text(d.type === 'thermostat' ? d.value + '°C' :
          d.type === 'light' ? '亮度 ' + d.value + '%' :
          d.on ? '运行中' : '已关闭')
          .fontSize(12).fontColor(T3)
      }.alignItems(HorizontalAlign.Start).margin({ left: 12 }).layoutWeight(1)
      Text(d.on ? '🟢' : '⚪').fontSize(18)
        .onClick(() => { d.on = !d.on; this.devices = getDevicesByRoom(this.room) })
    }
  }
})

在这里插入图片描述

每个设备卡片分左右两部分:左侧是 50×50 的 emoji 图标容器(浅灰底色 + 14px 圆角),右侧是设备名称和状态文字。

状态文字根据设备类型有三种显示方式:

  • thermostat(温控器):显示当前温度,如 24°C
  • light(灯):显示当前亮度,如 亮度 80%
  • 其他设备:显示开关状态文字,运行中已关闭

这个三元表达式嵌套了三层,读起来不太直观,但它把所有设备类型的状态展示逻辑压缩成了一行代码。如果用 if/else 展开,代码量会翻倍。

开关切换的刷新逻辑

.onClick(() => { d.on = !d.on; this.devices = getDevicesByRoom(this.room) })

切换设备状态后,调用 getDevicesByRoom(this.room) 重新获取房间设备列表,赋值给 @State devices 触发 UI 刷新。

这里有一个关键细节:d.on = !d.on 修改的是 DEVICES 数组里原始设备对象的属性(因为 getDevicesByRoom 返回的是引用),而 this.devices = getDevicesByRoom(this.room) 重新查询后返回的是一个新数组。@State 检测到 devices 引用变了,触发重新渲染,所有设备卡片的状态文字和开关圆点同时更新。

如果只写 d.on = !d.on 而不重新查询数组,UI 不会刷新——因为 @State 没有检测到 devices 数组引用的变化。

条件渲染:按设备类型显示滑块

if (d.on && d.type === 'light') {
  Slider({ value: d.value, min: 1, max: 100, step: 1, style: SliderStyle.OutSet })
    .blockColor(A).trackColor('#F0ECF8').selectedColor('#A78BFA')
    .trackThickness(4).blockSize({ width: 20, height: 20 })
    .onChange((v: number) => {
      d.value = Math.round(v)
      this.devices = getDevicesByRoom(this.room)
    }).width('100%')
}

if (d.on && d.type === 'thermostat') {
  Slider({ value: d.value, min: 16, max: 30, step: 1, style: SliderStyle.OutSet })
    .blockColor('#FF6B6B').trackColor('#FFF0F0').selectedColor('#FF9F43')
    .trackThickness(4).blockSize({ width: 20, height: 20 })
    .onChange((v: number) => {
      d.value = Math.round(v)
      this.devices = getDevicesByRoom(this.room)
    }).width('100%')
}

在这里插入图片描述

两个 if 条件渲染控制了滑块的显隐——只有设备开启(d.on)且类型匹配时才显示对应的滑块。设备关闭后滑块自动消失,卡片高度缩小。

灯的亮度滑块

  • 范围:1-100(百分比)
  • 滑块颜色:主色紫色系(blockColor: #6C5CE7selectedColor: #A78BFA
  • 轨道颜色:极浅紫(#F0ECF8
  • 轨道粗细:4vp

空调的温度滑块

  • 范围:16-30(摄氏度)
  • 滑块颜色:红色系(blockColor: #FF6B6BselectedColor: #FF9F43
  • 轨道颜色:极浅红(#FFF0F0
  • 轨道粗细:4vp

两种滑块用不同的颜色系区分功能——紫色代表灯光(冷色),红色代表温度(暖色)。这种颜色编码让用户不用看标签就能区分"这是调亮度的"和"这是调温度的"。

SliderStyle.OutSet 的选择

SliderStyle.OutSet 让滑块的圆形把手(block)突出显示在轨道上方,而不是嵌入轨道内部。这种样式在需要精确调节的场景里更常见——把手更大(20×20vp),用户更容易拖动。

如果用 SliderStyle.InSet,把手会嵌入轨道内部,视觉上更紧凑,但操作精度会降低。对于亮度和温度这种需要精确控制的参数,OutSet 是更合适的选择。

onChange 的刷新逻辑

.onChange((v: number) => {
  d.value = Math.round(v)
  this.devices = getDevicesByRoom(this.room)
})

滑块拖动时,Math.round(v) 把浮点数取整(比如 78.6 → 79),赋值给设备的 value 属性,然后重新查询房间设备列表触发刷新。

Math.round 是必要的——Slider 的 step: 1 虽然指定了步长为 1,但在某些设备上拖动时仍可能产生浮点数(比如 79.0000001)。取整后保证 value 始终是整数,显示在 UI 上不会出现"亮度 78.99999%"这种尴尬的情况。

卡片高度的动态适配

Row() {
  // ... 设备图标、名称、开关
}.width('100%').margin({ bottom: d.on && (d.type === 'light' || d.type === 'thermostat') ? 8 : 0 })

这行代码给设备卡片的底部加了一个条件 margin——当设备是灯或空调且处于开启状态时,底部留 8px 间距,为下方的 Slider 滑块预留空间。设备关闭或不是灯/空调类型时,底部间距为 0。

这个间距的变化让卡片高度能够自适应内容——有滑块时卡片更高(多出滑块的高度 + 8px 间距),没有滑块时卡片更紧凑。用户在滚动列表时能明显感觉到"这个设备可以调参数"和"这个设备只能开关"的区别。

数据流的闭环

房间页面的数据流形成了一个完整的闭环:

首页 (Index)
  │
  │ params: room (房间名称)
  ↓
RoomPage
  │
  ├→ getDevicesByRoom(room)  查询房间设备
  │    ↓
  │  设备列表渲染
  │    ↓
  ├→ toggleDevice(id)        切换开关
  │    ↓
  ├→ setDeviceValue(id, val) 调节参数(Slider onChange)
  │    ↓
  └→ getDevicesByRoom(room)  重新查询 → UI 刷新

每次操作后都重新查询房间设备列表,而不是直接修改 @State devices 数组里的元素。这种"重新查询"的刷新方式虽然比"直接修改"多了一次数组遍历,但保证了数据的一致性——因为 getDevicesByRoom 读取的是 DEVICES 源数组,返回的永远是最新的数据。

如果用"直接修改"的方式(比如 this.devices[idx].on = !this.devices[idx].on),需要手动复制数组才能触发刷新。"重新查询"把"数据修改"和"UI 刷新"分成了两步——第一步修改源数据(toggleDevice / setDeviceValue),第二步重新查询获取最新数据(getDevicesByRoom)。职责分离更清晰。

三种设备类型的控制对比

设备类型 状态显示 开关控制 参数控制 Slider 范围 Slider 颜色
light 亮度 X% 🟢/⚪ 切换 亮度滑块 1-100 紫色系
thermostat X°C 🟢/⚪ 切换 温度滑块 16-30 红色系
appliance 运行中/已关闭 🟢/⚪ 切换 - -
curtain 运行中/已关闭 🟢/⚪ 切换 - -

灯和温控器有参数控制,其他设备只有开关控制。这个差异通过 if 条件渲染实现——不需要为每种设备类型写单独的组件,只需要在同一个卡片里根据类型决定是否显示滑块。

这种"同一结构 + 条件分支"的模式比"为每种类型写独立组件"更轻量。在设备类型不多(4 种)的情况下,条件分支的可读性是可以接受的。如果以后设备类型增加到十几种,可能需要考虑用策略模式或组件工厂来替代条件分支。

回到首页的数据同步

用户在 RoomPage 关闭了一个设备后,点击返回按钮回到首页:

Row() { SymbolGlyph($r('sys.symbol.chevron_left'))... }
  .onClick(() => { router.back() })

router.back() 返回首页时,首页的 onPageShow 被触发:

onPageShow(): void { this.devCount = getActiveCount() }

getActiveCount() 重新遍历 DEVICES 数组统计在线设备数——因为 RoomPage 里的 toggleDevice 已经修改了源数组,所以这里读到的是最新状态。首页的状态卡片数字和快捷控制的圆点颜色自动更新。

这个数据同步不需要额外的事件机制或回调——因为所有页面共享同一份 DEVICES 数据,修改和读取操作天然一致。onPageShow 是触发刷新的信号,getActiveCount 是获取最新数据的方法,两者配合实现了跨页面的数据同步。

Logo

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

更多推荐