HarmonyOS 校园应用系列之ArkTS 表单工程:水电缴费页的金额选择器与代扣开关联动

应用背景:10 水电缴费的缴费创建页(Func1Tab.ets,278 行)是用户发起缴费的核心表单。本文聚焦 RoomCard 房间卡TypeSelector 类型三选一AmountSelector 金额选择AutoPaySwitch 代扣开关PreviewCard 缴费预览 等模块。

应用效果图

一、页面整体结构

// Func1Tab.ets — 页面根结构
Column() {
  this.Header()
  Scroll() {
    Column({ space: 16 }) {
      this.RoomCard()
      this.TypeSelector()
      this.AmountSelector()
      this.CustomInput()
      this.AutoPaySwitch()
      this.PreviewCard()
      this.TemplateSection()
      this.SubmitBtn()
    }
    .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)

根布局为 Column 两段式:Header 固定在 Scroll 外部不随内容滚动,滚动区内部 Column({ space: 16 }) 纵向排布 8 个模块,底部 padding 动态叠加 safeBottom 安全区。

项目源码开源:https://gitee.com/codenestFlow/HarmonyOSHub

配图

二、Header 固定标题栏

// Header — 固定标题栏
@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)
}

标题栏白底,高度 safeTop + 56 动态适配状态栏,alignItems(VerticalAlign.Bottom) 让"水电缴费" 20vp 粗体标题贴底显示。固定效果由"放在 Scroll 外部"这一结构实现,无需 sticky 属性。

三、RoomCard 宿舍信息卡片

// RoomCard — 宿舍信息
@Builder
RoomCard() {
  Row({ space: 14 }) {
    Row() {
      Text('🏠').fontSize(30)
    }
    .width(56).height(56)
    .backgroundColor(C.primarySoft).borderRadius(D.rMd)
    .justifyContent(FlexAlign.Center)

    Column({ space: 4 }) {
      Text('梅园 6 栋 302 室').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text)
      Text('绑定人: 张同学 · 校园卡 6228***4821').fontSize(12).fontColor(C.textDim)
    }
    .alignItems(HorizontalAlign.Start).layoutWeight(1)

    Text('切换 >').fontSize(12).fontColor(C.primary)
      .onClick(() => { promptAction.showToast({ message: '切换房间' }); })
  }
  .width('100%')
  .padding(14)
  .backgroundColor(C.card)
  .borderRadius(D.rLg)
  .border({ width: 1, color: C.stroke })
}

卡片左侧 🏠 Emoji(56vp 圆角方块浅青底),中间纵排两行信息(地址 / 绑定人+脱敏卡号),右侧"切换 >"主色链接点击 toast,预留多房间切换入口。

缴费页顶部

四、TypeSelector 缴费类型选择器

4.1 类型定义与状态

// 类型选项定义
private types: string[] = ['电费', '水费', '燃气费'];
@State typeIdx: number = 0;

类型用纯字符串数组承载,三种缴费类型覆盖校园生活主要场景。typeIdx 记录当前选中索引,默认 0(电费)。

4.2 三选一渲染

// TypeSelector — 类型选择器
@Builder
TypeSelector() {
  Column({ space: 12 }) {
    Text('缴费类型').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%')
    Row({ space: 10 }) {
      ForEach(this.types, (t: string, idx: number) => {
        Column({ space: 4 }) {
          Text(idx === 0 ? '⚡' : idx === 1 ? '💧' : '🔥').fontSize(24)
          Text(t).fontSize(13).fontColor(this.typeIdx === idx ? '#FFFFFF' : C.textSub)
        }
        .layoutWeight(1)
        .padding({ top: 14, bottom: 14 })
        .backgroundColor(this.typeIdx === idx ? C.primary : C.card)
        .borderRadius(D.rMd)
        .border({ width: 1, color: this.typeIdx === idx ? C.primary : C.stroke })
        .onClick(() => { this.typeIdx = idx; })
      }, (t: string) => t)
    }
    .width('100%')
  }
  .width('100%')
  .padding(14)
  .backgroundColor(C.card)
  .borderRadius(D.rLg)
  .border({ width: 1, color: C.stroke })
}

