HarmonyOS 应用 · 班费管理收支页深度解析:四状态表单与 Flex 分类胶囊
HarmonyOS 应用 · 班费管理收支页深度解析:四状态表单与 Flex 分类胶囊
项目编号:47-class-fund 技术栈:HarmonyOS ArkTS · ArkUI 声明式 UI · Stage 模型 本篇页面:收支页
Func1Tab.ets(108 行) 系列导航:(一)首页 · (二)收支 · (三)成员 · (四)统计

一、四 @State 驱动的表单状态机
@State isIncome: boolean = false;
@State amount: string = '';
@State remark: string = '';
@State activeCat: number = 0;
private incomeCats: string[] = ['班费收缴', '活动结余', '其他收入'];
private expenseCats: string[] = ['活动采购', '物资费用', '团建费用', '奖品支出', '其他支出'];
private getCats(): string[] {
return this.isIncome ? this.incomeCats : this.expenseCats;
}
收支页有四个 @State:isIncome(支出/收入切换)、amount(金额输入)、remark(备注输入)、activeCat(分类选中索引)。getCats() 是派生方法——根据 isIncome 返回对应分类数组,支出 5 个、收入 3 个。isIncome 变化时 getCats() 返回不同数组,分类列表即时刷新。
二、页面骨架:TypeSwitch + AmountCard + CatCard + RemarkCard + Button
build() {
Column() {
this.Header()
Scroll() {
Column({ space: 16 }) {
this.TypeSwitch()
this.AmountCard()
this.CatCard()
this.RemarkCard()
Button('保存记录').fontSize(15).fontColor('#FFFFFF').backgroundColor(C.primary)
.width('100%').height(48).borderRadius(D.rMd)
.onClick(() => { promptAction.showToast({ message: this.amount.length > 0 ? '记录已保存' : '请输入金额' }); })
}
.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)
}
Header 在 Scroll 外固定,Scroll 内五块:TypeSwitch(类型切换)+ AmountCard(金额)+ CatCard(分类)+ RemarkCard(备注)+ Button(保存)。Button 直接内联在 build 中,未抽 Builder——因为它是单行组件,无需复用。保存按钮的 onClick 做了空值校验:amount.length > 0 才提示"记录已保存",否则提示"请输入金额"。
三、TypeSwitch:isIncome 驱动的红绿双选
@Builder
TypeSwitch() {
Row() {
Text('支出').fontSize(14)
.fontColor(!this.isIncome ? '#FFFFFF' : C.textSub)
.textAlign(TextAlign.Center).layoutWeight(1).padding({ top: 11, bottom: 11 })
.backgroundColor(!this.isIncome ? C.danger : Color.Transparent).borderRadius(D.rSm)
.onClick(() => { this.isIncome = false; this.activeCat = 0; })
Text('收入').fontSize(14)
.fontColor(this.isIncome ? '#FFFFFF' : C.textSub)
.textAlign(TextAlign.Center).layoutWeight(1).padding({ top: 11, bottom: 11 })
.backgroundColor(this.isIncome ? C.ok : Color.Transparent).borderRadius(D.rSm)
.onClick(() => { this.isIncome = true; this.activeCat = 0; })
}
.width('100%').padding(4).backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke })
}
isIncome 驱动红绿双选——支出用 C.danger 红色、收入用 C.ok 绿色:
// 支出选中:红底白字
.backgroundColor(!this.isIncome ? C.danger : Color.Transparent)
// 收入选中:绿底白字
.backgroundColor(this.isIncome ? C.ok : Color.Transparent)
点击支出设 isIncome = false + activeCat = 0,点击收入设 isIncome = true + activeCat = 0——切换类型时重置分类选中索引,避免越界(支出 5 项 → 收入 3 项,索引 4 会越界)。

