HarmonyOS智能家居——首页仪表盘架构拆解
智能家居 App 的首页是一个信息密集的控制中枢——一个页面里同时展示了设备状态总览、房间导航、快捷设备开关、能耗趋势图、智能场景控制五个区块。和其他类型的 App 不同,智能家居首页不只是数据的展示层,用户在首页就能直接操作——点击快捷控制切换设备开关,点击场景卡片激活不同的家居模式。这意味着首页的状态管理需要同时处理"读取数据"和"修改后刷新"两个方向。
完整效果

颜色体系
const A: string = '#6C5CE7' // 主色·科技紫
const BG: string = '#F5F4FA' // 背景灰
const CW: string = '#FFFFFF' // 卡片白
const T1: string = '#1E1B2E' // 标题黑
const T2: string = '#888888' // 副文本
紫色 #6C5CE7 作为主色,在智能家居场景里代表科技感和未来感。背景色 #F5F4FA 比纯灰更偏冷调,和紫色搭配比暖灰色更协调。T1/T2 两级文本颜色负责文字层次,没有用到 T3(#BBBBBB),因为首页的辅助文字不多。
数据层:DeviceData 的完整架构
export interface Device {
id: number; name: string; icon: string; type: string
room: string; on: boolean; value: number; color: string
}
export interface Room {
name: string; icon: string; deviceCount: number
activeCount: number; color: string
}
export interface Scene {
name: string; icon: string; desc: string
active: boolean; color: string
}
export interface EnergyRecord { time: string; usage: number }

四个接口定义了智能家居的四个核心实体。Device 的 on 字段控制开关状态,value 存储数值参数(亮度百分比、温度摄氏度等),type 区分设备类型(light/thermostat/appliance/curtain)。Room 用名称而不是 ID 作为标识,因为房间数量有限且名称唯一。Scene 的 active 字段控制场景的运行状态。
操作函数
export function toggleDevice(id: number): boolean {
for (let i: number = 0; i < DEVICES.length; i++) {
if (DEVICES[i].id === id) { DEVICES[i].on = !DEVICES[i].on; return DEVICES[i].on }
}
return false
}
export function getActiveCount(): number {
let c: number = 0
for (let i: number = 0; i < DEVICES.length; i++) { if (DEVICES[i].on) c++ }
return c
}
export function getDevicesByRoom(room: string): Device[] {
const result: Device[] = []
for (let i: number = 0; i < DEVICES.length; i++) {
if (DEVICES[i].room === room) result.push(DEVICES[i])
}
return result
}

toggleDevice 直接修改 DEVICES 数组里对应设备的 on 属性,然后返回新的开关状态。getActiveCount 遍历数组统计 on === true 的设备数量。getDevicesByRoom 按房间名称过滤设备,返回该房间的所有设备列表。
这些函数直接操作模块级的 DEVICES 常量——因为它是 export const 的引用类型(数组),任何导入它的页面都能读写同一份数据。这种"共享可变状态"让所有页面看到的设备状态保持一致,但也意味着任何页面的修改都会影响其他页面。在原型阶段这是合理的,后续做数据持久化时需要替换成统一的服务层管理。
@State 的双重刷新机制
@State devCount: number = getActiveCount()
@State scenes: Scene[] = []
aboutToAppear(): void {
const s: Scene[] = []
for (let i: number = 0; i < SCENES.length; i++) { s.push(SCENES[i]) }
this.scenes = s
}
onPageShow(): void { this.devCount = getActiveCount() }
devCount 在两个时机被更新——初始赋值时和每次 onPageShow 时。onPageShow 保证了用户从 RoomPage 返回时,如果在那里关闭了一个设备,首页的在线设备数会自动更新。
scenes 在 aboutToAppear 里从 SCENES 常量复制到 @State。复制的目的是让组件拥有自己的数据副本——如果直接引用 SCENES,修改 active 属性会影响原始数据,可能导致其他页面看到意外的状态变化。
场景激活的排他逻辑
private activateScene(idx: number): void {
for (let i: number = 0; i < this.scenes.length; i++) {
this.scenes[i].active = false
}
this.scenes[idx].active = true
const copy: Scene[] = []
for (let j: number = 0; j < this.scenes.length; j++) { copy.push(this.scenes[j]) }
this.scenes = copy
}
激活场景时先执行排他操作——把所有场景设为非激活,再把选中的场景设为激活。这保证了同一时间只有一个场景在运行。
最后的"复制 + 替换引用"是 ArkTS @State 的固定套路:直接修改数组元素的属性不会触发 UI 刷新,必须创建新数组替换整个引用。这个模式在需要动态修改数组的场景里反复出现。
设备状态卡片
Stack({ alignContent: Alignment.Center }) {
Column() {}.width('100%').height(110).borderRadius(20)
.linearGradient({ angle: 135, colors: [[A, 0], ['#7C6FF0', 0.5], ['#A78BFA', 1]] })
Row() {
Column() {
Text('设备状态').fontSize(14).fontColor('rgba(255,255,255,0.7)')
Text(this.devCount + '/' + DEVICES.length + ' 在线').fontSize(22)
.fontWeight(FontWeight.Bold).fontColor(Color.White).margin({ top: 4 })
}.alignItems(HorizontalAlign.Start)
Text('🟢').fontSize(44)
}.width('100%').padding(20)
}

