HarmonyOS7月度预算看板实战:预算进度计算与分类预警
2026/7/17 5:53:36 网站建设 项目流程

文章目录

      • 前言
      • 先看这个页面解决了什么问题
      • 页面结构速览
      • 关键状态是怎么组织的
      • 业务逻辑最值得看的函数
      • 第一段重点代码:状态与数据入口
      • 第二段重点代码:核心交互逻辑
      • 使用方法:把页面跑起来之后怎么操作
      • 这个实现为什么值得借鉴
      • 可以继续优化的地方
      • 完整代码
      • 最后总结

前言

做 月度预算看板 这类页面时,最怕两件事:一是业务信息一多就乱,二是代码写着写着失去层次。这个案例的价值,恰恰在于它把常见功能浓缩进一页,适合拿来做完整练习。

适用场景:适合记账应用、预算控制、家庭财务工具。
本篇重点:预算进度计算与分类预警
讲解方式:产品视角讲解

先看这个页面解决了什么问题

这个案例主要解决三件事:

  • 月度预算看板的核心数据集中托管在组件状态中。
  • 让用户在一个页面里完成查看、筛选、操作、反馈的完整闭环。
  • 用相对克制的 ArkUI 组件组合,把业务信息分层展示出来。

从实用角度看,这种页面通常都不是孤立存在的,它往往会挂在应用的某个一级入口下面。也就是说,示例里很多处理方式,都可以直接迁移到真实项目。

页面结构速览

先不急着看细节,先把页面结构建立起来:

区域作用代码里的体现
顶部区域提供标题与导航返回Row+Image+Text
内容主体展示核心信息与主操作区Column/List/Scroll等组合

| 统计或辅助区 | 展示聚合信息、筛选项或说明文字 | 计算属性与状态联动 |
| 行为入口 | 处理新增、切换、确认、推进等动作 | 事件函数和按钮点击逻辑 |

这一页里出现的主要数据类型有:BudgetCategory_5kgm

关键状态是怎么组织的

这类页面能不能写稳,关键往往不在布局,而在状态设计。当前案例里比较值得关注的状态包括:

  • month
  • totalBudget
  • categories
  • showAdjust

  • adjustTarget

站在真实业务页面的角度解释字段、统计和状态设计。

如果你自己扩展这个页面,建议优先判断哪些数据属于:

  • 长期状态:需要持续保留在页面上的数据,比如列表内容、筛选条件。
  • 瞬时状态:只在某一次交互中短暂生效,比如弹窗开关、输入框值、当前选中项。
  • 派生状态:不必单独保存,而是通过计算属性实时推导出来。

业务逻辑最值得看的函数

当前页面里,建议优先读下面这些方法:

  • get totalSpent
  • get remaining
  • overBudget
  • ListItem
  • if

我更建议你按照“先看输入,再看输出,最后看状态修改”的顺序去理解这些函数。这样读代码,比从头到尾硬看要轻松很多。

第一段重点代码:状态与数据入口

这部分代码定义了页面最核心的数据源,是理解整个案例的起点。

