进度面板与超支高亮:ArkUI 让鸿蒙预算页像财务仪表


实例:预算管理(Budget)|风格:进度面板
一、设计理念:预算页要「一眼知道花超没有」
预算管理的 UI 目标是财务仪表盘——顶部「总进度」(花了多少/剩多少/超支预警)+ 中部「分类进度条」(每类花到哪了)+ 底部「支出明细」(具体流水)。实例 17 的风格「进度面板」:深色总预算卡 + 分类进度条列表 + 支出明细,超支用红色高亮——「进度可视化 + 超支预警」是核心视觉。
页面信息架构:
- 标题栏:预算管理 + 当前月份 + 刷新;
- 总预算进度卡(深色):总进度百分比 + 已花/总预算 + 进度条 + 剩余或超支文案;
- 分类预算明细:每分类进度条 + 「已花/预算」+ 超支标签;
- 分类支出明细:选中分类的流水(金额红字 + 日期备注 + 删除);
- 新增支出弹窗:金额 + 备注。
二、页面骨架与核心状态
@State budgets: BudgetWithSpend[] = [];
@State expenses: Expense[] = [];
@State budgetTotal: number = 0;
@State spendTotal: number = 0;
@State selectedCategory: string = '餐饮';
@State addVisible: boolean = false;
@State fAmount: string = '';
@State fNote: string = '';
核心状态:budgets(预算+进度)、expenses(选中分类明细)、budgetTotal/spendTotal(总预算/总支出)、selectedCategory(选中分类——决定明细列表)。
build 骨架(Scroll 包裹——页面内容多需滚动):
Column() {
// 标题栏
Scroll() {
Column() {
// 总预算进度卡(深色)
// 分类预算明细(进度条列表)
// 分类支出明细(流水)
}
}
// 新增支出弹窗
}
三、总预算进度卡:深色仪表盘
顶部总进度卡用深色渐变(与页面浅色形成对比,突出仪表感):
Column() {
Text('本月总支出进度').fontSize(13).fontColor('#9CA3AF')
Stack({ alignContent: Alignment.Center }) {
Row().width(110).height(110).borderRadius(55).backgroundColor('#1F2937')
Column() {
Text(`${this.totalPercent()}%`).fontSize(28).fontWeight(FontWeight.Bold).fontColor(Color.White)
Text(`¥${this.spendTotal.toFixed(0)} / ¥${this.budgetTotal.toFixed(0)}`).fontSize(11).fontColor('#9CA3AF').margin({ top: 4 })
}
}
// 进度条
Row() {
Row() {
Row().width('100%').height(10).borderRadius(5)
.backgroundColor(this.totalPercent() > 100 ? '#EF4444' : '#4ADE80')
}
.width(`${this.totalPercent()}%`)
.height(10).borderRadius(5)
}
.width('80%').height(10).borderRadius(5).backgroundColor('#374151').margin({ top: 12 })
Text(this.totalPercent() > 100 ? '⚠️ 已超支,请控制开支!' : `剩余可用 ¥${(this.budgetTotal - this.spendTotal).toFixed(2)}`)
.fontSize(13).fontWeight(FontWeight.Medium)
.fontColor(this.totalPercent() > 100 ? '#F87171' : '#4ADE80')
.margin({ top: 10 })
}
.width('94%').padding(18)
.borderRadius(18)
.linearGradient({ angle: 180, colors: [['#0F172A', 0], ['#1E293B', 1]] })
.margin({ top: 10 })
深色卡片三要素:
- 大数字百分比:28fp 白色大字(
64%)居中在 110×110 深灰圆内——仪表盘中心; - 已花/总预算:
¥2453 / ¥7500灰字——对照数字; - 进度条 + 状态文案:绿色条(正常)/ 红色条 + 「⚠️ 已超支」(超支)——状态一眼可辨。
linearGradient 深色渐变:angle: 180 + ['#0F172A', 0] 到 ['#1E293B', 1]——ArkUI 线性渐变(首现于本实例),深蓝黑渐变卡片从浅灰页面中「浮出」——仪表盘的视觉焦点。
totalPercent 计算(页面层):
private totalPercent(): number {
if (this.budgetTotal <= 0) {
return 0;
}
return Math.min(100, Math.round(this.spendTotal / this.budgetTotal * 100));
}
进度封顶 100:Math.min(100, …)——进度条宽度不溢出;超支由文案区分(> 100 判断显示超支文案而非百分比)。「视觉封顶 + 逻辑判断」分离。
四、分类预算明细:进度条列表
每分类一张白卡——分类名 + 进度/超支标签 + 金额 + 进度条:
Column() {
Text('分类预算明细').fontSize(16).fontWeight(FontWeight.Bold)
ForEach(this.budgets, (bw: BudgetWithSpend) => {
Column() {
Row() {
Text(bw.budget.category).fontSize(14).fontWeight(FontWeight.Medium)
Text(bw.overrun ? '超支!' : `${bw.percent}%`)
.fontSize(11).padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor(bw.overrun ? '#FEE2E2' : '#E5F6EC')
.fontColor(bw.overrun ? '#DC2626' : '#059669')
Blank()
Text(`¥${bw.spent.toFixed(0)} / ¥${bw.budget.amount.toFixed(0)}`).fontSize(12).fontColor('#6B7280')
}.width('100%')
Row() {
Row() {
Row().width('100%').height(10).borderRadius(5)
.backgroundColor(bw.overrun ? '#EF4444' : bw.budget.color)
}
.width(`${bw.percent}%`)
.height(10).borderRadius(5)
}
.width('100%').height(10).borderRadius(5).backgroundColor('#E5E7EB').margin({ top: 6 })
}
.width('100%').padding(12).backgroundColor(Color.White).borderRadius(10).margin({ top: 8 })
.onClick(() => this.switchCategory(bw.budget.category))
}, (bw: BudgetWithSpend) => `${bw.budget.id}-${bw.budget.category}`)
}
.width('94%').padding(16).backgroundColor(Color.White).borderRadius(16).margin({ top: 12 })
.alignItems(HorizontalAlign.Start)
进度条卡五要素:
- 分类名:14fp 中等字重;
- 状态标签:「超支!」红底红字 vs 「62%」绿底绿字——超支标红的核心视觉;
- 已花/预算:
¥1380 / ¥1500灰字——Blank() 撑到右侧; - 进度条:分类色条(超支红色)铺在浅灰轨道上——宽度
${percent}%; - 点卡切分类:onClick switchCategory——明细列表联动。
进度条的宽度绑定:Row().width(${bw.percent}%)——百分比字符串宽度(ArkUI 支持百分比宽度),外层轨道 width('100%')。「嵌套 Row 实现进度条」:内层填充条宽度百分比 = 进度。双层进度条结构(轨道 + 填充)是 ArkUI 无 Progress 组件的自制方案(或直接用 Progress 组件——落地用自制 Row 更可控)。
超支的颜色切换:bw.overrun ? '#EF4444' : bw.budget.color——超支覆盖分类色(红色优先),状态标签同步红绿切换。
五、分类支出明细:流水列表
底部「选中分类的支出流水」:
Column() {
Row() {
Text(`${this.selectedCategory} 支出明细`).fontSize(16).fontWeight(FontWeight.Bold)
Blank()
Text('+').fontSize(22).fontColor('#3B82F6').onClick(() => {
this.fAmount = ''; this.fNote = '';
this.addVisible = true;
})
}.width('100%')
if (this.expenses.length === 0) {
Text('该分类暂无支出').fontSize(13).fontColor('#999999').margin({ top: 12 })
} else {
ForEach(this.expenses, (e: Expense) => {
Row({ space: 10 }) {
Column({ space: 2 }) {
Text(`-¥${e.amount.toFixed(2)}`).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#DC2626')
Text(`${this.fmtTime(e.spendTime)}${e.note ? ' · ' + e.note : ''}`).fontSize(11).fontColor('#999999')
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('✕').fontSize(14).fontColor('#CCCCCC').onClick(() => this.onDelete(e.id))
}
.width('100%').padding(12).backgroundColor(Color.White).borderRadius(10).margin({ top: 8 })
}, (e: Expense) => `e-${e.id}`)
}
}
.width('94%').padding(16).margin({ top: 12, bottom: 24 })
.alignItems(HorizontalAlign.Start)
流水行三要素:
- 金额红字:
-¥28.5015fp 红色粗体——支出即红色(财务惯例); - 日期 + 备注:
6月24日 · 奶茶(有备注才显示「· 备注」)——条件拼接; - 删除 ✕:灰色小叉。
空态:expenses.length === 0 显示「该分类暂无支出」——空列表友好提示(选中分类无流水时不显示空区域)。
六、新增支出弹窗
if (this.addVisible) {
Column() {
Text(`💰 记一笔${this.selectedCategory}支出`).fontSize(18).fontWeight(FontWeight.Bold)
TextInput({ placeholder: '金额(元)*', text: this.fAmount }).type(InputType.Number)
.margin({ top: 12 }).onChange((v: string) => this.fAmount = v)
TextInput({ placeholder: '备注(可选)', text: this.fNote })
.margin({ top: 8 }).onChange((v: string) => this.fNote = v)
Row({ space: 8 }) {
Button('取消').layoutWeight(1).backgroundColor('#EEF2F7').fontColor('#555555')
.onClick(() => this.addVisible = false)
Button('保存').layoutWeight(1).backgroundColor('#3B82F6')
.onClick(() => this.onAdd())
}.margin({ top: 16 })
}
.padding(20).borderRadius(16).backgroundColor(Color.White).width('88%')
.position({ x: '6%', y: '30%' })
}
弹窗标题带选中分类:记一笔餐饮支出——用户知道记到哪类(从分类卡进入)。金额数字键盘:InputType.Number。onAdd 校验:parseFloat 后 !amount || amount <= 0 → Toast「请输入有效金额」——金额合法性校验(负数/零/非数字拒绝)。
七、UI 风格要素一览
| 风格项 | 取值 | 说明 |
|---|---|---|
| 页面背景 | #F8FAFC 极浅灰蓝 |
财务清爽 |
| 总预算卡 | 深色渐变 #0F172A→#1E293B |
仪表盘焦点 |
| 进度条 | 双层 Row 百分比宽 | 进度可视化 |
| 超支高亮 | 红底标签 + 红进度条 | 预警视觉 |
| 支出金额 | 红色粗体 -¥ | 财务惯例 |
| 状态文案 | 剩余可用 / ⚠️ 已超支 | 对照提示 |
八、文章小结
本篇文章完成了实例 17 的 UI 层:深色总预算进度卡(大数字 + 进度条 + 剩余/超支文案)+ 分类进度条列表(状态标签 + 双层进度条 + 超支标红)+ 支出明细流水(红色金额 + 空态提示)+ 新增支出弹窗。设计核心是「进度可视化 + 超支预警」——深色仪表卡聚焦总进度、红色高亮预警超支分类;技术亮点是双层 Row 自制进度条(百分比宽度填充)与 linearGradient 深色渐变(首次登场)。
下一篇(17-3)深入数据层,讲解预算与支出的聚合计算——WHERE + SUM 分类聚合与进度/超支的派生逻辑。
九、补充:进度条与超支视觉
9.1 双层 Row 进度条 vs Progress 组件
本实例的进度条是双层 Row 自制方案(外层轨道 + 内层填充,宽度百分比),而不是 ArkUI 的 Progress 组件。对比:
| 维度 | 双层 Row 自制 | Progress 组件 |
|---|---|---|
| 实现 | Row().width('100%') 轨道 + 内层 width(\${percent}%`)` 填充 |
<Progress value={percent} total={100} /> |
| 颜色切换 | backgroundColor 直接换色(超支红/分类色) |
需 color 属性或样式覆盖 |
| 圆角 | 双层各自 borderRadius(5) 自由控制 |
自带轨道圆角,定制受限 |
| 状态联动 | 填充色与标签同一三元判断,天然同步 | 需额外计算状态再分别设置 |
| 适用 | 进度 + 超支双色逻辑复杂时更可控 | 单纯进度展示更简洁 |
结论:双层 Row 适合「颜色随状态变化 + 与标签联动」的场景——超支时轨道填充色、状态标签、剩余文案三处同步变红,用同一个 overrun 判断驱动,避免多处状态不同步。
9.2 超支红色高亮的统一规则
实例 17 的红色体系是一套统一规则,所有超支视觉共用同一组色值:
| 用途 | 正常态 | 超支态 |
|---|---|---|
| 进度条填充 | 分类色 / #4ADE80 |
#EF4444 |
| 状态标签背景 | #E5F6EC 绿底 |
#FEE2E2 红底 |
| 状态标签文字 | #059669 |
#DC2626 |
| 顶部状态文案 | #4ADE80 剩余可用 |
#F87171 ⚠️ 已超支 |
规则:进度条、标签、文案三处只认一个来源——bw.overrun(分类)或 totalPercent() > 100(总卡),不各自算一遍超支条件,避免「进度条红了标签还是绿」的割裂。
9.3 金额输入校验细节
新增支出弹窗的 onAdd 校验链:
private onAdd() {
const amount = parseFloat(this.fAmount);
if (!amount || amount <= 0) {
promptAction.showToast({ message: '请输入有效金额' });
return;
}
// 校验通过 → 插入支出 → 刷新列表与进度
}
要点:parseFloat 容忍前导空格与小数(" 28.5" → 28.5);!amount 拦截 NaN/0/空串;amount <= 0 拦负数。校验失败只 Toast 不关闭弹窗——输入框保留,用户直接修改重提。
9.4 FAQ
Q1:进度条宽度用百分比字符串,小数点很多会不会布局异常?
不会。ArkUI 的 width() 接受 "62.5%" 这类字符串,会按容器宽度比例解析;Math.round 已把 percent 取整,宽度值始终是整数百分比,渲染稳定。
Q2:为什么总进度封顶 100,超支却还要单独判断?
进度条是视觉比例,超出 100% 会溢出轨道破坏布局,所以 Math.min(100, ...) 封顶;而超支是业务状态,必须用真实比例 > 100 判断才能显示「⚠️ 已超支」。两者分离:宽度永远 ≤ 100%,文案永远反映真实比例。
更多推荐




所有评论(0)