HarmonyOS智能家居——设置页面的分组菜单与开关交互
设置页面是智能家居 App 里菜单项最多的页面——三组菜单共 12 个选项,其中 5 个是可切换的开关,7 个是跳转链接。从截图看,页面分四个区域:顶部用户卡片、设备管理(3 项链接)、偏好设置(5 项开关)、系统(4 项链接),底部有一个红色的"退出登录"按钮。实现上用了三个 Builder(Link、Toggle、Div),把菜单项的两种形态——可跳转的链接和可切换的开关——分别抽象成了独立的 Builder。
完整效果
数据结构
@State notifyOn: boolean = true
@State darkMode: boolean = false
@State autoUpdate: boolean = true
@State voiceWake: boolean = true
@State energySave: boolean = false

五个 @State 布尔值分别控制五个开关的状态。每个开关都是独立的——修改"通知提醒"不会影响"深色模式"。这种"每个开关一个变量"的做法虽然啰嗦(5 个变量),但最直观,新手也能看懂。
更紧凑的做法是用一个对象管理所有开关:
@State settings: Record<string, boolean> = {
notify: true, darkMode: false, autoUpdate: true,
voiceWake: true, energySave: false
}
但 ArkTS 里 Record 的属性修改不会自动触发 @State 刷新,需要额外处理。在当前规模下,5 个独立变量是更稳妥的选择。
用户卡片
Row() {
Stack() {
Column() {}.width(50).height(50).borderRadius(25)
.linearGradient({ angle: 135, colors: [[A, 0], ['#A78BFA', 1]] })
Text('🏠').fontSize(22)
}
Column() {
Text('我的家').fontSize(16).fontWeight(FontWeight.Bold).fontColor(T1)
Text('5个房间 · 17台设备').fontSize(12).fontColor(T3).margin({ top: 2 })
}.alignItems(HorizontalAlign.Start).margin({ left: 14 }).layoutWeight(1)
SymbolGlyph($r('sys.symbol.chevron_right')).fontSize(14).fontColor(['#DDD'])
}
.width('100%').padding(18).backgroundColor('#FFFFFF').borderRadius(18)
.margin({ left: 16, right: 16, bottom: 16 })
.onClick(() => { router.pushUrl({ url: 'pages/RoomPage', params: { 'room': '客厅' } as Record<string,Object> }) })

用户卡片的结构和运动健康 App、旅行 App 的个人中心卡片一致——左侧渐变圆头像 + 中间文字 + 右侧箭头。点击后跳转到 RoomPage,默认显示客厅。
"5个房间 · 17台设备"是硬编码的——没有从 ROOMS.length 和 DEVICES.length 动态计算。如果以后数据变化,需要手动更新这个字符串。更好的做法是动态拼接:
Text(ROOMS.length + '个房间 · ' + DEVICES.length + '台设备')
但在原型阶段,写死更省事。
三组菜单的分组结构
// 设备管理
Column({ space: 0 }) {
Text('设备管理').fontSize(12).fontWeight(FontWeight.Bold).fontColor(T3)
.width('100%').padding(14).padding({ bottom: 4 })
this.Link('🏠', '家庭管理', '管理房间和设备', () => { ... })
this.Div()
this.Link('🔊', '语音助手', '小爱同学 · 已连接', () => { ... })
this.Div()
this.Link('🔌', '添加设备', '扫描附近的新设备', () => { ... })
}