@Statemonth:string='2026年6月'@StatetotalBudget:number=8000@Statecategories:BudgetCategory_5kgm[]=[{name:'餐饮',icon:'🍜',budget:2000,spent:1560,color:'#FF6B9D'},{name:'交通',icon:'🚌',budget:600,spent:320,color:'#007AFF'},{name:'娱乐',icon:'🎮',budget:1000,spent:1200,color:'#FFA502'},{name:'购物',icon:'🛍️',budget:2000,spent:850,color:'#2ED573'},{name:'医疗',icon:'🏥',budget:500,spent:120,color:'#5A67D8'},{name:'教育',icon:'📚',budget:800,spent:680,color:'#FF4757'},{name:'居家',icon:'🏠',budget:500,spent:200,color:'#FFC107'},{name:'其他',icon:'📦',budget:600,spent:90,color:'#78909C'},]@StateshowAdjust:boolean=false@StateadjustTarget:BudgetCategory_5kgm|null=nullgettotalSpent():number{returnthis.categories.reduce((s,c)=>s+c.spent,0)}getremaining():number{

这里有几个很实用的判断标准:

  • @State字段通常就是页面的驱动中心。
  • 如果一个字段一改,界面就会立即跟着变化,那它大概率就应该放进状态里。
  • 如果某个值完全可以通过已有状态算出来,就更适合做计算属性。

第二段重点代码:核心交互逻辑

真正决定这个页面“好不好用”的,往往不是布局,而是交互逻辑是否顺手。下面这段代码就是关键部分:

totalSpent():number{returnthis.categories.reduce((s,c)=>s+c.spent,0)}getremaining():number{returnthis.totalBudget-this.totalSpent}overBudget(cat:BudgetCategory_5kgm):boolean{returncat.spent>cat.budget}build(){Stack(){Column(){Row(){Image($r('app.media.ic_back')).width(24).height(24).onClick(()=>router.back())Text('月度预算').fontSize(18).fontWeight(FontWeight.Bold).layoutWeight(1).textAlign(TextAlign.Center)Text('📊').fontSize(20)}.width('100%').padding({left:16,right:16,top:12,bottom:12})

可以重点注意三件事:

  1. 这个函数接收了什么输入。
  2. 它到底修改了哪些状态。
  3. 修改之后,页面上哪些区域会同步刷新。

把这三点串起来,基本就能搞懂页面的交互链路。

使用方法:把页面跑起来之后怎么操作

如果你想验证这个案例是否真正理解到位,可以按下面顺序手动操作一遍:

  1. 先进入页面,观察顶部信息和默认展示内容。
  2. 再尝试切换筛选项、按钮或标签,看列表是否会立即变化。
  3. 接着执行一次主操作,比如新增、切换、标记、选择或推进。
  4. 最后回头观察统计区、提示区或状态区有没有同步更新。

如果这一整套流程都能说清楚,说明你已经不只是“看懂了界面”,而是看懂了它的运行机制。

这个实现为什么值得借鉴

从 ArkUI 工程角度看,这个案例至少有三点值得保留:

  • 状态粒度比较克制:没有把所有东西都拆成独立变量。
  • 视图结构相对清楚:顶部、内容区、辅助区和操作区分得开。
  • 业务函数命名直白:阅读和后续维护成本都比较低。

尤其是在做教学示例时,这种写法很重要。因为读者不是来欣赏技巧的,而是来确认“这个页面到底怎么搭起来”。

可以继续优化的地方

如果你准备把这个页面继续扩展成正式项目,我会优先改这几件事:

  • 把数据模型和页面组件拆开,避免页面文件承担过多职责。
  • 把重复使用的卡片、标签、行项抽成独立组件。
  • 给关键操作补上更明确的异常处理、空状态和边界提示。
  • 如果涉及远程数据,再补上加载态和接口层封装。

完整代码

下面保留案例完整代码,方便你直接对照学习、复制和重构。

import{router}from'@kit.ArkUI'interfaceBudgetCategory_5kgm{name:stringicon:stringbudget:numberspent:numbercolor:string}@Entry@Componentstruct MonthlyBudgetDashboard{@Statemonth:string='2026年6月'@StatetotalBudget:number=8000@Statecategories:BudgetCategory_5kgm[]=[{name:'餐饮',icon:'🍜',budget:2000,spent:1560,color:'#FF6B9D'},{name:'交通',icon:'🚌',budget:600,spent:320,color:'#007AFF'},{name:'娱乐',icon:'🎮',budget:1000,spent:1200,color:'#FFA502'},{name:'购物',icon:'🛍️',budget:2000,spent:850,color:'#2ED573'},{name:'医疗',icon:'🏥',budget:500,spent:120,color:'#5A67D8'},{name:'教育',icon:'📚',budget:800,spent:680,color:'#FF4757'},{name:'居家',icon:'🏠',budget:500,spent:200,color:'#FFC107'},{name:'其他',icon:'📦',budget:600,spent:90,color:'#78909C'},]@StateshowAdjust:boolean=false@StateadjustTarget:BudgetCategory_5kgm|null=nullgettotalSpent():number{returnthis.categories.reduce((s,c)=>s+c.spent,0)}getremaining():number{returnthis.totalBudget-this.totalSpent}overBudget(cat:BudgetCategory_5kgm):boolean{returncat.spent>cat.budget}build(){Stack(){Column(){Row(){Image($r('app.media.ic_back')).width(24).height(24).onClick(()=>router.back())Text('月度预算').fontSize(18).fontWeight(FontWeight.Bold).layoutWeight(1).textAlign(TextAlign.Center)Text('📊').fontSize(20)}.width('100%').padding({left:16,right:16,top:12,bottom:12})// Overview cardColumn(){Text(this.month).fontSize(13).fontColor('#FFFFFFAA').margin({bottom:8})Row(){Column(){Text(\`¥\${this.totalBudget}\`).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFF')Text('总预算').fontSize(11).fontColor('#FFFFFFAA').margin({top:4})}.layoutWeight(1).alignItems(HorizontalAlign.Center)Column(){Text(\`¥\${this.totalSpent}\`).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFF')Text('已花费').fontSize(11).fontColor('#FFFFFFAA').margin({top:4})}.layoutWeight(1).alignItems(HorizontalAlign.Center)Column(){Text(\`¥\${Math.abs(this.remaining)}\`).fontSize(22).fontWeight(FontWeight.Bold).fontColor(this.remaining>=0?'#7BED9F':'#FF6B6B')Text(this.remaining>=0?'剩余':'超支').fontSize(11).fontColor('#FFFFFFAA').margin({top:4})}.layoutWeight(1).alignItems(HorizontalAlign.Center)}.width('100%').margin({bottom:12})Progress({value:this.totalSpent,total:this.totalBudget}).color(this.remaining>=0?'#7BED9F':'#FF6B6B').backgroundColor('#FFFFFF40').height(8).borderRadius(4)Text(\`已使用 \${Math.round(this.totalSpent/this.totalBudget*100)}%\`).fontSize(11).fontColor('#FFFFFFAA').margin({top:6})}.width('calc(100% - 32vp)').borderRadius(16).padding(20).margin({bottom:16}).linearGradient({colors:[['#667EEA',0.0],['#764BA2',1.0]],angle:135})// Category listList({space:8}){ForEach(this.categories,(cat:BudgetCategory_5kgm)=>{ListItem(){Column(){Row(){Text(cat.icon).fontSize(28).width(44).textAlign(TextAlign.Center)Column(){Row(){Text(cat.name).fontSize(14).fontWeight(FontWeight.Medium).fontColor('#333').layoutWeight(1)if(this.overBudget(cat)){Text('超支').fontSize(11).fontColor('#FFF').backgroundColor('#FF4757').borderRadius(6).padding({left:6,right:6,top:2,bottom:2})}}.margin({bottom:6})Progress({value:cat.spent,total:cat.budget}).color(this.overBudget(cat)?'#FF4757':cat.color).backgroundColor('#F0F0F0').height(6).borderRadius(3)Row(){Text(\`已用 ¥\${cat.spent}\`).fontSize(11).fontColor('#888')Text(\`/¥\${cat.budget}\`).fontSize(11).fontColor('#CCC')Blank()Text(\`\${Math.round(cat.spent/cat.budget*100)}%\`).fontSize(11).fontColor(this.overBudget(cat)?'#FF4757':cat.color)}.margin({top:4})}.layoutWeight(1).padding({left:10})}}.width('100%').backgroundColor('#FFF').borderRadius(12).padding(12).shadow({radius:3,color:'#0000001A',offsetY:2}).onClick(()=>{this.adjustTarget=cat;this.showAdjust=true})}})}.layoutWeight(1).padding({left:16,right:16})}.width('100%').height('100%').backgroundColor('#F5F5F5')// Adjust dialogif(this.showAdjust&&this.adjustTarget!==null){Column(){Column(){Text(\`\${this.adjustTarget!.icon}\${this.adjustTarget!.name}预算\`).fontSize(17).fontWeight(FontWeight.Bold).margin({bottom:16})Text(\`当前预算:¥\${this.adjustTarget!.budget}\`).fontSize(14).fontColor('#666').margin({bottom:4})Text(\`已花费:¥\${this.adjustTarget!.spent}\`).fontSize(14).fontColor(this.overBudget(this.adjustTarget!)?'#FF4757':'#2ED573').margin({bottom:16})Button('关闭').width('100%').backgroundColor('#007AFF').fontColor('#FFF').borderRadius(12).onClick(()=>this.showAdjust=false)}.backgroundColor('#FFF').borderRadius(16).padding(24).width('80%')}.width('100%').height('100%').backgroundColor('#00000060').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)}}}}

最后总结

月度预算看板 这个案例并不靠复杂技巧取胜,它更像一个结构扎实、逻辑完整的业务页面样板。把这篇文章里的状态设计、函数组织和页面分层吃透之后,你再去写同类型功能,速度会明显快很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询