预算管理全量代码与效果:ArkTS 在 HarmonyOS 的私人财务仪表


实例:预算管理(Budget)|收官文章
一、文件清单
| 文件 | 职责 | 行数 |
|---|---|---|
database/BudgetDao.ets |
数据层:双表、分类聚合、进度计算、6 类预算 + 25 笔支出种子 | 约 310 行 |
pages/samples/BudgetPage.ets |
UI 层:总进度卡 + 分类进度条 + 支出明细 + 记支出弹窗 | 约 225 行 |
resources/base/profile/main_pages.json |
路由注册:pages/samples/BudgetPage |
追加一行 |
pages/Index.ets |
首页入口按钮 | 追加一个按钮 |
本篇文章完整展示可编译运行的 BudgetPage 代码,最后描述运行效果。
二、BudgetPage 完整代码
import { common } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
import { BudgetDao, BudgetWithSpend, Expense } from '../../database/BudgetDao';
@Entry
@Component
struct BudgetPage {
@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 = '';
private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
aboutToAppear(): void {
this.refresh();
}
private currentMonth(): string {
const now = new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
}
async refresh(): Promise<void> {
try {
await BudgetDao.initSeedData(this.context);
this.budgets = await BudgetDao.queryWithSpend(this.context, this.currentMonth());
const t = await BudgetDao.totalForMonth(this.context, this.currentMonth());
this.budgetTotal = t.budgetTotal;
this.spendTotal = t.spendTotal;
if (this.budgets.length > 0) {
this.selectedCategory = this.budgets[0].budget.category;
}
await this.loadExpenses();
} catch (e) {
promptAction.showToast({ message: `加载失败: ${e}` });
}
}
async loadExpenses(): Promise<void> {
this.expenses = await BudgetDao.queryExpensesByCategory(this.context, this.selectedCategory);
}
async switchCategory(cat: string): Promise<void> {
this.selectedCategory = cat;
await this.loadExpenses();
}
async onAdd(): Promise<void> {
const amount = parseFloat(this.fAmount);
if (!amount || amount <= 0) {
promptAction.showToast({ message: '请输入有效金额' });
return;
}
const expense: Expense = {
id: 0,
category: this.selectedCategory,
amount: amount,
note: this.fNote.trim(),
spendTime: Date.now(),
};
await BudgetDao.insertExpense(this.context, expense);
this.addVisible = false;
this.fAmount = ''; this.fNote = '';
await this.refresh();
promptAction.showToast({ message: '✅ 支出已记录' });
}
onDelete(id: number): void {
BudgetDao.deleteExpense(this.context, id).then(async () => {
await this.refresh();
promptAction.showToast({ message: '🗑 已删除' });
});
}
/** 计算总进度百分比 */
private totalPercent(): number {
if (this.budgetTotal <= 0) {
return 0;
}
return Math.min(100, Math.round(this.spendTotal / this.budgetTotal * 100));
}
private fmtTime(ts: number): string {
const d = new Date(ts);
return `${d.getMonth() + 1}月${d.getDate()}日`;
}
build() {
Column() {
// ===== 标题栏 =====
Row() {
Column() {
Text('💹 预算管理').fontSize(22).fontWeight(FontWeight.Bold)
Text(`${this.currentMonth()} · 预算 vs 实际`).fontSize(12).fontColor('#999999').margin({ top: 2 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('↻').fontSize(22).onClick(() => this.refresh())
}.width('100%').padding({ left: 16, right: 16, top: 12 })
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 })
// ===== 分类预算明细 =====
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)
// ===== 分类支出明细 =====
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)
}
}
.width('100%').layoutWeight(1).scrollBar(BarState.Off)
// ===== 新增支出弹窗 =====
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%' })
}
}
.width('100%').height('100%').backgroundColor('#F8FAFC')
}
}
三、注册与运行
main_pages.json追加"pages/samples/BudgetPage";Index.ets追加:Button('💹 17 预算管理').fontSize(15).width('70%') .onClick(() => this.getUIContext().getRouter().pushUrl({ url: 'pages/samples/BudgetPage' }))- 构建验证 →
BUILD SUCCESSFUL。
四、运行效果描述
进入「💹 17 预算管理」:
第一屏:标题栏「💹 预算管理 · 2024-06 · 预算 vs 实际」;深色渐变总预算卡——「本月总支出进度」+ 大数字「35%」+「¥2598 / ¥7500」+ 绿色进度条 + 「剩余可用 ¥4901.50」;「分类预算明细」6 张白卡——餐饮「42%」绿标签 + ¥623/¥1500 + 橙色进度条、购物「61%」+ 粉色进度条…;「餐饮 支出明细」9 笔流水(生日聚餐 -¥210.00 最顶、楼下快餐 -¥28.50 最底)。
交互一(超支预警闭环):点「购物」卡 → 明细变 6 笔(球鞋 -¥420.00 等)→ 点明细右上「+」→ 弹窗「💰 记一笔购物支出」→ 金额输 2000 → 保存 → Toast「✅ 支出已记录」→ 购物卡变「超支!」红标签、进度条变红、总进度 35%→61%、剩余可用减少 → 超支预警完整呈现。
交互二(分类联动):点「学习」卡 → 「学习 支出明细」1 笔(在线课程 -¥45.00)→ 点「住房」→ 1 笔(电费 -¥320.00)。
交互三(删除恢复):删除刚才的 2000 元支出 → 购物恢复「61%」绿、总进度回落。
交互四(金额校验):点「+」→ 金额留空点保存 → Toast「请输入有效金额」(!amount || amount <= 0 拦截)。
五、代码质量要点回顾
| 关注点 | 本实例做法 |
|---|---|
| 双表对照 | budget 计划 + expense 流水 |
| 分类聚合 | WHERE + SUM |
| 进度计算 | spent/amount + 封顶 + 除零守卫 |
| 超支判断 | spent > amount 独立布尔 |
| 复合返回 | BudgetWithSpend 实体+派生 |
| 进度条 | 双层 Row 百分比宽 |
| 金额校验 | parseFloat + <= 0 拦截 |
六、文章小结
「预算管理」收官。五篇文章覆盖:双表对照建模(-1)→ 进度面板 UI(-2)→ 分类聚合与超支判断(-3)→ 6 类预算 + 25 笔支出种子(-4)→ 全量代码(-5)。核心技术是「预算 vs 实际」的双表对照结构(业务键 category 关联)、WHERE + SUM 条件聚合、进度/超支的派生计算(封顶 + 除零守卫 + 独立布尔)、双层 Row 自制进度条与超支红色高亮。这是「计划 vs 执行」类财务应用的完整范式。
更多推荐



所有评论(0)