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

页面结构
从上到下三个区域:
- 导航栏:返回按钮 + 房间名称 + 设备数量
- 设备列表:纵向滚动,每个设备一张卡片
- 底部留白:为可能的底部操作预留空间
每张设备卡片内部有两种形态——普通设备只显示名称和开关,可调节设备(灯、空调)在开启状态下多一个 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: #6C5CE7,selectedColor: #A78BFA) - 轨道颜色:极浅紫(
#F0ECF8) - 轨道粗细:4vp
空调的温度滑块
- 范围:16-30(摄氏度)
- 滑块颜色:红色系(
blockColor: #FF6B6B,selectedColor: #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 是获取最新数据的方法,两者配合实现了跨页面的数据同步。
更多推荐



所有评论(0)