三段紫色渐变背景,左侧显示在线设备数(devCount/总数 在线),右侧用绿色圆点表示"系统正常"。devCount 是 @State 变量,快捷控制切换设备后会触发这里重新渲染。
渐变的三段颜色从深紫(#6C5CE7)过渡到中紫(#7C6FF0)再到浅紫(#A78BFA),135° 对角线方向。投影用紫色系的 14% 透明度(A + '25'),和卡片背景色协调。
房间横向滚动
Scroll() {
Row({ space: 10 }) {
ForEach(ROOMS, (room: Room) => {
Column() {
Text(room.icon).fontSize(30).margin({ bottom: 8 })
Text(room.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(T1)
Text(room.activeCount + '/' + room.deviceCount + ' 设备').fontSize(10).fontColor(T2)
}.width(100).padding(14).backgroundColor(CW).borderRadius(16)
.border({ width: 1.5, color: room.color + '30' })
.onClick(() => {
router.pushUrl({ url: 'pages/RoomPage', params: { 'room': room.name } as Record<string,Object> })
})
})
}
}
.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)
五个房间卡片横向排列,超出屏幕时可以滚动。每个卡片 100vp 宽,包含房间图标、名称、在线设备数。
边框颜色是每个房间的主题色加 18% 透明度:room.color + '30'。客厅是红色边框(#FF6B6B30),卧室是紫色边框(#8A5DFA30),厨房是橙色边框(#FF9F4330)。这种彩色边框让用户不用看文字就能区分不同房间。
点击跳转到 RoomPage,传递房间名称作为参数。RoomPage 根据名称从 getDevicesByRoom() 获取该房间的设备列表。
快捷控制网格
Grid() {
GridItem() {
Row() {
Text('💡').fontSize(22).margin({ right: 10 })
Text('客厅主灯').fontSize(12).fontColor(T1).layoutWeight(1)
Text(DEVICES[0].on ? '🟢' : '⚪').fontSize(16)
}.width('100%').padding(12).backgroundColor(CW).borderRadius(14)
.onClick(() => { toggleDevice(1); this.devCount = getActiveCount() })
}
// ... 其他三个设备
}
.columnsTemplate('1fr 1fr').columnsGap(10).rowsGap(10)
2×2 网格展示四个常用设备。右侧的绿色/灰色圆点直接从 DEVICES[x].on 读取状态——因为 toggleDevice 修改的是模块级数组,这里读到的永远是最新状态。
点击时执行两步操作:先 toggleDevice 切换设备开关,再 getActiveCount 重新统计在线数赋值给 @State devCount。这保证了快捷控制的圆点颜色和顶部状态卡片的数字同步更新。
四个设备是硬编码的(DEVICES[0]、DEVICES[1]、DEVICES[2]、DEVICES[5]),对应客厅主灯、客厅空调、电视、床头灯。这是"首页快捷入口"的设计——把最常用的设备固定在首页,用户不需要进入房间页就能快速控制。
能耗柱状图
Row() {
ForEach(ENERGY_DATA, (e: EnergyRecord) => {
Column() {
Column() {}.width(18).height(e.usage * 30).borderRadius(9)
.linearGradient({ angle: 180, colors: [[A, 0], ['#A78BFA', 1]] })
Text(e.time).fontSize(9).fontColor(T2).margin({ top: 4 })
}.alignItems(HorizontalAlign.Center).layoutWeight(1)
})
}
.height(80).alignItems(VerticalAlign.Bottom)
7 条能耗记录生成 7 个柱子。柱子高度用 e.usage * 30 计算——usage 范围是 0.2 到 2.8 kWh,乘以 30 后变成 6vp 到 84vp。alignItems(VerticalAlign.Bottom) 让所有柱子底部对齐,形成标准的柱状图。
紫色渐变从上到下(angle: 180),深紫在上、浅紫在下,和顶部状态卡片的渐变方向一致。每个柱子 18vp 宽,圆角 9vp(半圆顶部),视觉上比较圆润。
智能场景卡片
ForEach(this.scenes, (s: Scene, idx: number) => {
Column() {
Text(s.icon).fontSize(28).margin({ bottom: 6 })
Text(s.name).fontSize(11).fontWeight(FontWeight.Bold).fontColor(T1)
Text(s.active ? '运行中' : '点击启动').fontSize(9)
.fontColor(s.active ? s.color : T2).margin({ top: 2 })
}.width(90).padding(12)
.backgroundColor(s.active ? s.color + '15' : CW)
.borderRadius(14)
.border({ width: s.active ? 1.5 : 1, color: s.active ? s.color : '#F0EEF4' })
.onClick(() => { this.activateScene(idx) })
})
五个场景卡片有两种视觉形态:
- 未激活:白色背景 + 1px 灰色边框 + 灰色"点击启动"
- 激活中:主题色 8% 透明度背景(
s.color + '15')+ 1.5px 主题色边框 + 主题色"运行中"
双重视觉反馈——背景色和边框色同时变化,让用户一眼区分哪个场景在运行。每个场景有自己的主题色(离家红色、回家绿色、睡眠紫色、影院黄色、起床橙色),激活时边框和文字都变成对应颜色。
底部导航栏
@Builder NavBtn(label: string, icon: string, active: boolean) {
Column({ space: 2 }) {
Text(icon).fontSize(20)
Text(label).fontSize(9).fontColor(active ? A : '#C0BFC6')
}.onClick(() => {
if (label === '房间') router.pushUrl({ url: 'pages/RoomPage', params: { 'room': '客厅' } as Record<string,Object> })
else if (label === '安防') router.pushUrl({ url: 'pages/SecurityPage' })
else if (label === '能耗') router.pushUrl({ url: 'pages/EnergyPage' })
else if (label === '设置') router.pushUrl({ url: 'pages/SettingsPage' })
})
}
五个 Tab:首页、房间、安防、能耗、设置。“房间” Tab 传了默认参数 'room': '客厅',因为 RoomPage 需要房间名称来显示对应设备列表。“首页” Tab 没有跳转逻辑——用户已在首页,点击无操作。
页面数据流
DeviceData (设备/房间/场景/能耗数据 + 操作函数)
│
├→ Index (首页仪表盘)
│ ├→ toggleDevice() 切换设备状态
│ ├→ getActiveCount() 统计在线数
│ ├→ activateScene() 激活场景
│ └→ RoomPage (传房间名称,查看房间设备)
│
├→ RoomPage
│ └→ getDevicesByRoom() 按房间查询设备
│
├→ SecurityPage (安防监控)
├→ EnergyPage (能耗详情)
└→ SettingsPage (设置)
首页是数据流的核心——它调用 toggleDevice 和 activateScene 修改数据,调用 getActiveCount 读取汇总数据,通过路由把用户引导到各个子页面。所有页面共享同一份 DEVICES 数组,修改和读取操作不会产生数据不一致的问题。
首页的信息架构
五个区块的排列顺序不是随意的,它遵循了"从宏观到微观"的信息架构:
- 设备状态卡片(宏观):一句话概括"11/17 在线",用户 1 秒内了解全局
- 房间导航(中观):按空间维度组织设备,用户可以选择进入某个房间
- 快捷控制(微观):直接操作最常用的 4 个设备,不需要进入房间
- 能耗趋势(时间维度):展示一天内的用电变化,补充时间维度的信息
- 智能场景(自动化):一键触发多个设备的联动操作
这种"全局状态 → 空间导航 → 快捷操作 → 趋势数据 → 自动化控制"的排列顺序,让用户可以按照自己的需求层次逐层深入——想了解全局看顶部,想控制具体设备看中间,想自动化操作看底部。
更多推荐



所有评论(0)