四、AmountCard:¥ 前缀 + TextInput 数字输入
@Builder
AmountCard() {
Row({ space: 6 }) {
Text('¥').fontSize(28).fontWeight(FontWeight.Bold).fontColor(C.text)
TextInput({ placeholder: '0.00' }).type(InputType.Number)
.backgroundColor(Color.Transparent).borderRadius(0).height(56)
.fontSize(28).layoutWeight(1)
.onChange((v: string) => { this.amount = v; })
}
.width('100%').padding({ left: 16, right: 16 }).backgroundColor(C.card).borderRadius(D.rLg).border({ width: 1, color: C.stroke })
}
28 号 ¥ 前缀 + 28 号 TextInput,type(InputType.Number) 限制数字键盘——金额输入只允许数字。backgroundColor(Color.Transparent) 让输入框透明融入卡片。onChange 写入 this.amount 触发 @State 刷新。
五、CatCard:Flex wrap 自动换行分类胶囊
@Builder
CatCard() {
Column({ space: 10 }) {
Text('分类').fontSize(14).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.getCats(), (c: string, idx: number) => {
Text(c).fontSize(12)
.fontColor(this.activeCat === idx ? '#FFFFFF' : C.textSub)
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.backgroundColor(this.activeCat === idx ? C.primary : C.cardSoft).borderRadius(14)
.margin({ right: 10, bottom: 10 })
.onClick(() => { this.activeCat = idx; })
}, (c: string, idx: number) => c + idx)
}.width('100%')
}
.width('100%').padding(16).backgroundColor(C.card).borderRadius(D.rLg).border({ width: 1, color: C.stroke })
}
Flex({ wrap: FlexWrap.Wrap }) 是本页特色——自动换行的弹性布局。支出有 5 个分类,一行放不下,FlexWrap.Wrap 让胶囊自动换行。每个胶囊 borderRadius(14) 全圆角 + margin({ right: 10, bottom: 10 }) 间距。
activeCat 驱动选中态——字色与底色两路分支:
// 选中:绿底白字
.backgroundColor(this.activeCat === idx ? C.primary : C.cardSoft)
// 未选:浅灰底灰字
.fontColor(this.activeCat === idx ? '#FFFFFF' : C.textSub)
getCats() 根据 isIncome 返回不同数组——切换类型时分类列表整体替换。
六、RemarkCard:备注输入
@Builder
RemarkCard() {
Column({ space: 10 }) {
Text('备注').fontSize(14).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%')
TextInput({ placeholder: '添加备注说明' })
.backgroundColor(C.cardSoft).borderRadius(D.rSm).height(44)
.onChange((v: string) => { this.remark = v; })
}
.width('100%').padding(16).backgroundColor(C.card).borderRadius(D.rLg).border({ width: 1, color: C.stroke })
}
14 号标题 + TextInput 备注。与 AmountCard 不同,备注输入框用 C.cardSoft 浅灰底 + borderRadius(D.rSm) 小圆角——视觉权重低于金额输入,符合"金额是主信息、备注是辅助"的层级关系。


七、表单状态联动链路
四个 @State 形成联动链路:
TypeSwitch 点击 → isIncome 翻转 → getCats() 返回新数组 → CatCard 胶囊列表刷新
→ activeCat 重置为 0 → 选中态回到第一个
AmountCard 输入 → amount 更新 → 保存按钮校验 amount.length > 0
CatCard 点击 → activeCat 更新 → 选中胶囊高亮转移
RemarkCard 输入 → remark 更新 → 随记录保存
isIncome 是链路起点——它的变化同时影响 TypeSwitch 的红绿选中态、CatCard 的分类列表、activeCat 的重置,一次点击触发三处刷新。
八、可演进方向
| 方向 | 现状 | 改法 | 收益 |
|---|---|---|---|
| 保存写入 | Toast 模拟 | 写入 records 数组 + 余额重算 | 记录真实保存 |
| 金额校验 | 仅非空 | 正数 + 两位小数 + 上限 | 金额格式合法 |
| 分类管理 | 写死数组 | 服务端配置 + 自定义分类 | 分类可扩展 |
| 日期选择 | 无 | DatePicker 选日期 | 记录日期可调 |
| 表单重置 | 无 | 保存后清空 amount/remark/activeCat | 连续录入 |
收支页是班费管理的录入入口——四 @State 驱动的表单状态机、isIncome 红绿双选、Flex wrap 自动换行分类胶囊。从 TypeSwitch 的类型联动到保存按钮的空值校验,每一层都有明确的职责边界。

更多推荐



所有评论(0)