当前位置: 首页 > news >正文

HarmonyOS NEXT 企业级记账APP:预算中心开发(ProgressBar + CustomDialog + ArkTS 编译陷阱实战)

预算中心开发(ProgressBar + CustomDialog + ArkTS 编译陷阱实战)

本文是《HarmonyOS NEXT 企业级开发实战:30篇打造智能记账APP》系列的第16篇,对应 Git Tagv0.1.6。承接第 15 篇的搜索筛选,本篇开发 BudgetView 预算中心,支持月度预算设置、已用进度、剩余金额、超支提醒。同时深入讲解 ArkTS 中@Prop不支持函数类型的arkts-no-props-function错误,以及属性名与基类方法冲突的编译问题。

前言

预算中心是记账应用的财务管家——用户设置月度预算后,应用自动统计本月支出,实时展示已用/剩余/进度条,超支时红警提醒。本章封装 BudgetCard、ProgressBar 两个核心组件,集入 BudgetRepository 完成数据闭环,并通过@CustomDialog实现预算设置对话框。

本文将带你:

  1. 开发 BudgetView 完整页面(预算卡 + 进度条 + 超支提醒 + 收支概览)
  2. 封装 BudgetCard 展示预算详情(onTap回调替代@BuilderParam
  3. 封装 ProgressBar 进度条组件(barHeight替代height避免基类冲突)
  4. 封装 InputDialog@CustomDialog自定义对话框替代@Builder方案
  5. 深入理解 ArkTS 中arkts-no-props-function与属性名冲突的编译陷阱

企业级核心原则:预算必须实时同步、视觉清晰、超支预警。参考 ArkUI ProgressBar 了解官方约定。


一、Budget 数据模型

1.1 完整源码

Budget模型封装了月度预算的核心数据与计算逻辑:

// model/Budget.ets export class Budget { id: string = ''; month: string = ''; budget: number = 0; used: number = 0; remain: number = 0; constructor(id?: string, month?: string, budget?: number) { if (id !== undefined) this.id = id; if (month !== undefined) this.month = month; if (budget !== undefined) this.budget = budget; } refresh(used: number): void { this.used = used; this.remain = this.budget - this.used; } isOverrun(): boolean { return this.used > this.budget; } usage(): number { if (this.budget === 0) return 0; return Math.min(this.used / this.budget, 1); } static fromObject(data: ESObject): Budget { const b = new Budget(); if (data === null || data === undefined) { return b; } if (data.id !== undefined) b.id = String(data.id); if (data.month !== undefined) b.month = String(data.month); if (data.budget !== undefined) b.budget = Number(data.budget); if (data.used !== undefined) b.used = Number(data.used); if (data.remain !== undefined) b.remain = Number(data.remain); return b; } }

1.2 核心方法说明

方法返回类型说明
refresh(used)void刷新已用金额并计算剩余
isOverrun()boolean是否超支(used > budget)
usage()number使用率 0~1(上限 1,超支不超 100%)
fromObject(data)Budget从 ESObject 反序列化(JSON 恢复)

设计要点usage()返回Math.min(used / budget, 1),超支时进度条封顶 100% 而非溢出,保证 UI 不会变形。


二、BudgetViewModel 业务层

2.1 完整源码

BudgetViewModel负责加载月度预算、统计收支、设置预算:

// viewmodel/BudgetViewModel.ets import { BillRepository } from '../repository/BillRepository'; import { BudgetRepository } from '../repository/BudgetRepository'; import { BillType } from '../constants/BillType'; import { Budget } from '../model/Budget'; import { DateUtil } from '../utils/DateUtil'; import { MoneyUtil } from '../utils/MoneyUtil'; import { ToastUtil } from '../utils/ToastUtil'; export class BudgetViewModel { currentMonth: string = ''; budget: Budget | null = null; totalExpense: number = 0; totalIncome: number = 0; progress: number = 0; isOverrun: boolean = false; isLoading: boolean = true; private budgetRepo = BudgetRepository.getInstance(); private billRepo = BillRepository.getInstance(); async loadData(): Promise<void> { this.isLoading = true; this.currentMonth = DateUtil.formatDate(Date.now()).substring(0, 7); this.budget = await this.budgetRepo.findCurrentMonth(); const start = DateUtil.monthStart(Date.now()); const end = DateUtil.monthEnd(Date.now()); const bills = await this.billRepo.findByDateRange(start, end); this.totalExpense = bills.filter(b => b.type === BillType.EXPENSE).reduce((s, b) => s + b.money, 0); this.totalIncome = bills.filter(b => b.type === BillType.INCOME).reduce((s, b) => s + b.money, 0); if (this.budget) { this.budget.refresh(this.totalExpense); this.progress = this.budget.usage(); this.isOverrun = this.budget.isOverrun(); } this.isLoading = false; } async setBudget(amountCents: number): Promise<void> { if (amountCents <= 0) { ToastUtil.show('预算必须大于 0'); return; } this.budget = await this.budgetRepo.setMonthBudget(this.currentMonth, amountCents); await this.loadData(); ToastUtil.show('预算已设置'); } }

2.2 字段说明

字段类型用途
currentMonthstring当前月份(如2026-07
budgetBudget | null本月预算实体(未设置时为 null)
totalExpensenumber本月支出(分)
totalIncomenumber本月收入(分)
progressnumber使用率 0~1
isOverrunboolean是否超支
isLoadingboolean加载态

2.3 月度统计流程

BudgetViewModel.loadData() ↓ 获取当前月份 → DateUtil.formatDate(Date.now()).substring(0, 7) ↓ 查询本月预算 → budgetRepo.findCurrentMonth() ↓ 查询本月账单 → billRepo.findByDateRange(monthStart, monthEnd) ↓ 统计支出/收入 → filter + reduce ↓ 刷新预算 → budget.refresh(totalExpense) ↓ 计算进度与超支 → usage() / isOverrun()

金额单位:所有金额以为单位存储(整数),通过MoneyUtil.format()转换为元显示,避免浮点精度问题。


三、ProgressBar 通用组件

3.1 完整源码

ProgressBar使用 Stack 叠放轨道与进度条,支持自定义颜色和高度:

// components/chart/ProgressBar.ets import { AppColors } from '../../theme/Colors'; import { AppSpace } from '../../theme/Spacing'; @Component export struct ProgressBar { @Prop progress: number = 0; @Prop color: string = AppColors.Budget; @Prop trackColor: string = AppColors.Background; @Prop barHeight: number = 8; build() { Stack() { Column().width('100%').height(this.barHeight).backgroundColor(this.trackColor).borderRadius(this.barHeight / 2) Column().width(`${Math.min(this.progress, 1) * 100}%`).height(this.barHeight) .backgroundColor(this.color).borderRadius(this.barHeight / 2) .animation({ duration: 300, curve: Curve.EaseInOut }) } .width('100%').height(this.barHeight) } }

3.2 设计要点

  • Stack叠放轨道(底层)与进度条(上层),实现进度覆盖效果
  • 进度条宽度用百分比${Math.min(this.progress, 1) * 100}%,超支时封顶 100%
  • 添加300ms EaseInOut 动画,进度变化平滑过渡
  • barHeight同时控制轨道、进度条和容器高度,保持视觉一致
  • borderRadius设为barHeight / 2,自动适配圆角

注意:属性名使用barHeight而非height,原因详见第四章的编译陷阱分析。


四、ArkTS 编译陷阱:属性名冲突与函数类型

4.1 height 属性名冲突

最初 ProgressBar 组件使用@Prop height: number = 8作为属性名,但编译时报错:

// ❌ 错误写法:height 与基类方法冲突 @Component export struct ProgressBar { @Prop progress: number = 0; @Prop color: string = AppColors.Budget; @Prop trackColor: string = AppColors.Background; @Prop height: number = 8; // 编译报错! build() { Stack() { Column().width('100%').height(this.height)... } .width('100%').height(this.height) } }

编译器报错信息:

Property 'height' in type 'ProgressBar' is not assignable to the same property in base type 'CustomComponent'. Type 'number' is not assignable to type '((value: Length) => void) | undefined'.

根因分析:ArkTS 中所有@Component结构体都隐式继承自CustomComponent基类。CustomComponent已经定义了height方法(用于设置组件高度的链式调用)。当你在子组件中用@Prop height: number声明同名属性时,类型number与基类方法((value: Length) => void) | undefined不兼容,导致编译失败。

4.2 @Prop 不支持函数类型

最初 BudgetCard 组件使用@BuilderParam onClick接收点击回调,但尝试用@Prop传递函数时也遇到了编译错误:

// ❌ 错误写法:@Prop 不能用于函数类型 @Component export struct BudgetCard { @Prop budget: Budget = new Budget('', '', 0); @Prop totalExpense: number = 0; @Prop onClick: () => void = () => {}; // 编译报错!arkts-no-props-function }

编译器报错信息:

arkts-no-props-function

根因分析:ArkTS 的@Prop装饰器仅支持值类型string/number/boolean/ 对象等),不支持函数类型。这是因为@Prop会在父子组件之间进行深拷贝,而函数无法被序列化拷贝。ArkTS 编译器通过arkts-no-props-function规则直接拦截这种用法。

4.3 解决方案对比

问题错误写法正确写法原因
height 冲突@Prop height: number@Prop barHeight: number避免与CustomComponent.height()方法同名
函数回调@Prop onClick: () => voidonTap: () => void = () => {}普通属性可接收函数,@Prop不行
文本对齐TextAlign.RightTextAlign.EndRight已废弃,用End替代

4.4 TextAlign.Right 弃用问题

// ❌ 旧写法:TextAlign.Right 已废弃 Text(MoneyUtil.formatWithComma(this.budget.budget)) .textAlign(TextAlign.Right) // ✅ 新写法:使用 TextAlign.End Text(MoneyUtil.formatWithComma(this.budget.budget)) .textAlign(TextAlign.End)

原因:鸿蒙 ArkUI 中TextAlign.Right在某些版本中被标记为废弃,推荐使用TextAlign.End。两者效果相同(右对齐),但End在 RTL(从右到左)布局中能正确适配,是更规范的写法。

总结:ArkTS 编译器比 TypeScript 更严格,属性名不能与基类方法冲突,@Prop不能用于函数类型。遇到编译错误时,重命名属性改用普通属性是最常见的解决方案。


五、BudgetCard 通用组件

5.1 完整源码

BudgetCard展示预算金额、进度条、已用/剩余金额,超支时进度条变红:

// components/card/BudgetCard.ets import { Budget } from '../../model/Budget'; import { ProgressBar } from '../chart/ProgressBar'; import { AppColors } from '../../theme/Colors'; import { AppFontSize, AppFontWeight } from '../../theme/Typography'; import { AppSpace } from '../../theme/Spacing'; import { MoneyUtil } from '../../utils/MoneyUtil'; @Component export struct BudgetCard { @Prop budget: Budget = new Budget('', '', 0); @Prop totalExpense: number = 0; onTap: () => void = () => {}; build() { Column() { Row() { Text('本月预算').fontSize(AppFontSize.SM).fontColor(AppColors.SecondaryText) Text(MoneyUtil.formatWithComma(this.budget.budget)) .fontSize(AppFontSize.XL).fontColor(AppColors.PrimaryText).fontWeight(AppFontWeight.Bold) .layoutWeight(1).textAlign(TextAlign.End) }.width('100%') ProgressBar({ progress: this.budget.usage(), color: this.budget.isOverrun() ? AppColors.Expense : AppColors.Budget, barHeight: 12 }).margin({ top: AppSpace.MD }) Row() { Column() { Text('已用').fontSize(AppFontSize.XS).fontColor(AppColors.SecondaryText) Text(`¥${MoneyUtil.format(this.totalExpense)}`) .fontSize(AppFontSize.MD).fontColor(this.budget.isOverrun() ? AppColors.Expense : AppColors.PrimaryText) }.alignItems(HorizontalAlign.Start) Column() { Text('剩余').fontSize(AppFontSize.XS).fontColor(AppColors.SecondaryText) Text(`¥${MoneyUtil.format(Math.max(this.budget.remain, 0))}`) .fontSize(AppFontSize.MD).fontColor(AppColors.Budget) }.alignItems(HorizontalAlign.Start).margin({ left: AppSpace.LG }) }.width('100%').margin({ top: AppSpace.SM }).justifyContent(FlexAlign.Start) } .width('100%').padding(AppSpace.CardPadding) .backgroundColor(AppColors.CardBackground).borderRadius(AppSpace.CardRadius) .onClick(() => { this.onTap(); }) } }

5.2 设计要点

  • onTap: () => void = () => {}作为普通属性接收点击回调,默认空函数避免未传参时报错
  • 进度条颜色根据isOverrun()动态切换:正常蓝色、超支红色
  • 剩余金额用Math.max(this.budget.remain, 0)确保不显示负数
  • 已用金额在超支时也变红,形成视觉一致性
  • textAlign(TextAlign.End)右对齐预算金额,与左侧标签形成对比
  • 整个卡片可点击(onClick),触发onTap回调打开设置对话框

5.3 超支视觉策略

状态进度条颜色已用金额颜色说明
正常蓝色#007AFF深色#1C1C1E预算充足
超支红色#FF3B30红色#FF3B30进度封顶 100%,红色告警

视觉一致性:超支时进度条和已用金额同时变红,让用户一眼感知到预算超支状态,无需阅读具体数字。


六、InputDialog 自定义对话框

6.1 完整源码

InputDialog使用@CustomDialog装饰器,封装通用的文本输入对话框:

// components/dialog/InputDialog.ets import { AppColors } from '../../theme/Colors'; import { AppFontSize } from '../../theme/Typography'; import { AppSpace } from '../../theme/Spacing'; @CustomDialog export struct InputDialog { controller: CustomDialogController; title: string = ''; placeholder: string = ''; confirmText: string = '确认'; cancelText: string = '取消'; onConfirm: (value: string) => void = () => {}; onCancel: () => void = () => {}; @State inputValue: string = ''; build() { Column() { Text(this.title) .fontSize(AppFontSize.LG) .fontColor(AppColors.PrimaryText) .fontWeight(FontWeight.Bold) .margin({ bottom: AppSpace.MD }) TextInput({ placeholder: this.placeholder, text: $$this.inputValue }) .width('100%') .height(44) .backgroundColor(AppColors.Background) .borderRadius(AppSpace.SM) Row() { Button(this.cancelText) .backgroundColor(AppColors.Background) .fontColor(AppColors.SecondaryText) .onClick(() => { this.controller.close(); this.onCancel(); }) Button(this.confirmText) .backgroundColor(AppColors.Budget) .fontColor('#FFFFFF') .onClick(() => { this.controller.close(); this.onConfirm(this.inputValue); }) } .width('100%') .margin({ top: AppSpace.LG }) .justifyContent(FlexAlign.SpaceBetween) } .width('100%') .padding(AppSpace.LG) .backgroundColor(AppColors.CardBackground) .borderRadius(AppSpace.CardRadius) } }

6.2 @CustomDialog 机制说明

@CustomDialog vs @Builder 对比:旧版预算设置使用@Builder buildSetDialog()在页面内构建弹窗,代码耦合度高、复用性差。改用@CustomDialog后,对话框成为独立组件,可被任意页面复用,通过CustomDialogController统一管理生命周期。

6.3 对话框属性说明

属性类型说明
controllerCustomDialogController框架自动注入,用于关闭对话框
titlestring对话框标题
placeholderstring输入框占位文本
onConfirm(value: string) => void确认回调,接收输入值
onCancel() => void取消回调
inputValuestring(@State)双向绑定的输入值

关键技术TextInput使用$$this.inputValue双向绑定语法($$),用户输入时自动更新inputValue状态,点击确认时通过onConfirm(this.inputValue)传出。


七、BudgetView 页面实现

7.1 完整源码

BudgetView是预算中心的主页面,展示预算卡片、收支概览、超支提醒,并通过CustomDialogController调用InputDialog

// components/tabs/BudgetView.ets import { BudgetCard } from '../card/BudgetCard'; import { BudgetViewModel } from '../../viewmodel/BudgetViewModel'; import { AppColors } from '../../theme/Colors'; import { AppFontSize } from '../../theme/Typography'; import { AppSpace } from '../../theme/Spacing'; import { MoneyUtil } from '../../utils/MoneyUtil'; import { RouterUtil } from '../../utils/RouterUtil'; import { InputDialog } from '../dialog/InputDialog'; @Component struct BudgetView { @State viewModel: BudgetViewModel = new BudgetViewModel(); @State isLoading: boolean = true; aboutToAppear(): void { this.loadData(); } private async loadData(): Promise<void> { this.isLoading = true; await this.viewModel.loadData(); this.isLoading = false; } build() { Scroll() { Column() { Text('预算中心') .fontSize(20).fontWeight(FontWeight.Bold) .margin({ top: AppSpace.XL, bottom: AppSpace.MD }) .alignSelf(ItemAlign.Start) if (!this.isLoading) { if (this.viewModel.budget) { BudgetCard({ budget: this.viewModel.budget, totalExpense: this.viewModel.totalExpense, onTap: () => { this.showBudgetInput(); } }) // 本月收支统计 Row() { Column() { Text('本月支出').fontSize(AppFontSize.SM).fontColor(AppColors.SecondaryText) Text(`¥${MoneyUtil.format(this.viewModel.totalExpense)}`) .fontSize(AppFontSize.XL).fontColor(AppColors.Expense).fontWeight(FontWeight.Bold) .margin({ top: AppSpace.XS }) }.layoutWeight(1).alignItems(HorizontalAlign.Start) Column() { Text('本月收入').fontSize(AppFontSize.SM).fontColor(AppColors.SecondaryText) Text(`¥${MoneyUtil.format(this.viewModel.totalIncome)}`) .fontSize(AppFontSize.XL).fontColor(AppColors.Income).fontWeight(FontWeight.Bold) .margin({ top: AppSpace.XS }) }.layoutWeight(1).alignItems(HorizontalAlign.Start) } .width('100%') .padding(AppSpace.CardPadding) .backgroundColor(AppColors.CardBackground) .borderRadius(AppSpace.CardRadius) .margin({ top: AppSpace.MD }) if (this.viewModel.isOverrun) { Row() { Image($r('app.media.icon_expense')).width(20).height(20).fillColor(AppColors.Expense) Text('预算已超支,请注意控制消费') .fontSize(AppFontSize.SM).fontColor(AppColors.Expense) .margin({ left: AppSpace.SM }) } .width('100%') .padding(AppSpace.CardPadding) .backgroundColor(AppColors.ExpenseLight) .borderRadius(AppSpace.CardRadius) .margin({ top: AppSpace.MD }) } } else { // 未设置预算 Column() { Image($r('app.media.icon_empty')).width(80).height(80).fillColor(AppColors.SecondaryText) Text('尚未设置本月预算').fontSize(16).fontColor(AppColors.PrimaryText).margin({ top: AppSpace.MD }) Text('设置预算,合理规划每一笔支出').fontSize(12).fontColor(AppColors.SecondaryText).margin({ top: AppSpace.XS }) Button('设置预算') .margin({ top: AppSpace.LG }) .backgroundColor(AppColors.Budget) .onClick(() => { this.showBudgetInput(); }) } .width('100%') .padding({ top: 80, bottom: 80 }) } } } .width('100%') .padding({ left: AppSpace.XL, right: AppSpace.XL }) } .width('100%').height('100%') .scrollBar(BarState.Off) .backgroundColor(AppColors.Background) } private showBudgetInput(): void { let dialogController = new CustomDialogController({ builder: InputDialog({ title: '设置本月预算', placeholder: '请输入预算金额(元)', onConfirm: (value: string) => { const amount = parseFloat(value); if (isNaN(amount) || amount <= 0) { return; } const cents = Math.round(amount * 100); this.viewModel.setBudget(cents).then(() => { this.loadData(); }); }, onCancel: () => {} }), autoCancel: true, alignment: DialogAlignment.Center }); dialogController.open(); } } export default BudgetView;

7.2 CustomDialogController 调用解析

showBudgetInput()方法是整个对话框交互的核心:

private showBudgetInput(): void { let dialogController = new CustomDialogController({ builder: InputDialog({ title: '设置本月预算', placeholder: '请输入预算金额(元)', onConfirm: (value: string) => { const amount = parseFloat(value); if (isNaN(amount) || amount <= 0) { return; } const cents = Math.round(amount * 100); this.viewModel.setBudget(cents).then(() => { this.loadData(); }); }, onCancel: () => {} }), autoCancel: true, alignment: DialogAlignment.Center }); dialogController.open(); }

7.3 页面状态管理

状态类型触发时机UI 变化
isLoadingbooleanaboutToAppear/loadData加载中不渲染内容
viewModel.budgetBudget | nullloadData完成null 显示引导,非 null 显示卡片
viewModel.isOverrunbooleanloadData完成true 显示超支红色提醒条

关键改进:旧版使用@State showSetDialog: boolean+@Builder buildSetDialog()手动管理弹窗显隐,需要自己实现遮罩层和关闭逻辑。新版使用CustomDialogController,框架自动管理遮罩、动画和关闭,代码更简洁可靠。


八、最佳实践

8.1 进度条超支红色

// 超支时进度条 100% + 红色,视觉强警 progress = Math.min(this.budget.usage(), 1); // 上限 1 color = this.budget.isOverrun() ? AppColors.Expense : AppColors.Budget;

8.2 预算与账单联动

// 每次新增/删除账单后,预算页需重新统计 // 解决:预算页 aboutToAppear 重新 loadData aboutToAppear(): void { this.loadData(); }

8.3 颜色策略

使用率颜色语义色值
0%-60%蓝色正常#007AFF
60%-80%蓝色注意#007AFF
80%-100%蓝色警戒#007AFF
>100%红色超支#FF3B30

当前实现中,进度条颜色仅在超支时切换为红色,正常状态下统一使用蓝色。未来可扩展为多级颜色策略(60% 黄色、80% 橙色)。

8.4 预算数据可视化

预算数据可以通过图表组件直观展示:

图表类型展示内容交互方式
饼图各类支出占比点击查看详情
柱状图每日支出 vs 日均预算悬停显示数值
折线图月度支出趋势拖动查看历史

可视化展示让预算数据更加直观,帮助用户快速了解财务状况。详见后续 Canvas 绘图系列文章。


九、运行验证

9.1 编译检查

hvigorw assembleHap--modemodule-pproduct=default

9.2 功能验证

验证项预期
首次进入预算中心显示空状态引导,含"设置预算"按钮
设置 5000 元预算进度条展示已用占比,收支概览显示
超支后进度条红色 + 超支提醒条
修改预算点击卡片弹出 InputDialog,输入新值保存
账单变动后返回aboutToAppear 重新 loadData,数据刷新

9.3 预算设置流程

  1. 点击"设置预算"按钮或点击已有预算卡片
  2. showBudgetInput()创建CustomDialogController并打开InputDialog
  3. 在输入框中输入金额(元)
  4. 点击"确认",onConfirm回调将元转为分(Math.round(amount * 100)
  5. 调用viewModel.setBudget(cents)保存到 Repository
  6. 保存成功后调用loadData()刷新页面

9.4 预算修改流程

  1. 点击已有预算卡片,触发onTap回调
  2. 弹出InputDialog,输入新金额
  3. 点击"确认",setBudget会覆盖当前月份预算
  4. BudgetRepository.setMonthBudget查找已有记录并更新

十、常见问题

10.1 CustomDialog 不弹出

// 错误:CustomDialogController 未调用 open() let controller = new CustomDialogController({ builder: InputDialog({...}) }); // 忘记调用 open() // 解决:创建后必须调用 open() controller.open();

10.2 属性名 height 编译报错

// 错误:@Prop height 与 CustomComponent 基类方法冲突 @Prop height: number = 8; // 解决:改用 barHeight @Prop barHeight: number = 8;

10.3 @Prop 函数类型报错

// 错误:arkts-no-props-function @Prop onClick: () => void = () => {}; // 解决:改用普通属性(不加 @Prop) onTap: () => void = () => {};

十一、Git 提交

11.1 Commit Message

gitadd.gitcommit-m"feat(budget): 开发预算中心与 BudgetCard - 新增 BudgetView 完整页面含进度条+超支提醒+收支概览 - 封装 BudgetCard 通用预算卡组件(onTap 回调) - 封装 ProgressBar 通用进度条组件(barHeight 避免基类冲突) - 封装 InputDialog @CustomDialog 自定义输入对话框 - 新增 BudgetViewModel 处理月度统计 - 集入 BudgetRepository 与 BillRepository - 修复 arkts-no-props-function 与 height 属性名冲突"

11.2 CHANGELOG

## [v0.1.6] - 2026-07-27 ### Added - components/tabs/BudgetView.ets:预算中心页面 - components/card/BudgetCard.ets:预算卡片组件 - components/chart/ProgressBar.ets:进度条组件 - components/dialog/InputDialog.ets:自定义输入对话框 - viewmodel/BudgetViewModel.ets:预算业务逻辑 - model/Budget.ets:预算数据模型 ### Fixed - ProgressBar: height → barHeight(避免 CustomComponent 基类冲突) - BudgetCard: @BuilderParam onClick → onTap 普通属性(arkts-no-props-function) - TextAlign.Right → TextAlign.End(使用规范 API)

附录:运行效果截图

总结

本文完整介绍了预算中心开发,涵盖 Budget 数据模型、BudgetViewModel、ProgressBar、BudgetCard、InputDialog@CustomDialog、BudgetView 完整页面。通过本篇你可以:

  • 封装 ProgressBar 进度条组件,使用barHeight避免与基类height方法冲突
  • 封装 BudgetCard 预算卡组件,使用普通属性onTap替代@Prop函数类型
  • 封装 InputDialog@CustomDialog自定义对话框,替代@Builder手动弹窗方案
  • 通过CustomDialogController统一管理对话框生命周期
  • 理解 ArkTS 中arkts-no-props-function和属性名冲突的编译陷阱及解决方案
  • 联动 BillRepository 统计月度支出,处理超支视觉预警

下一篇预告:[《预算提醒与预算进度》] 将开发预算阈值提醒(80% 黄警、100% 红警、超支弹通知),封装 NotificationUtil 推送本地通知。


如果这篇文章对你有帮助,欢迎点赞收藏关注,你的支持是我持续创作的动力!在评论区告诉我你在 ArkTS 组件开发中还遇到过哪些编译陷阱,我会针对性地分享更多实战经验。


相关资源

  • 本篇源码:GitHub Tag v0.1.6
  • ArkUI ProgressBar:progress
  • ArkUI CustomDialog:custom-dialog
  • ArkUI Dialog:dialog
  • ArkUI @Prop 装饰器:prop-decorator
  • ArkUI Stack 布局:stack
  • ArkUI 动画:animation
  • ArkTS 编译规则:arkts-rules
  • 鸿蒙通知服务:notification-service
http://www.cnnetsun.cn/news/3789701.html

相关文章:

  • ESP32-S3-DEV-KIT-N8R8开发板:8MB PSRAM与AI加速在物联网与边缘计算中的应用
  • 从数学零基础到机器学习高手:25章完整学习路径终极指南
  • IGMP协议与组播网络技术详解
  • 2026国产智能数据平台TOP6深度测评报告
  • 左偏树与右偏树:原理、实现与应用
  • 【单片机课设毕设项目】物联网视角下基于 STM32 的智能晾衣架设计 基于多传感数据采集的家用晾晒控制系统(017201)
  • 【单片机课设毕设项目】基于嵌入式 STM32 的音乐流水灯喷泉控制系统 基于 HC08 蓝牙模块的音频联动喷泉硬件开发(017301)
  • Python自进化系统:动态优化算法与架构的工程实践
  • MCP 月下载 9700 万次只花 16 个月:一文讲透 MCP 和 Skill 的区别
  • AI数据批量处理性能瓶颈在哪?92%的团队都忽略了这3个隐性耗时环节(附压测对比数据)
  • 为什么92%的AI项目因决议追溯失效被叫停?——AI决议跟踪系统4层日志熔断机制深度拆解
  • 【纹样工程师认证考点】:AI连续图案生成必须掌握的3个数学本质(群论平移对称性/傅里叶周期重构/双线性插值边界修正),附MIT开源验证数据集
  • Wan2.2开源:消费级GPU运行720P视频生成,成本降低70%的AI视频革命
  • 终极RDP Wrapper修复指南:让Windows家庭版也能用远程桌面
  • C语言指针数组:原理、应用与优化技巧
  • 为什么选择可视化方法:5种矩阵分解的终极图解指南
  • 基于Python的B站视频下载技术方案:实现高效批量下载与画质选择
  • 5种矩阵分解图解:线性代数可视化学习的终极指南
  • 单片机毕设项目:基于嵌入式硬件的充电桩充电费用统计系统设计 基于 STM32 的带指示灯多路智能充电桩整机开发(017001)
  • 单片机毕设项目:基于单片机的温室消防环境一体化智能监测系统 基于 STC89C52 继电器联动环境智能控制系统设计(017501)
  • 如何构建高效AI工程团队:5大实战策略框架
  • 贾子新学术体系三大免疫法则与闭环运行机制研究
  • Chartify实战案例:如何用CSS图表打造令人惊艳的数据仪表盘
  • SSM框架企业人事管理系统开发实践与优化
  • GPT-SoVITS:5分钟打造专属AI语音,零基础也能玩转语音克隆
  • 3步掌握Mica For Everyone:从新手到专家的Windows 11美化指南
  • 地球村医疗器械注册交流群
  • ESP32-S3驱动1.69寸触摸屏:从SPI优化到LVGL移植的嵌入式GUI实战
  • 大规模安卓设备远程管理的架构设计与优化实践
  • 如何高效部署FunASR:实战技巧与性能优化指南