图标由索引三元表达式推导(0→⚡ / 1→💧 / 其余→🔥),无需扩展数据结构。选中态:实底主色背景 + 白字 + 主色描边;未选中:白底 + 次要色字 + 灰描边。三项 layoutWeight(1) 等分宽度,非固定尺寸。

缴费页中部

五、AmountSelector 金额选择器

5.1 预设金额

// 金额预设值
private amounts: string[] = ['¥20', '¥50', '¥100', '¥200'];
@State amountIdx: number = 1;

四种预设金额满足常见场景,amountIdx 默认 1(¥50)。

5.2 Flex 换行渲染

// AmountSelector — 金额选择器
@Builder
AmountSelector() {
  Column({ space: 12 }) {
    Text('选择金额').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%')
    Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
      ForEach(this.amounts, (a: string, idx: number) => {
        Text(a)
          .fontSize(15).fontWeight(FontWeight.Medium)
          .fontColor(this.amountIdx === idx ? '#FFFFFF' : C.text)
          .padding({ left: 20, right: 20, top: 12, bottom: 12 })
          .backgroundColor(this.amountIdx === idx ? C.primary : C.cardSoft)
          .borderRadius(D.rSm)
          .border({ width: 1, color: this.amountIdx === idx ? C.primary : C.stroke })
          .margin({ right: 10, bottom: 10 })
          .onClick(() => { this.amountIdx = idx; })
      }, (a: string) => a)
    }
    .width('100%')
  }
  .width('100%')
  .padding(14)
  .backgroundColor(C.card)
  .borderRadius(D.rLg)
  .border({ width: 1, color: C.stroke })
}

Flex 布局自动换行,比固定 Grid 更灵活。选中态与 TypeSelector 同一套视觉语言:主色实底+白字+主色描边;未选中浅灰底(C.cardSoft)+灰描边。

5.3 CustomInput 自定义金额

// CustomInput — 自定义金额输入
@Builder
CustomInput() {
  Column({ space: 8 }) {
    Text('自定义金额').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%')
    Row({ space: 10 }) {
      Text('¥').fontSize(18).fontColor(C.textDim)
      TextInput({ placeholder: '请输入金额', text: this.customAmount })
        .backgroundColor('transparent').borderRadius(D.rSm).height(44)
        .layoutWeight(1).type(InputType.Number)
        .placeholderColor(C.textDim).placeholderFont({ size: 14 })
        .onChange((val: string) => { this.customAmount = val; })
    }
    .width('100%')
    .padding({ left: 14, right: 14 })
    .backgroundColor(C.cardSoft).borderRadius(D.rSm)
  }
  .width('100%')
  .padding(14)
  .backgroundColor(C.card)
  .borderRadius(D.rLg)
  .border({ width: 1, color: C.stroke })
}

自定义输入是独立卡片¥ 前缀 + 数字键盘(type(InputType.Number))+ 浅灰底输入槽。注意预设与自定义互不干扰——金额 onClick 只更新 amountIdx,输入框 onChange 只更新 customAmount,两者各自独立;最终取值在 PreviewCard 中按"自定义优先"裁决。

缴费页下部

六、AutoPaySwitch 自动代扣开关

// AutoPaySwitch — 代扣开关
@Builder
AutoPaySwitch() {
  Row({ space: 12 }) {
    Column({ space: 4 }) {
      Text('自动代扣').fontSize(14).fontWeight(FontWeight.Medium).fontColor(C.text)
      Text('余额低于 ¥20 时自动充值').fontSize(11).fontColor(C.textDim)
    }
    .alignItems(HorizontalAlign.Start).layoutWeight(1)

    Toggle({ type: ToggleType.Switch, isOn: this.autoPay })
      .selectedColor(C.primary)
      .onChange((on: boolean) => {
        this.autoPay = on;
        promptAction.showToast({ message: on ? '已开启自动代扣' : '已关闭自动代扣' });
      })
  }
  .width('100%')
  .padding(14)
  .backgroundColor(C.card)
  .borderRadius(D.rLg)
  .border({ width: 1, color: C.stroke })
}

Toggle 用系统 Switch 控件,selectedColor(C.primary) 定制开启色,.onChange() 回调更新 @State autoPay 并 toast 反馈开关状态。左侧文案 layoutWeight(1) 占满剩余宽度,把 Toggle 推到最右端。

