Flutter for OpenHarmony 个人理财管理App实战 - 交易详情页面
本文介绍了交易详情页面的设计与实现,主要功能包括:展示交易分类图标、金额(收入绿色/支出红色)、账户信息、日期和备注等关键数据,并提供编辑和删除功能。页面采用卡片式布局,分为顶部信息卡片(突出显示分类图标和金额)、详情卡片(展示辅助信息)和操作区域。技术实现上使用Flutter框架,通过GetX管理状态,确保界面层次清晰、操作便捷。文章详细讲解了页面结构、颜色方案、数据展示逻辑以及删除操作的二次确
在上一篇文章中,我们完成了交易记录列表的开发。当用户点击某条交易记录时,需要查看该笔交易的详细信息。本篇将实现交易详情页面,展示完整的交易信息并提供编辑和删除功能。
功能需求分析
交易详情页面需要满足以下需求:
- 展示交易的分类图标和名称
- 突出显示交易金额(收入为绿色,支出为红色)
- 显示关联的账户信息
- 显示交易日期和创建时间
- 如有备注则显示备注内容
- 提供编辑交易的功能
- 提供删除交易的功能(需二次确认)
这些功能看起来简单,但细节处理很重要。好的详情页应该让用户一眼就能看到最重要的信息,同时提供便捷的操作入口。
页面结构设计
整个页面采用卡片式布局,分为多个主要区域:
- 顶部卡片:展示分类图标、名称和金额,这是最重要的信息
- 详情卡片:展示账户、日期、时间、备注等辅助信息
- 操作区域:编辑和删除按钮
这种布局让信息层次分明,用户可以快速获取关键信息,也能方便地进行操作。
创建交易详情页面
在 lib/app/modules/transaction/ 目录下创建 transaction_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 '../../core/services/transaction_service.dart';
import '../../core/services/category_service.dart';
import '../../core/services/account_service.dart';
import '../../core/services/storage_service.dart';
import '../../data/models/transaction_model.dart';
const _primaryColor = Color(0xFF2E7D32);
const _incomeColor = Color(0xFF4CAF50);
const _expenseColor = Color(0xFFE53935);
const _textSecondary = Color(0xFF757575);
导入部分包含了 Flutter 核心库、屏幕适配库、GetX、日期格式化库,以及项目内部的各个服务。TransactionService 用于删除交易,CategoryService 和 AccountService 用于获取关联的分类和账户信息,StorageService 提供货币符号。
颜色常量定义在文件顶部,和其他页面保持一致。_incomeColor 用绿色表示收入,_expenseColor 用红色表示支出,这种配色方案贯穿整个应用,用户已经形成了认知习惯。_textSecondary 用于次要文字,灰色调不会抢主要内容的风头。
页面主体结构
页面使用 StatelessWidget,因为所有数据都是从路由参数获取的,不需要管理本地状态:
class TransactionDetailPage extends StatelessWidget {
const TransactionDetailPage({super.key});
Widget build(BuildContext context) {
final transaction = Get.arguments as TransactionModel;
final categoryService = Get.find<CategoryService>();
final accountService = Get.find<AccountService>();
final transactionService = Get.find<TransactionService>();
final storage = Get.find<StorageService>();
final category = categoryService.getCategoryById(transaction.categoryId);
final account = accountService.getAccountById(transaction.accountId);
return Scaffold(
appBar: AppBar(
title: const Text('交易详情'),
actions: [
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _showDeleteDialog(transactionService, transaction.id),
),
],
),
Get.arguments 获取从列表页传递过来的交易数据,类型转换为 TransactionModel。然后通过 Get.find 获取各个服务的实例,再根据交易的 categoryId 和 accountId 查找关联的分类和账户信息。
AppBar 右侧放置删除按钮,使用红色的删除图标。点击时调用 _showDeleteDialog 方法显示确认对话框,防止用户误操作。删除是危险操作,必须有二次确认。
页面主体使用 SingleChildScrollView 包裹,确保内容超出屏幕时可以滚动:
body: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
_buildHeaderCard(transaction, category, storage),
SizedBox(height: 16.h),
_buildDetailCard(transaction, account),
SizedBox(height: 16.h),
_buildActionButtons(transaction, transactionService),
],
),
),
);
}
}
页面分为三个主要区域:顶部信息卡片、详情卡片、操作按钮。每个区域之间用 SizedBox 添加 16.h 的间距,保持视觉上的呼吸感。padding 设为 16.w,和其他页面保持一致的边距。
顶部信息卡片
顶部卡片是页面的视觉焦点,展示分类图标和交易金额:
Widget _buildHeaderCard(
TransactionModel transaction,
category,
StorageService storage
) {
return Card(
child: Padding(
padding: EdgeInsets.all(24.w),
child: Column(
children: [
CircleAvatar(
radius: 32.r,
backgroundColor: (category?.color ?? Colors.grey).withOpacity(0.2),
child: Icon(
category?.icon ?? Icons.help,
size: 32.sp,
color: category?.color ?? Colors.grey
),
),
SizedBox(height: 16.h),
Text(
category?.name ?? '未知分类',
style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.w600)
),
Card 组件包裹整个顶部区域,padding 设为 24.w 比普通卡片稍大,让内容有更多呼吸空间。CircleAvatar 显示分类图标,radius 设为 32.r 是一个较大的尺寸,让图标更醒目。
背景色使用分类颜色的 20% 透明度,既有颜色又不会太抢眼。category?.color 使用空安全操作符,如果分类不存在就用灰色作为默认值。分类名称用 18.sp 的中等字号和 w600 的粗细,比普通文字更突出。
金额显示部分,根据交易类型显示不同的颜色和符号:
SizedBox(height: 8.h),
Text(
'${transaction.type == TransactionType.income ? '+' : '-'}'
'${storage.currency}${transaction.amount.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 32.sp,
fontWeight: FontWeight.bold,
color: transaction.type == TransactionType.income
? _incomeColor
: _expenseColor,
),
),
],
),
),
);
}
金额是页面上最大的文字,用 32.sp 的字号和 bold 粗体。收入前面加 + 号并显示绿色,支出前面加 - 号并显示红色。这种视觉区分让用户一眼就能识别交易类型,不需要额外的标签说明。
toStringAsFixed(2) 保留两位小数,让金额显示更规范。storage.currency 获取用户设置的货币符号,支持多币种显示。三元表达式根据交易类型选择颜色,代码简洁明了。
详情信息卡片
详情卡片采用列表形式展示各项辅助信息:
Widget _buildDetailCard(TransactionModel transaction, account) {
return Card(
child: Column(
children: [
_buildDetailItem(
Icons.account_balance_wallet,
'账户',
account?.name ?? '未知'
),
const Divider(height: 1),
_buildDetailItem(
Icons.calendar_today,
'日期',
DateFormat('yyyy-MM-dd HH:mm').format(transaction.date)
),
Card 包裹所有详情项,Column 垂直排列。每个详情项之间用 Divider 分隔,height 设为 1 让分隔线尽量细,不会太抢眼。账户信息显示账户名称,如果账户不存在显示"未知"。
日期使用 DateFormat 格式化为 ‘yyyy-MM-dd HH:mm’ 格式,同时显示日期和时间。这个格式清晰易读,用户一眼就能看出交易发生的具体时间。
继续添加创建时间和备注信息:
const Divider(height: 1),
_buildDetailItem(
Icons.access_time,
'创建时间',
DateFormat('yyyy-MM-dd HH:mm').format(transaction.createdAt)
),
if (transaction.note != null && transaction.note!.isNotEmpty) ...[
const Divider(height: 1),
_buildDetailItem(Icons.note, '备注', transaction.note!),
],
],
),
);
}
创建时间和交易日期可能不同,比如用户补记之前的交易。备注信息使用条件渲染,只有当备注不为空时才显示。if 语句配合展开运算符 … 可以条件性地添加多个 Widget,这是 Dart 的语法特性。
这种设计避免了显示空白的备注行,让界面更简洁。transaction.note! 使用 ! 操作符是因为前面已经判断过不为 null 且不为空。
详情项组件
每个详情项的布局保持一致,左侧图标,中间标签,右侧值:
Widget _buildDetailItem(IconData icon, String label, String value) {
return Padding(
padding: EdgeInsets.all(16.w),
child: Row(
children: [
Icon(icon, color: _primaryColor, size: 20.sp),
SizedBox(width: 12.w),
Text(
label,
style: TextStyle(fontSize: 14.sp, color: _textSecondary)
),
const Spacer(),
Padding 设为 16.w,和其他组件保持一致的内边距。Row 组件水平排列图标、标签和值。图标使用主题色,size 设为 20.sp 是一个适中的大小。标签用灰色小字,起到说明作用。
Spacer 组件占据中间的空白区域,把值推到右边。这是 Flutter 中实现两端对齐的常用技巧,比设置 MainAxisAlignment.spaceBetween 更灵活。
值的显示部分:
Flexible(
child: Text(
value,
style: TextStyle(fontSize: 14.sp),
textAlign: TextAlign.right,
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
),
],
),
);
}
Flexible 包裹右侧文本,当内容过长时可以自动换行或截断,避免溢出。textAlign 设为 right 让文字右对齐,和标签形成视觉上的对称。maxLines 限制最多两行,overflow 设为 ellipsis 超出部分显示省略号。
这种设计既保证了布局的稳定性,又能处理各种长度的内容。比如备注可能很长,用 Flexible 和 maxLines 可以优雅地处理。
操作按钮区域
页面底部放置编辑和删除按钮:
Widget _buildActionButtons(
TransactionModel transaction,
TransactionService service
) {
return Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () => Get.toNamed(
Routes.addTransaction,
arguments: transaction
),
icon: const Icon(Icons.edit),
label: const Text('编辑'),
Row 组件让两个按钮水平排列,Expanded 让它们平分宽度。OutlinedButton.icon 是带图标的描边按钮,视觉上比实心按钮轻量,适合次要操作。
编辑按钮点击后跳转到添加交易页面,通过 arguments 传递当前交易对象。添加交易页面会检测到有传入数据,自动切换到编辑模式。这种复用设计减少了代码重复。
按钮样式配置:
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 12.h),
side: BorderSide(color: _primaryColor),
),
),
),
SizedBox(width: 12.w),
Expanded(
child: OutlinedButton.icon(
onPressed: () => _showDeleteDialog(service, transaction.id),
icon: const Icon(Icons.delete),
label: const Text('删除'),
padding 设置垂直内边距为 12.h,让按钮有足够的点击区域。side 设置边框颜色为主题色。两个按钮之间用 SizedBox 留出 12.w 的间距。
删除按钮的样式使用红色,强调这是危险操作:
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 12.h),
side: BorderSide(color: _expenseColor),
foregroundColor: _expenseColor,
),
),
),
],
);
}
foregroundColor 设为红色,让图标和文字都显示红色。side 也设为红色,保持视觉一致。这种颜色区分让用户在操作前就能意识到删除的危险性。
删除确认对话框
删除操作需要二次确认,防止误操作:
void _showDeleteDialog(TransactionService service, String id) {
Get.dialog(
AlertDialog(
title: const Text('删除记录'),
content: const Text('确定要删除这条记录吗?此操作不可恢复。'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('取消')
),
TextButton(
onPressed: () {
service.deleteTransaction(id);
Get.back();
Get.back();
Get.snackbar('成功', '记录已删除');
},
Get.dialog 显示对话框,AlertDialog 是 Material Design 的标准对话框组件。title 和 content 分别设置标题和内容,内容中说明了删除不可恢复,让用户做出知情的决定。
取消按钮调用 Get.back() 关闭对话框。删除按钮先调用服务删除数据,然后调用两次 Get.back():第一次关闭对话框,第二次返回列表页面。最后显示成功提示。
删除按钮的样式:
child: Text('删除', style: TextStyle(color: _expenseColor)),
),
],
),
);
}
删除按钮使用红色文字,和页面上的删除按钮保持一致。这种颜色强调让用户在最后一刻还能意识到这是危险操作,有机会取消。
编辑功能说明
编辑功能复用了添加交易页面,通过参数区分模式:
// 从详情页跳转到编辑页
Get.toNamed(Routes.addTransaction, arguments: transaction);
// 添加交易页面检测参数
void initState() {
super.initState();
final transaction = Get.arguments as TransactionModel?;
if (transaction != null) {
// 编辑模式,回填数据
_amountController.text = transaction.amount.toString();
_type = transaction.type;
// ... 其他字段
}
}
这种设计的好处是减少代码重复,新增和编辑的 UI 完全一样,只是数据来源不同。用户体验上也更统一,操作流程是一样的。保存时根据是否有原始数据判断是新增还是更新。
错误处理
处理数据异常情况,比如路由参数为空:
Widget build(BuildContext context) {
final args = Get.arguments;
if (args == null || args is! TransactionModel) {
return Scaffold(
appBar: AppBar(title: const Text('交易详情')),
body: const Center(
child: Text('无效的交易数据')
),
);
}
final transaction = args;
// ... 正常渲染
}
这种防御性编程可以避免应用崩溃。如果用户通过深度链接直接访问详情页,或者数据传递出现问题,页面会显示友好的错误提示而不是崩溃。
无障碍支持
为视障用户提供语义化标签:
Semantics(
label: '交易金额 ${transaction.amount} 元,'
'${transaction.type == TransactionType.income ? "收入" : "支出"}',
child: Text(
'${storage.currency}${transaction.amount.toStringAsFixed(2)}',
style: TextStyle(fontSize: 32.sp, fontWeight: FontWeight.bold),
),
)
Semantics 组件为屏幕阅读器提供描述信息。label 属性包含了金额和类型信息,视障用户可以通过语音了解交易详情。这是应用无障碍支持的重要组成部分。
性能优化
详情页的性能优化主要关注以下几点:
// 1. 使用 const 构造函数减少重建
const Divider(height: 1),
const Text('取消'),
// 2. 避免在 build 中做重复计算
final category = categoryService.getCategoryById(transaction.categoryId);
// 只计算一次,后续直接使用 category 变量
// 3. 图标使用 Material Icons,无需额外资源加载
Icon(Icons.edit, color: _primaryColor)
const 的使用可以让 Flutter 在重建时跳过这些不变的 Widget。关联数据的查找在 build 开头完成,避免在子组件中重复查找。Material Icons 是内置的,不需要网络加载。
小结
交易详情页面虽然功能相对简单,但在设计上需要注意以下几点:
- 信息层次分明,金额作为最重要的信息用最大字号显示
- 收入支出使用不同颜色区分,形成视觉习惯
- 删除等危险操作需要二次确认,防止误操作
- 空数据(如备注为空)需要做好条件渲染
- 编辑功能复用添加页面,减少代码重复
- 考虑错误处理和无障碍支持
下一篇我们将实现分类列表页面,让用户可以管理自己的收支分类。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)