每组菜单用一个 Column({ space: 0 }) 包裹,顶部是灰色的组标题(12px、T3 颜色),下方是菜单项列表。space: 0 保证菜单项之间没有额外间距——间距由 Div 分割线控制。
组标题用 fontSize(12) + fontWeight(Bold) + fontColor(T3),视觉上比菜单项更小更淡,形成明确的主次关系。标题下方的 padding({ bottom: 4 }) 给标题和第一个菜单项之间留了一点呼吸空间。
菜单项的两种形态
设备管理和系统组用的是 Link(带右箭头的跳转链接),偏好设置组用的是 Toggle(带开关状态的切换项)。两种形态的视觉结构几乎一样——左侧 emoji + 中间文字(标题 + 副标题),区别只在右侧:
- Link:灰色右箭头(
SymbolGlyph) - Toggle:绿色"开"或灰色"关"文字
Link Builder
@Builder Link(icon: string, label: string, sub: string, action: () => void) {
Row() {
Text(icon).fontSize(18).margin({ right: 12 })
Column() {
Text(label).fontSize(14).fontColor(T1)
Text(sub).fontSize(11).fontColor(T3).margin({ top: 1 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
SymbolGlyph($r('sys.symbol.chevron_right')).fontSize(12).fontColor(['#DDD'])
}.width('100%').padding({ left: 14, right: 14, top: 12, bottom: 12 })
.onClick(action)
}
四个参数:图标、标题、副标题、回调。副标题用 T3 颜色(#BBBBBB)和 11px 字号,比标题(T1、14px)更淡更小,形成信息层次。右箭头用系统 SymbolGlyph 而不是 emoji,视觉上更规范。
Toggle Builder
@Builder Toggle(icon: string, label: string, sub: string, on: boolean, action: () => void) {
Row() {
Text(icon).fontSize(18).margin({ right: 12 })
Column() {
Text(label).fontSize(14).fontColor(T1)
Text(sub).fontSize(11).fontColor(T3).margin({ top: 1 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text(on ? '🟢 开' : '⚫ 关').fontSize(12).fontColor(on ? A : T3)
}.width('100%').padding({ left: 14, right: 14, top: 12, bottom: 12 })
.onClick(action)
}
五个参数:图标、标题、副标题、开关状态、回调。比 Link 多了一个 on 参数,控制右侧显示"🟢 开"还是"⚫ 关"。
开关状态文字的颜色也跟随变化——开启时用主色(#6C5CE7),关闭时用 T3 灰色。这让开关状态不只是文字不同,颜色也提供了视觉反馈。
和原生 Toggle 组件的区别
HarmonyOS 有内置的 Toggle 组件,但这里没有用。原因是内置 Toggle 的样式比较固定——一个滑块在圆形轨道上左右滑动。而这个页面的开关用文字(“开”/“关”)+ emoji 圆点来表示,视觉上更轻量,和菜单项的文字风格更协调。
在需要"安静"的设置页面里,文字开关比滑块开关更不抢眼——用户扫一眼就能看到哪些开着哪些关着,不需要辨认滑块的位置。
分割线 Div Builder
@Builder Div() {
Divider().strokeWidth(0.5).color('#F0EEF4').margin({ left: 56 })
}
分割线从左侧 56px 开始——跳过了 emoji 图标(18px)+ 右边距(12px)+ 一点余量,让分割线从文字区域开始,不会穿过图标。这个 margin({ left: 56 }) 的值和运动健康 App、旅行 App 里的分割线处理方式一致。
strokeWidth(0.5) 是最细的分割线——0.5vp 在高分辨率屏幕上几乎是一条细线,既能分隔菜单项,又不会太显眼。
退出登录按钮
Row() {
Text('退出登录').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#FF6B6B')
}
.width('100%').height(50).backgroundColor('#FFFFFF').borderRadius(18)
.justifyContent(FlexAlign.Center).margin({ left: 16, right: 16, bottom: 30 })
.onClick(() => { router.back() })
红色文字、居中对齐、独立的白色卡片。点击后只是 router.back()——没有实际的退出登录逻辑,只是一个 UI 占位。
按钮用 justifyContent(FlexAlign.Center) 让文字水平居中,和菜单项的左对齐形成对比。视觉上,退出按钮和其他菜单项明显不同——它不是"菜单里的一项",而是"独立的操作"。
三组菜单的功能映射
| 分组 | 菜单项 | 类型 | 点击行为 |
|---|---|---|---|
| 设备管理 | 家庭管理 | Link | 跳转 RoomPage |
| 设备管理 | 语音助手 | Link | 切换 voiceWake |
| 设备管理 | 添加设备 | Link | 跳转 RoomPage |
| 偏好设置 | 通知提醒 | Toggle | 切换 notifyOn |
| 偏好设置 | 深色模式 | Toggle | 切换 darkMode |
| 偏好设置 | 自动更新 | Toggle | 切换 autoUpdate |
| 偏好设置 | 语音唤醒 | Toggle | 切换 voiceWake |
| 偏好设置 | 节能模式 | Toggle | 切换 energySave |
| 系统 | 网络设置 | Link | 跳转 RoomPage |
| 系统 | 固件更新 | Link | 跳转 SecurityPage |
| 系统 | 数据统计 | Link | 跳转 EnergyPage |
| 系统 | 关于 | Link | router.back() |
12 个菜单项中,只有 5 个有实际的状态切换逻辑(偏好设置组),其他 7 个要么跳转到已有页面,要么是空操作。这种"少数真实、多数占位"的分布是设置页面的典型状态——先把菜单结构搭好,功能后面逐步填充。
语音助手的特殊行为
语音助手在设备管理组里用的是 Link 样式(有右箭头),但点击行为是切换 voiceWake 状态——和偏好设置里的"语音唤醒"切换的是同一个变量。这意味着点击"语音助手"会同时影响两个菜单项的显示状态,因为它们共享 @State voiceWake。
这种"两个菜单项控制同一个状态"的设计在实际 App 里不太常见,但在原型阶段可以接受——它演示了 @State 变量可以在不同 Builder 之间共享。
设置页面和其他页面的状态差异
设置页面的 @State 变量和其他页面有一个关键区别——它们的修改不会影响其他页面:
- 修改
notifyOn不会影响首页的任何显示 - 修改
darkMode不会真的切换深色主题 - 修改
energySave不会影响设备的实际能耗
这是因为这些开关目前只控制页面内的显示状态,没有接入真实的功能逻辑。在真正的 App 里,darkMode 需要切换全局主题,energySave 需要修改设备的运行参数,notifyOn 需要控制系统的通知权限。这些功能需要通过服务层或全局状态管理来实现,不能只靠页面内的 @State。
更多推荐
所有评论(0)