设置页面是智能家居 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.lengthDEVICES.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

Logo

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

更多推荐