HarmonyOS 应用 · 寝室投票投票页深度解析:Radio 单选与 enabled 按钮联动

项目编号:49-dorm-vote 技术栈:HarmonyOS ArkTS · ArkUI 声明式 UI · Stage 模型 本篇页面:投票页 Func2Tab.ets(81 行) 系列导航:(一)首页 · (二)创建 · (三)投票 · (四)结果

49 投票页首屏


一、Option interface 与 selected 状态

interface Option {
  id: number;
  emoji: string;
  name: string;
}

三个字段:id 是 key 锚点,emoji 是选项图标,name 是选项名。

  @StorageProp('safeTop') safeTop: number = 0;
  @StorageProp('safeBottom') safeBottom: number = 0;
  @State selected: number = -1;

  private options: Option[] = [
    { id: 1, emoji: '🍜', name: '黄焖鸡米饭' },
    { id: 2, emoji: '🍔', name: '汉堡炸鸡' },
    { id: 3, emoji: '🍕', name: '披萨意面' },
    { id: 4, emoji: '🍛', name: '麻辣香锅' }
  ];

@State selected: number = -1——初始 -1 表示未选中任何选项。-1 是"哨兵值":selected >= 0 即有选中项,驱动按钮的启用/禁用。四个外卖选项:黄焖鸡/汉堡炸鸡/披萨意面/麻辣香锅。


二、页面骨架:QuestionCard + OptionList + Button

  build() {
    Column() {
      this.Header()
      Scroll() {
        Column({ space: 16 }) {
          this.QuestionCard()
          this.OptionList()
          Button(this.selected >= 0 ? '确认投票' : '请选择一个选项').fontSize(15)
            .fontColor('#FFFFFF').backgroundColor(this.selected >= 0 ? C.primary : C.textDim)
            .width('100%').height(48).borderRadius(D.rMd)
            .enabled(this.selected >= 0)
            .onClick(() => { promptAction.showToast({ message: '投票成功' }); })
        }
        .width('100%')
        .padding({ left: D.pad, right: D.pad, top: 16, bottom: D.pad + this.safeBottom + 20 })
      }
      .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
    }
    .width('100%').height('100%').backgroundColor(C.bg)
  }

selected 驱动按钮三态——文案、底色、可点性三路分支:

// 文案:确认投票 vs 请选择一个选项
Button(this.selected >= 0 ? '确认投票' : '请选择一个选项')

// 底色:粉色 vs 灰色
.backgroundColor(this.selected >= 0 ? C.primary : C.textDim)

// 可点性:启用 vs 禁用
.enabled(this.selected >= 0)

enabled() 是本页特色——系列首次使用组件禁用属性。未选中时按钮灰色 + 禁用(点击无效),选中后按钮变粉 + 启用——必须选择才能投票的强约束,比 Toast 校验更前置。


三、Header 与 QuestionCard:问题上下文

  @Builder
  Header() {
    Row() { Text('参与投票').fontSize(20).fontWeight(FontWeight.Bold).fontColor(C.text) }
    .width('100%').height(this.safeTop + 56).padding({ top: this.safeTop, left: D.pad, right: D.pad })
    .backgroundColor(C.card).alignItems(VerticalAlign.Bottom)
  }

  @Builder
  QuestionCard() {
    Column({ space: 8 }) {
      Text('🍜 今晚点什么外卖?').fontSize(18).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%')
      Text('单选 · 3/4 人已投票 · 剩余 2 小时').fontSize(12).fontColor(C.textDim).width('100%')
    }
    .width('100%').padding(16).backgroundColor(C.card).borderRadius(D.rLg).border({ width: 1, color: C.stroke })
  }

18 号加粗问题 + 12 号灰色上下文"单选 · 3/4 人已投票 · 剩余 2 小时"——一句话交代投票类型、进度、剩余时间。问题卡让用户先理解"投什么"再选择。


四、OptionList:Radio 单选 + 卡片边框联动

  @Builder
  OptionList() {
    Column({ space: 12 }) {
      ForEach(this.options, (o: Option, idx: number) => {
        Row({ space: 12 }) {
          Text(o.emoji).fontSize(26)
          Text(o.name).fontSize(15).fontColor(C.text).layoutWeight(1)
          Radio({ value: o.name, group: 'vote' })
            .checked(this.selected === idx).radioStyle({ checkedBackgroundColor: C.primary })
            .onClick(() => { this.selected = idx; })
        }
        .width('100%').padding(16)
        .backgroundColor(this.selected === idx ? C.primarySoft : C.card).borderRadius(D.rMd)
        .border({ width: this.selected === idx ? 2 : 1, color: this.selected === idx ? C.primary : C.stroke })
        .onClick(() => { this.selected = idx; })
      }, (o: Option) => o.id.toString())
    }.width('100%')
  }

Radio 组件——系列首次出现的单选组件:

Radio({ value: o.name, group: 'vote' })
  .checked(this.selected === idx)
  .radioStyle({ checkedBackgroundColor: C.primary })
  .onClick(() => { this.selected = idx; })

group: 'vote' 同组 Radio 互斥(单选语义的系统级保证),checked(this.selected === idx) 绑定选中态,radioStyle({ checkedBackgroundColor: C.primary }) 选中圆点粉色。

卡片双态联动——selected === idx 驱动底色与边框:

// 底色:浅粉 vs 白底
.backgroundColor(this.selected === idx ? C.primarySoft : C.card)

// 边框:2px 粉色 vs 1px 浅灰
.border({ width: this.selected === idx ? 2 : 1, color: this.selected === idx ? C.primary : C.stroke })

选中项卡片浅粉底 + 2px 粉色边框(加粗突出),未选白底 + 1px 浅灰边框。整卡 onClick 设 this.selected = idx——点击卡片任意位置(图标/名称/Radio)都能选中。

49 投票页投票成功

49 投票页选中选项

49 投票页完整视图


五、单选状态链路

selected 形成三处联动:

点击卡片/Radio → selected = idx
                 → Radio checked 转移(单选互斥)
                 → 卡片底色/边框高亮转移
                 → 按钮变"确认投票"粉色 + enabled 启用

一次点击触发三处刷新——Radio 组互斥 + 卡片高亮 + 按钮启用的完整链路。enabled 的存在让"未选择"状态在按钮层面就被拦截,无需 Toast 提示。


六、可演进方向

方向现状改法收益
投票写入Toast 模拟服务器提交 + 票数+1真实投票
多选支持仅单选multiChoice 时 Checkbox + selected 数组单/多选切换
倒计时静态"剩余 2 小时"动态倒计时 + 到时禁用投票限时
投票后反馈Toast跳转结果页查看实时票数闭环反馈
已投状态可重复投记录已投选项 + 按钮变已投一人一票

投票页是投票的核心交互——selected 驱动 Radio 互斥、卡片高亮、按钮启用的三处联动。从哨兵值 -1 的状态设计到 enabled 的禁用约束,每一层都有明确的职责边界。

系列导航:(一)首页 · (二)创建 · (三)投票 · (四)结果

Logo

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

更多推荐