七、PreviewCard 缴费预览

// PreviewCard — 缴费预览
@Builder
PreviewCard() {
  Column({ space: 12 }) {
    Text('缴费预览').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%')
    ForEach(this.previewItems, (p: PreviewItem) => {
      Row() {
        Text(p.label).fontSize(13).fontColor(C.textDim)
        Blank()
        Text(p.value).fontSize(13).fontColor(C.text).fontWeight(FontWeight.Medium)
      }
      .width('100%')
    }, (p: PreviewItem) => p.label)
    Divider().color(C.stroke).strokeWidth(0.5)
    Row() {
      Text('实付金额').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text)
      Blank()
      Text(this.customAmount.length > 0 ? '¥' + this.customAmount : this.amounts[this.amountIdx])
        .fontSize(22).fontColor(C.danger).fontWeight(FontWeight.Bold)
    }
    .width('100%')
  }
  .width('100%')
  .padding(14)
  .backgroundColor(C.card)
  .borderRadius(D.rLg)
  .border({ width: 1, color: C.stroke })
}

五行预览(缴费类型/缴费金额/房屋信息/支付方式/到账时间)用 ForEach + Blank 两端对齐;Divider 分隔后是实付金额裁决逻辑customAmount 非空优先,否则回落 amounts[amountIdx]——这就是预设与自定义"互不干扰、显示时择一"的实现方式。

八、TemplateSection 快捷模板

// TemplateSection — 快捷模板
@Builder
TemplateSection() {
  Column({ space: 12 }) {
    Text('快捷模板').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%')
    ForEach(this.templates, (t: TemplateItem) => {
      Row({ space: 12 }) {
        Row() {
          Text(t.emoji).fontSize(22)
        }
        .width(42).height(42)
        .backgroundColor(C.primarySoft).borderRadius(D.rSm)
        .justifyContent(FlexAlign.Center)

        Column({ space: 3 }) {
          Text(t.name).fontSize(14).fontColor(C.text).fontWeight(FontWeight.Medium)
          Text(t.desc).fontSize(11).fontColor(C.textDim)
        }
        .alignItems(HorizontalAlign.Start).layoutWeight(1)

        Text(t.amount).fontSize(15).fontColor(C.primary).fontWeight(FontWeight.Bold)
      }
      .width('100%')
      .padding({ left: 12, right: 12, top: 10, bottom: 10 })
      .backgroundColor(C.cardSoft).borderRadius(D.rSm)
      .onClick(() => {
        promptAction.showToast({ message: '使用模板: ' + t.name });
      })
    }, (t: TemplateItem) => t.id.toString())
  }
  .width('100%')
  .padding(14)
  .backgroundColor(C.card)
  .borderRadius(D.rLg)
  .border({ width: 1, color: C.stroke })
}

四个模板(电费快捷充值 ¥50 / 水费月度缴纳 ¥30 / 电费大额充值 ¥200 / 水费季度预缴 ¥100)浅灰底行卡,点击 toast"使用模板: xxx"。

九、SubmitBtn 提交按钮

// SubmitBtn — 提交按钮
@Builder
SubmitBtn() {
  Button('确认缴费')
    .width('100%').height(50)
    .fontSize(16).fontColor('#FFFFFF').backgroundColor(C.primary)
    .borderRadius(D.rMd)
    .onClick(() => { promptAction.showToast({ message: '缴费成功!' }); })
}

提交按钮走极简路线:主色纯色实底 + 白字,宽 100%、高 50、D.rMd 圆角,点击 toast"缴费成功!"。不做禁用态判断——表单始终有默认值(类型默认电费、金额默认 ¥50),天然可提交。

缴费页底部

十、本章小结

组件核心技术设计亮点
RoomCardRow 三段式布局脱敏卡号 + 切换链接
TypeSelector字符串数组 + 索引三元图标选中态实底主色+白字
AmountSelectorFlex Wrap 换行预设/自定义互不干扰
AutoPaySwitchToggle SwitchselectedColor + toast 反馈
PreviewCardBlank 两端对齐 + 条件取值自定义金额优先裁决
SubmitBtn纯色实底按钮默认值兜底免校验

这套表单模式可直接复用于任何支付/充值场景——话费充值、会员购买、打赏等。

Logo

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

更多推荐