Flutter for OpenHarmony 个人理财管理App实战 - 预算详情页面
预算详情页面提供单个预算的完整信息展示,包括进度概览、金额统计、支出记录和趋势分析。页面核心功能包含:1) 圆形进度指示器直观显示使用情况;2) 预算金额、已用金额、剩余金额统计;3) 相关支出记录列表;4) 预算使用趋势图;5) 编辑和删除功能;6) 超支预警提示。设计采用Flutter框架实现,通过GetX状态管理各类服务,页面布局包含警告横幅、进度卡片、统计卡片、趋势图和交易列表,帮助用户全
预算详情页面展示单个预算的完整信息,包括使用进度和相关的支出记录。用户可以在这里了解预算的具体使用情况,及时调整消费行为。
功能设计
预算详情页面包含:
- 预算进度概览(圆形进度指示器)
- 预算金额、已用金额、剩余金额统计
- 相关支出记录列表
- 预算使用趋势图
- 编辑和删除预算功能
- 预算预警提示
设计思路
预算详情页面的核心目标是帮助用户:
- 直观了解预算使用进度
- 查看具体的支出明细
- 及时发现超支风险
- 方便地管理预算设置
页面实现
创建 budget_detail_page.dart,首先导入依赖:
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:percent_indicator/percent_indicator.dart';
import '../../core/services/category_service.dart';
import '../../core/services/transaction_service.dart';
import '../../core/services/budget_service.dart';
import '../../core/services/storage_service.dart';
导入了 Flutter 基础组件、屏幕适配、GetX 状态管理、日期格式化、圆形进度指示器等。这些是实现预算详情页面的基础依赖。
继续导入数据模型和路由:
import '../../data/models/budget_model.dart';
import '../../data/models/transaction_model.dart';
import '../../routes/app_pages.dart';
const _primaryColor = Color(0xFF2E7D32);
const _incomeColor = Color(0xFF4CAF50);
const _expenseColor = Color(0xFFE53935);
const _warningColor = Color(0xFFFF9800);
const _textSecondary = Color(0xFF757575);
定义了颜色常量,包括主色调、收入色、支出色、警告色和次要文字色。这些颜色会在整个页面中使用,保持视觉一致性。
定义页面类和状态类:
class BudgetDetailPage extends StatefulWidget {
const BudgetDetailPage({super.key});
State<BudgetDetailPage> createState() => _BudgetDetailPageState();
}
class _BudgetDetailPageState extends State<BudgetDetailPage> {
late BudgetModel _budget;
final _categoryService = Get.find<CategoryService>();
final _transactionService = Get.find<TransactionService>();
final _budgetService = Get.find<BudgetService>();
final _storage = Get.find<StorageService>();
使用 StatefulWidget 因为页面需要管理预算数据的状态。通过 Get.find 获取各种服务实例,用于获取分类、交易、预算数据。
初始化方法:
void initState() {
super.initState();
_budget = Get.arguments as BudgetModel;
}
从路由参数中获取预算对象,这是页面展示的核心数据。
构建页面主体结构:
Widget build(BuildContext context) {
final category = _budget.categoryId != null
? _categoryService.getCategoryById(_budget.categoryId!)
: null;
// 获取相关支出记录
final transactions = _transactionService
.getTransactionsByMonth(_budget.year, _budget.month)
.where((t) => t.type == TransactionType.expense &&
(_budget.isOverall || t.categoryId == _budget.categoryId))
.toList();
transactions.sort((a, b) => b.date.compareTo(a.date));
如果是分类预算,获取对应的分类信息。获取该月份的所有支出记录,如果是分类预算则只筛选该分类的支出,按时间倒序排列。
页面布局结构:
return Scaffold(
appBar: AppBar(
title: Text(_budget.isOverall ? '总预算详情' : (category?.name ?? '预算详情')),
actions: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _navigateToEdit(),
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _showDeleteDialog(),
),
],
),
body: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
if (_budget.isOverBudget) _buildWarningBanner(),
_buildProgressCard(category),
SizedBox(height: 16.h),
_buildStatisticsCard(transactions),
SizedBox(height: 16.h),
_buildDailyTrendCard(transactions),
SizedBox(height: 16.h),
_buildTransactionList(transactions),
],
),
),
);
}
}
AppBar 标题根据预算类型显示不同文字,右侧有编辑和删除按钮。如果预算超支,顶部显示警告横幅。页面内容包括进度卡片、统计卡片、趋势图和支出列表。
超支预警横幅
当预算超支时显示醒目的警告:
Widget _buildWarningBanner() {
return Container(
margin: EdgeInsets.only(bottom: 16.h),
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: _expenseColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: _expenseColor.withOpacity(0.3)),
),
child: Row(
children: [
Icon(Icons.warning_amber_rounded, color: _expenseColor, size: 24.sp),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'预算已超支',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: _expenseColor,
),
),
Text(
'已超出预算 ${_storage.currency}${(_budget.spent - _budget.amount).toStringAsFixed(2)}',
style: TextStyle(fontSize: 12.sp, color: _textSecondary),
),
],
),
),
],
),
);
}
## 进度概览卡片
进度卡片是页面的核心,展示预算使用情况:
```dart
Widget _buildProgressCard(category) {
// 计算进度颜色
Color progressColor = _primaryColor;
if (_budget.percentage >= 100) {
progressColor = _expenseColor;
} else if (_budget.percentage >= 80) {
progressColor = _warningColor;
}
根据预算使用比例动态计算进度条颜色。正常情况下是绿色,达到 80% 变为橙色警告,超支则变为红色。这种颜色变化能直观提醒用户预算状态。
卡片主体结构:
return Card(
child: Padding(
padding: EdgeInsets.all(24.w),
child: Column(
children: [
if (category != null) ...[
CircleAvatar(
radius: 32.r,
backgroundColor: category.color.withOpacity(0.2),
child: Icon(category.icon, size: 32.sp, color: category.color)
),
SizedBox(height: 12.h),
],
Text(
DateFormat('yyyy年MM月').format(DateTime(_budget.year, _budget.month)),
style: TextStyle(fontSize: 16.sp, color: _textSecondary)
),
SizedBox(height: 16.h),
如果是分类预算,顶部显示分类图标。然后显示预算所属的月份。使用 DateFormat 格式化日期为中文格式。
圆形进度指示器:
CircularPercentIndicator(
radius: 80.r,
lineWidth: 12.w,
percent: (_budget.percentage / 100).clamp(0.0, 1.0),
center: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${_budget.percentage.toStringAsFixed(0)}%',
style: TextStyle(fontSize: 24.sp, fontWeight: FontWeight.bold)
),
Text('已使用', style: TextStyle(fontSize: 12.sp, color: _textSecondary)),
],
),
progressColor: progressColor,
backgroundColor: Colors.grey[200]!,
animation: true,
animationDuration: 800,
),
SizedBox(height: 20.h),
使用 CircularPercentIndicator 显示圆形进度。中心显示百分比数字,进度条颜色根据使用情况变化。clamp 确保百分比在 0-100% 范围内,即使超支也不会超过 100%。动画时长 800ms,让进度显示更流畅。
底部统计信息:
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildInfo('预算', _budget.amount, _storage.currency),
_buildInfo('已用', _budget.spent, _storage.currency),
_buildInfo('剩余', _budget.remaining, _storage.currency, isRemaining: true),
],
),
],
),
),
);
}
三列布局显示预算总额、已用金额和剩余金额。spaceAround 让三列均匀分布。
统计信息项构建方法:
Widget _buildInfo(String label, double value, String currency, {bool isRemaining = false}) {
Color valueColor = const Color(0xFF212121);
if (isRemaining) valueColor = value >= 0 ? _incomeColor : _expenseColor;
return Column(
children: [
Text(label, style: TextStyle(fontSize: 12.sp, color: _textSecondary)),
SizedBox(height: 4.h),
Text(
'$currency${value.toStringAsFixed(0)}',
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold, color: valueColor)
),
],
);
}
每个统计项包含标签和数值。剩余金额根据正负显示不同颜色:正数为绿色,负数(超支)为红色。这种颜色编码让用户一眼就能看出预算状态。
统计信息卡片
统计卡片展示预算使用的详细数据:
Widget _buildStatisticsCard(List<TransactionModel> transactions) {
final daysInMonth = DateTime(_budget.year, _budget.month + 1, 0).day;
final today = DateTime.now();
final daysPassed = today.year == _budget.year && today.month == _budget.month
? today.day
: daysInMonth;
final dailyBudget = _budget.amount / daysInMonth;
final dailySpent = daysPassed > 0 ? _budget.spent / daysPassed : 0.0;
final transactionCount = transactions.length;
final avgTransaction = transactionCount > 0 ? _budget.spent / transactionCount : 0.0;
计算各种统计指标。daysInMonth 获取该月总天数,daysPassed 计算已过去的天数。dailyBudget 是日均预算,dailySpent 是日均支出,avgTransaction 是笔均金额。这些指标帮助用户了解消费习惯。
卡片布局:
return Card(
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('统计数据', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)),
SizedBox(height: 16.h),
Row(
children: [
Expanded(child: _buildStatItem('日均预算', dailyBudget)),
Expanded(child: _buildStatItem('日均支出', dailySpent)),
],
),
SizedBox(height: 12.h),
Row(
children: [
Expanded(child: _buildStatItem('交易笔数', transactionCount.toDouble(), isCount: true)),
Expanded(child: _buildStatItem('笔均金额', avgTransaction)),
],
),
],
),
),
);
}
两行两列的网格布局,展示四个统计指标。第一行是日均预算和日均支出,第二行是交易笔数和笔均金额。这种布局紧凑且易读。
统计项构建方法:
Widget _buildStatItem(String label, double value, {bool isCount = false}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(fontSize: 12.sp, color: _textSecondary)),
SizedBox(height: 4.h),
Text(
isCount ? '${value.toInt()} 笔' : '${_storage.currency}${value.toStringAsFixed(2)}',
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600),
),
],
);
}
每个统计项包含标签和数值。isCount 参数区分笔数和金额,笔数显示为整数加"笔"单位,金额显示为货币格式。
每日支出趋势图
趋势图让用户直观看到每天的支出情况:
Widget _buildDailyTrendCard(List<TransactionModel> transactions) {
// 按日期汇总支出
final dailyExpense = <int, double>{};
for (var t in transactions) {
final day = t.date.day;
dailyExpense[day] = (dailyExpense[day] ?? 0) + t.amount;
}
final daysInMonth = DateTime(_budget.year, _budget.month + 1, 0).day;
final dailyBudget = _budget.amount / daysInMonth;
遍历所有交易记录,按日期汇总每天的支出金额。dailyExpense 是一个 Map,key 是日期(1-31),value 是当天的总支出。计算日均预算作为参考线。
卡片头部:
return Card(
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('每日支出', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)),
Text(
'日均预算: ${_storage.currency}${dailyBudget.toStringAsFixed(0)}',
style: TextStyle(fontSize: 12.sp, color: _textSecondary),
),
],
),
SizedBox(height: 16.h),
标题左侧显示"每日支出",右侧显示日均预算金额,让用户知道每天应该控制在多少以内。
柱状图实现:
SizedBox(
height: 120.h,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: List.generate(daysInMonth, (index) {
final day = index + 1;
final expense = dailyExpense[day] ?? 0;
final maxExpense = dailyExpense.values.isEmpty
? dailyBudget
: dailyExpense.values.reduce((a, b) => a > b ? a : b);
final height = maxExpense > 0 ? (expense / maxExpense * 80).h : 0.0;
final isOverBudget = expense > dailyBudget;
用 Row 和 List.generate 生成每天的柱子。找出最大支出金额,用于计算每个柱子的相对高度。判断当天支出是否超过日均预算,用于设置柱子颜色。
柱子渲染:
return Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 1.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
height: height.clamp(2.0, 80.h),
decoration: BoxDecoration(
color: isOverBudget ? _expenseColor : _primaryColor,
borderRadius: BorderRadius.vertical(top: Radius.circular(2.r)),
),
),
SizedBox(height: 4.h),
if (day % 5 == 1 || day == daysInMonth)
Text(
'$day',
style: TextStyle(fontSize: 8.sp, color: _textSecondary),
)
else
SizedBox(height: 10.h),
],
),
),
);
}),
),
),
],
),
),
);
}
每个柱子用 Expanded 平均分配宽度。柱子高度根据支出金额按比例计算,最小 2 像素确保有支出的日子能看到柱子。超过日均预算的柱子显示为红色,正常的为绿色。底部每隔 5 天显示一次日期标签,避免太拥挤。
相关支出列表
支出列表展示与该预算相关的所有交易记录:
Widget _buildTransactionList(List<TransactionModel> transactions) {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('相关支出', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)),
Text(
'共 ${transactions.length} 笔',
style: TextStyle(fontSize: 12.sp, color: _textSecondary),
),
],
),
),
卡片头部显示标题和交易总笔数,让用户快速了解支出记录的数量。
空状态和列表项:
if (transactions.isEmpty)
Padding(
padding: EdgeInsets.all(24.w),
child: Center(
child: Text('暂无支出记录', style: TextStyle(color: _textSecondary, fontSize: 14.sp))
)
)
else
...transactions.take(20).map((t) {
final cat = _categoryService.getCategoryById(t.categoryId);
return ListTile(
leading: CircleAvatar(
backgroundColor: (cat?.color ?? Colors.grey).withOpacity(0.2),
child: Icon(cat?.icon ?? Icons.help, color: cat?.color ?? Colors.grey, size: 20.sp)
),
title: Text(cat?.name ?? '未知'),
subtitle: Text(DateFormat('MM-dd HH:mm').format(t.date)),
如果没有支出记录,显示空状态提示。否则显示交易列表,最多显示 20 条。每个列表项包含分类图标、分类名称和交易时间。
列表项尾部和点击事件:
trailing: Text(
'-${_storage.currency}${t.amount.toStringAsFixed(2)}',
style: TextStyle(color: _expenseColor, fontWeight: FontWeight.w600)
),
onTap: () => Get.toNamed(Routes.transactionDetail, arguments: t),
);
}),
右侧显示支出金额,用红色表示支出。点击列表项可以跳转到交易详情页面查看更多信息。
查看更多按钮:
if (transactions.length > 20)
Padding(
padding: EdgeInsets.all(16.w),
child: Center(
child: TextButton(
onPressed: () {
// 查看更多
},
child: Text('查看全部 ${transactions.length} 笔记录'),
),
),
),
],
),
);
}
如果交易记录超过 20 条,底部显示"查看更多"按钮,让用户可以查看完整列表。这种分页加载方式避免一次性渲染太多列表项影响性能。
编辑和删除功能
编辑预算功能:
void _navigateToEdit() {
Get.toNamed(Routes.budgetEdit, arguments: _budget)?.then((result) {
if (result != null && result is BudgetModel) {
setState(() => _budget = result);
}
});
}
点击编辑按钮跳转到预算编辑页面,传入当前预算对象。编辑完成后,如果返回了新的预算对象,更新当前页面的数据并刷新界面。
删除预算功能:
void _showDeleteDialog() {
Get.dialog(AlertDialog(
title: const Text('删除预算'),
content: const Text('确定要删除这个预算吗?此操作不可恢复。'),
actions: [
TextButton(onPressed: () => Get.back(), child: const Text('取消')),
TextButton(
onPressed: () {
_budgetService.deleteBudget(_budget.id);
Get.back();
Get.back();
Get.snackbar('成功', '预算已删除');
},
child: Text('删除', style: TextStyle(color: _expenseColor)),
),
],
));
}
点击删除按钮弹出确认对话框,避免误操作。确认后调用 BudgetService 删除预算,关闭对话框和详情页面,返回上一页并显示成功提示。删除按钮用红色文字突出显示,提醒用户这是危险操作。
BudgetModel 数据模型
预算数据模型定义:
class BudgetModel {
final String id;
final double amount;
final double spent;
final String? categoryId;
final int year;
final int month;
final bool isOverall;
BudgetModel({
required this.id,
required this.amount,
this.spent = 0,
this.categoryId,
required this.year,
required this.month,
this.isOverall = false,
});
id 是预算的唯一标识,amount 是预算金额,spent 是已用金额。categoryId 是分类 ID,总预算时为 null。year 和 month 指定预算所属的月份。isOverall 标记是否是总预算。
计算属性:
double get remaining => amount - spent;
double get percentage => amount > 0 ? (spent / amount * 100) : 0;
bool get isOverBudget => spent > amount;
remaining 计算剩余金额,percentage 计算使用百分比,isOverBudget 判断是否超支。这些计算属性让 UI 代码更简洁。
复制方法:
BudgetModel copyWith({
double? amount,
double? spent,
String? categoryId,
int? year,
int? month,
bool? isOverall,
}) {
return BudgetModel(
id: id,
amount: amount ?? this.amount,
spent: spent ?? this.spent,
categoryId: categoryId ?? this.categoryId,
year: year ?? this.year,
month: month ?? this.month,
isOverall: isOverall ?? this.isOverall,
);
}
}
copyWith 方法用于创建修改后的副本,保持数据不可变性。这是 Flutter 中常用的模式,便于状态管理。
预算提醒功能
预算提醒帮助用户及时了解预算状态:
void _checkBudgetAlert() {
if (_budget.percentage >= 80 && _budget.percentage < 100) {
// 显示警告提示
Get.snackbar(
'预算提醒',
'您的预算已使用 ${_budget.percentage.toStringAsFixed(0)}%,请注意控制支出',
backgroundColor: _warningColor.withOpacity(0.9),
colorText: Colors.white,
duration: const Duration(seconds: 4),
);
当预算使用达到 80% 但未超支时,显示橙色警告提示。提醒用户注意控制支出,避免超支。
超支提醒:
} else if (_budget.isOverBudget) {
Get.snackbar(
'预算超支',
'您的预算已超支 ${_storage.currency}${(_budget.spent - _budget.amount).toStringAsFixed(2)}',
backgroundColor: _expenseColor.withOpacity(0.9),
colorText: Colors.white,
duration: const Duration(seconds: 4),
);
}
}
当预算超支时,显示红色提示,告知用户具体超支金额。这种分级提醒机制帮助用户更好地管理预算。可以在页面初始化时调用这个方法,让用户进入页面就能看到提醒。
小结
预算详情页面让用户可以:
- 直观查看预算使用进度
- 了解具体的支出明细
- 通过趋势图发现消费规律
- 及时发现超支情况
- 方便地编辑和管理预算
下一篇将实现统计分析页面。
欢迎加入 OpenHarmony 跨平台开发社区,获取更多技术资源和交流机会:
https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)