Flutter for OpenHarmony 个人理财管理App实战 - 账户详情页面

账户详情页面展示单个账户的完整信息,包括余额、收支统计和该账户下的所有交易记录。用户可以在这里全面了解账户的财务状况,也可以进行编辑和删除操作。

功能设计

账户详情页面包含以下功能:

  1. 账户信息卡片(图标、名称、余额、类型)
  2. 账户收支统计(本月收入、支出、净流入)
  3. 该账户的交易记录列表
  4. 按类型筛选交易记录
  5. 编辑和删除账户入口
  6. 下拉刷新数据

这种设计让用户既能看到账户的整体状况,也能查看具体的交易明细。统计信息帮助用户了解账户的资金流动情况。

控制器实现

创建 account_detail_controller.dart 管理页面状态,完整代码如下:

dart 复制代码
import 'package:get/get.dart';
import '../../core/services/transaction_service.dart';
import '../../core/services/account_service.dart';
import '../../core/services/storage_service.dart';
import '../../data/models/account_model.dart';
import '../../data/models/transaction_model.dart';

class AccountDetailController extends GetxController {
  final _transactionService = Get.find<TransactionService>();
  final _accountService = Get.find<AccountService>();
  final _storage = Get.find<StorageService>();

  late AccountModel account;
  final transactions = <TransactionModel>[].obs;

这段代码导入了控制器所需的所有依赖。TransactionService 负责获取和管理交易记录,AccountService 负责账户信息的获取和更新,StorageService 提供全局设置如货币符号。控制器继承自 GetxController,这是 GetX 状态管理的基础类,提供了生命周期管理和依赖注入功能。通过 Get.find 获取服务实例,这些服务在应用启动时已经通过 InitialBinding 注册,确保在整个应用生命周期内都是单例。定义了多个响应式状态变量:transactions 存储交易记录列表,monthlyIncome 和 monthlyExpense 存储本月收支统计,selectedFilter 存储当前的筛选条件。使用 .obs 让这些变量变成响应式的,当它们的值改变时,所有使用 Obx 包裹的 UI 组件都会自动重建。

dart 复制代码
  final monthlyIncome = 0.0.obs;
  final monthlyExpense = 0.0.obs;
  final selectedFilter = 'all'.obs;

  String get currency => _storage.currency;
  double get monthlyNet => monthlyIncome.value - monthlyExpense.value;

  @override
  void onInit() {
    super.onInit();
    account = Get.arguments as AccountModel;
    _loadData();
  }

控制器定义了两个计算属性(getter):currency 直接返回 StorageService 中的货币符号,monthlyNet 计算本月净流入(收入减去支出)。计算属性的优势是它们会根据依赖的数据自动更新,不需要手动维护。onInit 是 GetX 的生命周期方法,在控制器创建后立即调用,类似于 StatefulWidget 的 initState。Get.arguments 获取从账户列表页传递过来的账户对象,这是页面间传递数据的标准方式。_loadData 方法负责加载该账户的所有数据,包括交易记录和本月统计。将数据加载逻辑封装在单独的方法中,便于在多个地方复用,比如初始化时调用一次,下拉刷新时再调用一次。

dart 复制代码
  void _loadData() {
    var list = _transactionService.getTransactionsByAccount(account.id);
    
    if (selectedFilter.value == 'income') {
      list = list.where((t) => t.type == TransactionType.income).toList();
    } else if (selectedFilter.value == 'expense') {
      list = list.where((t) => t.type == TransactionType.expense).toList();
    }

    list.sort((a, b) => b.date.compareTo(a.date));
    transactions.value = list;

_loadData 方法首先从 TransactionService 获取该账户的所有交易记录。根据 selectedFilter 的值筛选不同类型的交易:'income' 只显示收入,'expense' 只显示支出,'all' 显示全部。where 方法返回一个 Iterable,需要调用 toList() 转换为 List。sort 方法按日期倒序排列,让最新的交易显示在前面。compareTo 返回负数表示 a < b,返回正数表示 a > b,这里用 b.date.compareTo(a.date) 实现倒序。将筛选和排序后的列表赋值给 transactions.value,由于 transactions 是响应式变量,这个赋值会触发所有监听它的 UI 组件重建。这种数据驱动的方式让状态管理变得简单直观。

dart 复制代码
    final now = DateTime.now();
    final monthStart = DateTime(now.year, now.month, 1);
    final monthTransactions = _transactionService
        .getTransactionsByAccount(account.id)
        .where((t) => t.date.isAfter(monthStart));

    monthlyIncome.value = monthTransactions
        .where((t) => t.type == TransactionType.income)
        .fold(0.0, (sum, t) => sum + t.amount);

    monthlyExpense.value = monthTransactions
        .where((t) => t.type == TransactionType.expense)
        .fold(0.0, (sum, t) => sum + t.amount);
  }

本月统计的计算逻辑:首先获取当前时间,然后创建本月第一天的 DateTime 对象(年、月、日=1)。从 TransactionService 获取该账户的所有交易记录,使用 where 筛选出日期在本月第一天之后的交易。isAfter 方法比较两个 DateTime,返回 true 表示当前交易发生在本月。然后分别计算收入和支出:再次使用 where 筛选出收入类型的交易,使用 fold 方法累加金额。fold 的第一个参数是初始值 0.0,第二个参数是累加函数,sum 是累加器,t 是当前交易,返回 sum + t.amount。这种函数式编程的方式简洁优雅,避免了手动循环和临时变量。最后将计算结果赋值给响应式变量,UI 会自动更新显示。

dart 复制代码
  void setFilter(String filter) {
    selectedFilter.value = filter;
    _loadData();
  }

  void refresh() {
    final updated = _accountService.getAccountById(account.id);
    if (updated != null) {
      account = updated;
    }
    _loadData();
  }

  void deleteAccount() {
    _accountService.deleteAccount(account.id);
    Get.back();
    Get.snackbar('已删除', '账户"${account.name}"已删除');
  }
}

控制器提供了三个公共方法供页面调用。setFilter 方法更新筛选条件并重新加载数据,当用户点击筛选标签时调用。refresh 方法用于刷新页面数据,首先尝试从 AccountService 重新获取账户信息(因为账户可能在编辑页被修改了),如果获取成功就更新 account 对象,然后调用 _loadData 刷新交易列表和统计数据。这个方法在两个场景下使用:用户下拉刷新页面时,以及从编辑页返回时。deleteAccount 方法执行删除操作,调用 AccountService 的 deleteAccount 方法删除账户,然后调用 Get.back() 返回上一页(账户列表页),最后显示成功提示。这三个方法体现了控制器的职责:响应用户操作,更新数据状态,协调页面跳转。

页面实现

创建 account_detail_page.dart,完整代码如下:

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/category_service.dart';
import '../../data/models/transaction_model.dart';
import '../../routes/app_pages.dart';
import 'account_detail_controller.dart';

const _primaryColor = Color(0xFF2E7D32);
const _incomeColor = Color(0xFF4CAF50);
const _expenseColor = Color(0xFFE53935);
const _textSecondary = Color(0xFF757575);

页面文件导入了所有必要的依赖。flutter/material.dart 提供 Flutter 的核心 UI 组件,flutter_screenutil 提供屏幕适配功能,让 UI 在不同尺寸的设备上保持一致的视觉效果。intl 包的 DateFormat 用于格式化日期和时间,支持多种格式和本地化。CategoryService 用于根据 categoryId 获取分类信息,在显示交易记录时需要显示分类的图标、名称和颜色。四个颜色常量定义了页面的配色方案,和其他页面保持一致,形成统一的视觉语言。这种一致性让用户在不同页面间切换时不会感到困惑,降低了认知负担。

dart 复制代码
class AccountDetailPage extends StatelessWidget {
  const AccountDetailPage({super.key});

  @override
  Widget build(BuildContext context) {
    final controller = Get.put(AccountDetailController());

    return Scaffold(
      appBar: AppBar(
        title: Text(controller.account.name),
        centerTitle: true,
        actions: [
          IconButton(
            icon: const Icon(Icons.edit),
            onPressed: () async {
              await Get.toNamed(Routes.accountEdit, arguments: controller.account);
              controller.refresh();
            },

页面使用 StatelessWidget,所有状态由 AccountDetailController 管理。Get.put 注册控制器,当页面销毁时控制器也会自动释放,避免内存泄漏。AppBar 的 title 显示账户名称,centerTitle 让标题居中显示,这种设计让用户一眼就知道当前查看的是哪个账户。编辑按钮使用 async/await 处理页面跳转,await 会等待编辑页面关闭后才继续执行,然后调用 controller.refresh() 刷新数据。这样可以确保如果用户在编辑页修改了账户信息,详情页能够显示最新的数据。tooltip 属性提供长按提示,增强可访问性,特别是对于视障用户使用屏幕阅读器时很有帮助。

dart 复制代码
            tooltip: '编辑',
          ),
          PopupMenuButton<String>(
            onSelected: (value) {
              if (value == 'delete') {
                _confirmDelete(controller);
              }
            },
            itemBuilder: (context) => [
              const PopupMenuItem(
                value: 'delete',
                child: Row(
                  children: [
                    Icon(Icons.delete, color: Colors.red),
                    SizedBox(width: 8),
                    Text('删除', style: TextStyle(color: Colors.red)),
                  ],
                ),
              ),
            ],
          ),
        ],
      ),

PopupMenuButton 提供了一个弹出菜单,点击后显示更多操作选项。目前只有一个删除选项,但这种设计为将来添加更多选项预留了空间。onSelected 回调接收用户选择的菜单项的值,这里判断如果是 'delete' 就调用 _confirmDelete 显示确认对话框。PopupMenuItem 定义菜单项,value 是菜单项的标识,child 是显示的内容。删除选项使用红色图标和文字,在视觉上强调这是危险操作。Row 水平排列图标和文字,SizedBox 添加 8 像素的间距。这种视觉设计遵循了"防错"原则,通过颜色提醒用户这是一个需要谨慎操作的功能。

dart 复制代码
      body: RefreshIndicator(
        onRefresh: () async => controller.refresh(),
        child: SingleChildScrollView(
          physics: const AlwaysScrollableScrollPhysics(),
          padding: EdgeInsets.all(16.w),
          child: Column(
            children: [
              _buildAccountCard(controller),
              SizedBox(height: 16.h),
              _buildStatsCard(controller),
              SizedBox(height: 16.h),
              _buildFilterBar(controller),
              SizedBox(height: 8.h),
              _buildTransactionList(controller),
            ],
          ),
        ),
      ),
    );
  }

RefreshIndicator 实现下拉刷新功能,这是移动应用的标准交互模式。onRefresh 回调返回 Future,当用户下拉时会调用 controller.refresh() 刷新数据。SingleChildScrollView 让整个页面内容可以垂直滚动,physics 设为 AlwaysScrollableScrollPhysics 确保即使内容不足一屏也能触发下拉刷新。padding 在页面四周添加 16.w 的边距,让内容不会紧贴屏幕边缘。Column 将页面分为四个主要区域:账户信息卡片展示账户的基本信息和余额,统计卡片显示本月的收支情况,筛选栏让用户可以按类型查看交易,交易列表显示该账户的所有交易记录。SizedBox 在各个区域之间添加间距,让页面布局更加舒适。

dart 复制代码
  Widget _buildAccountCard(AccountDetailController controller) {
    final account = controller.account;
    
    return Card(
      elevation: 4,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.r)),
      child: Container(
        width: double.infinity,
        padding: EdgeInsets.all(24.w),
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: [account.color, account.color.withOpacity(0.7)],
            begin: Alignment.topLeft,
            end: Alignment.bottomRight,
          ),
          borderRadius: BorderRadius.circular(16.r),
        ),

账户信息卡片是页面的视觉焦点,使用账户的颜色作为背景,让每个账户都有独特的视觉标识。Card 的 elevation 设为 4 增加阴影效果,让卡片有浮起的感觉。shape 设置圆角,RoundedRectangleBorder 配合 BorderRadius.circular(16.r) 创建统一的圆角效果。Container 的 decoration 使用 LinearGradient 创建渐变背景,从账户颜色渐变到 70% 透明度的账户颜色,begin 和 end 定义渐变方向从左上到右下。这种渐变效果让卡片更有层次感和现代感。width 设为 double.infinity 让卡片占满父容器的宽度,padding 设为 24.w 在内容四周留出较大的空间,让卡片看起来更加舒适大气。

dart 复制代码
        child: Column(
          children: [
            Container(
              padding: EdgeInsets.all(16.w),
              decoration: BoxDecoration(
                color: Colors.white.withOpacity(0.2),
                shape: BoxShape.circle,
              ),
              child: Icon(account.icon, size: 40.sp, color: Colors.white),
            ),
            SizedBox(height: 16.h),
            Text(
              account.name,
              style: TextStyle(color: Colors.white, fontSize: 20.sp, fontWeight: FontWeight.w600),
            ),

账户图标放在一个半透明白色的圆形容器中,形成视觉焦点。Container 的 padding 设为 16.w 让图标周围有足够的空间,decoration 使用 BoxShape.circle 创建圆形,背景色是白色的 20% 透明度,在彩色背景上形成柔和的对比。Icon 使用账户的图标,size 设为 40.sp 足够大能引起注意,白色图标在彩色背景上清晰可见。账户名称使用 20.sp 的白色文字,fontWeight 设为 w600 稍微加粗,让名称更加醒目。这种大小和粗细的组合在彩色背景上有很好的可读性。

dart 复制代码
            SizedBox(height: 4.h),
            Container(
              padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h),
              decoration: BoxDecoration(
                color: Colors.white.withOpacity(0.2),
                borderRadius: BorderRadius.circular(12.r),
              ),
              child: Text(
                _getAccountTypeName(account.type),
                style: TextStyle(color: Colors.white70, fontSize: 12.sp),
              ),
            ),
            SizedBox(height: 16.h),
            Text('当前余额', style: TextStyle(color: Colors.white70, fontSize: 14.sp)),
            SizedBox(height: 4.h),

账户类型标签使用半透明白色圆角矩形作为背景,形成标签效果。Container 的 padding 设置水平 12.w 和垂直 4.h 的内边距,让文字周围有适当的空间。decoration 使用 BoxDecoration 设置背景色和圆角,背景色是白色的 20% 透明度,borderRadius 设为 12.r 创建圆角效果。类型名称使用半透明白色(white70)和 12.sp 的小字号,不会太抢眼但清晰可见。"当前余额"标签使用相同的半透明白色,提示下方数字的含义。这种层次分明的设计让用户能够快速识别不同的信息。

dart 复制代码
            Text(
              '${controller.currency}${account.balance.toStringAsFixed(2)}',
              style: TextStyle(
                color: Colors.white,
                fontSize: 36.sp,
                fontWeight: FontWeight.bold,
              ),
            ),
          ],
        ),
      ),
    );
  }

  String _getAccountTypeName(AccountType type) {
    switch (type) {
      case AccountType.cash: return '现金账户';
      case AccountType.bank: return '银行卡';
      case AccountType.creditCard: return '信用卡';
      case AccountType.alipay: return '支付宝';
      case AccountType.wechat: return '微信';
      case AccountType.investment: return '投资账户';
      case AccountType.other: return '其他账户';
    }
  }

余额是卡片上最重要的信息,使用 36.sp 的超大字号和 bold 粗体显示,在整个卡片中最为醒目。纯白色在彩色背景上有极高的对比度,确保在任何账户颜色下都清晰可读。toStringAsFixed(2) 保留两位小数,让金额显示更加规范专业。_getAccountTypeName 方法使用 switch 语句根据账户类型枚举返回对应的中文名称。使用枚举而不是字符串有几个优势:避免拼写错误,IDE 可以提供代码补全和类型检查,重构时更安全。这个方法为每种账户类型提供了友好的中文显示名称,让用户能够快速识别账户类型。

dart 复制代码
  Widget _buildStatsCard(AccountDetailController controller) {
    return Card(
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.r)),
      child: Padding(
        padding: EdgeInsets.all(16.w),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('本月统计', style: TextStyle(fontSize: 14.sp, color: _textSecondary)),
            SizedBox(height: 16.h),
            Obx(() => Row(
              children: [
                Expanded(
                  child: _buildStatItem(
                    '收入', controller.monthlyIncome.value, _incomeColor,
                    Icons.arrow_downward, controller.currency,
                  ),
                ),

统计卡片展示本月的收支情况,帮助用户了解账户的资金流动。Card 设置圆角,Padding 添加内边距。"本月统计"标题使用灰色小字,和下方的数据形成层次对比。crossAxisAlignment 设为 start 让内容左对齐。Obx 包裹整个 Row,当本月收支数据变化时自动更新显示。Row 将三个统计项水平排列,每个统计项用 Expanded 包裹,让它们平分可用宽度。这种布局确保三个统计项始终保持相同的宽度,视觉上更加整齐统一。

dart 复制代码
                Container(width: 1, height: 50.h, color: Colors.grey[200]),
                Expanded(
                  child: _buildStatItem(
                    '支出', controller.monthlyExpense.value, _expenseColor,
                    Icons.arrow_upward, controller.currency,
                  ),
                ),
                Container(width: 1, height: 50.h, color: Colors.grey[200]),
                Expanded(
                  child: _buildStatItem(
                    '净流入', controller.monthlyNet,
                    controller.monthlyNet >= 0 ? _incomeColor : _expenseColor,
                    controller.monthlyNet >= 0 ? Icons.trending_up : Icons.trending_down,
                    controller.currency,
                  ),
                ),
              ],
            )),
          ],
        ),
      ),
    );
  }

三个统计项之间使用 Container 作为分隔线,width 设为 1 像素,height 设为 50.h,颜色使用浅灰色(grey[200]),视觉上分隔不同的统计项。收入统计使用绿色和向下箭头图标,向下箭头表示资金流入账户。支出统计使用红色和向上箭头图标,向上箭头表示资金流出账户。净流入统计比较特殊,它的颜色和图标根据正负值动态变化:正数(收入大于支出)使用绿色和向上趋势图标,负数(支出大于收入)使用红色和向下趋势图标。这种视觉反馈让用户一眼就能看出账户的资金流动方向和趋势,无需仔细阅读数字就能快速理解账户的财务状况。

dart 复制代码
  Widget _buildStatItem(String label, double value, Color color, IconData icon, String currency) {
    return Column(
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(icon, size: 14.sp, color: color),
            SizedBox(width: 4.w),
            Text(label, style: TextStyle(fontSize: 12.sp, color: _textSecondary)),
          ],
        ),
        SizedBox(height: 8.h),
        Text(
          '$currency${value.abs().toStringAsFixed(2)}',
          style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold, color: color),
        ),
      ],
    );
  }

_buildStatItem 方法构建单个统计项,采用垂直布局。顶部是标签行,使用 Row 水平排列图标和文字,mainAxisAlignment 设为 center 让它们居中显示。图标使用小尺寸(14.sp)和对应的颜色,和文字大小协调。标签文字使用灰色小字(12.sp),不会太抢眼。底部是金额数字,使用 16.sp 的字号和 bold 粗体,颜色和图标保持一致。金额使用 abs() 取绝对值,因为净流入可能是负数,但我们只显示数值大小,正负通过颜色来表示。toStringAsFixed(2) 保留两位小数,让金额显示更加规范。这种设计让每个统计项的信息层次分明,用户可以快速识别和理解。

dart 复制代码
  Widget _buildFilterBar(AccountDetailController controller) {
    return Obx(() => Row(
      children: [
        _buildFilterChip('全部', 'all', controller),
        SizedBox(width: 8.w),
        _buildFilterChip('收入', 'income', controller),
        SizedBox(width: 8.w),
        _buildFilterChip('支出', 'expense', controller),
        const Spacer(),
        Text(
          '共 ${controller.transactions.length} 笔',
          style: TextStyle(fontSize: 12.sp, color: _textSecondary),
        ),
      ],
    ));
  }

筛选栏让用户可以按交易类型查看记录,提供了更灵活的数据查看方式。Obx 包裹整个 Row,当筛选条件或交易数量变化时自动更新显示。三个筛选标签分别是"全部"、"收入"、"支出",SizedBox 在它们之间添加 8.w 的间距。Spacer 是一个弹性空间组件,它会占据所有剩余空间,把交易数量统计推到行的最右边。交易数量使用灰色小字显示,格式为"共 X 笔",让用户知道当前筛选条件下有多少条交易记录。这种设计既提供了筛选功能,又提供了数据统计,帮助用户更好地了解账户的使用情况。

dart 复制代码
  Widget _buildFilterChip(String label, String value, AccountDetailController controller) {
    final isSelected = controller.selectedFilter.value == value;
    
    return GestureDetector(
      onTap: () => controller.setFilter(value),
      child: Container(
        padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
        decoration: BoxDecoration(
          color: isSelected ? controller.account.color : Colors.grey[100],
          borderRadius: BorderRadius.circular(20.r),
        ),
        child: Text(
          label,
          style: TextStyle(
            fontSize: 13.sp,
            color: isSelected ? Colors.white : _textSecondary,
            fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
          ),
        ),
      ),
    );
  }

_buildFilterChip 方法构建单个筛选标签,采用药丸形状的设计。isSelected 判断当前标签是否被选中,通过比较 controller.selectedFilter.value 和标签的 value。GestureDetector 处理点击事件,点击时调用 controller.setFilter(value) 更新筛选条件。Container 的 padding 设置水平和垂直内边距,让文字周围有适当的空间。decoration 根据选中状态设置不同的背景色:选中时使用账户颜色,和顶部的账户卡片形成视觉关联;未选中时使用浅灰色(grey[100])。borderRadius 设为 20.r 创建圆角药丸形状。文字样式也根据选中状态变化:选中时使用白色粗体文字,未选中时使用灰色普通文字。这种明显的样式变化让用户能够清楚地知道当前选中的是哪个筛选条件。

dart 复制代码
  Widget _buildTransactionList(AccountDetailController controller) {
    return Obx(() {
      if (controller.transactions.isEmpty) {
        return Card(
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.r)),
          child: Padding(
            padding: EdgeInsets.all(32.w),
            child: Column(
              children: [
                Icon(Icons.receipt_long, size: 64.sp, color: Colors.grey[300]),
                SizedBox(height: 16.h),
                Text('暂无交易记录', style: TextStyle(color: _textSecondary, fontSize: 16.sp)),
                SizedBox(height: 8.h),
                Text(
                  '使用此账户记录第一笔交易吧',
                  style: TextStyle(color: _textSecondary.withOpacity(0.7), fontSize: 12.sp),
                ),
              ],
            ),
          ),
        );
      }

交易记录列表首先检查是否为空。空状态显示大尺寸浅灰色图标和提示文字,引导用户添加交易。

dart 复制代码
      final grouped = _groupByDate(controller.transactions);

      return Card(
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.r)),
        child: Column(
          children: grouped.entries.map((entry) {
            return Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                _buildDateHeader(entry.key, entry.value, controller.currency),
                ...entry.value.map((t) => _buildTransactionItem(t, controller)),
              ],
            );
          }).toList(),
        ),
      );
    });
  }

_groupByDate 按日期分组交易记录。遍历每个分组,为每个日期创建标题和交易列表。展开运算符 ... 将交易项列表展开。

dart 复制代码
  Map<String, List<TransactionModel>> _groupByDate(List<TransactionModel> transactions) {
    final grouped = <String, List<TransactionModel>>{};
    for (var t in transactions) {
      final key = DateFormat('yyyy-MM-dd').format(t.date);
      grouped.putIfAbsent(key, () => []).add(t);
    }
    return grouped;
  }

遍历交易列表,用日期字符串作为 key 分组。putIfAbsent 在 key 不存在时创建空列表,然后添加交易记录。

dart 复制代码
  Widget _buildDateHeader(String date, List<TransactionModel> transactions, String currency) {
    final dayIncome = transactions
        .where((t) => t.type == TransactionType.income)
        .fold(0.0, (sum, t) => sum + t.amount);
    final dayExpense = transactions
        .where((t) => t.type == TransactionType.expense)
        .fold(0.0, (sum, t) => sum + t.amount);

    final dateTime = DateFormat('yyyy-MM-dd').parse(date);
    final isToday = DateFormat('yyyy-MM-dd').format(DateTime.now()) == date;
    final displayDate = isToday ? '今天' : DateFormat('MM月dd日 E', 'zh_CN').format(dateTime);

日期标题计算当天的收入和支出总额。判断是否是今天,今天显示"今天"而不是日期。

dart 复制代码
    return Container(
      padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
      decoration: BoxDecoration(
        color: Colors.grey[50],
        border: Border(bottom: BorderSide(color: Colors.grey[200]!)),
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(
            displayDate,
            style: TextStyle(fontSize: 13.sp, color: _textSecondary, fontWeight: FontWeight.w500),
          ),
          Row(
            children: [
              if (dayIncome > 0)
                Text('收:$currency${dayIncome.toStringAsFixed(0)}',
                    style: TextStyle(fontSize: 12.sp, color: _incomeColor)),
              if (dayIncome > 0 && dayExpense > 0) SizedBox(width: 8.w),
              if (dayExpense > 0)
                Text('支:$currency${dayExpense.toStringAsFixed(0)}',
                    style: TextStyle(fontSize: 12.sp, color: _expenseColor)),
            ],
          ),
        ],
      ),
    );
  }

Container 设置浅灰色背景和底部边框。Row 让日期和收支统计左右分布。收入和支出只在有值时显示,用 if 条件渲染。

dart 复制代码
  Widget _buildTransactionItem(TransactionModel t, AccountDetailController controller) {
    final categoryService = Get.find<CategoryService>();
    final category = categoryService.getCategoryById(t.categoryId);

    return ListTile(
      leading: CircleAvatar(
        radius: 20.r,
        backgroundColor: (category?.color ?? Colors.grey).withOpacity(0.2),
        child: Icon(category?.icon ?? Icons.help, color: category?.color ?? Colors.grey, size: 20.sp),
      ),
      title: Text(category?.name ?? '未知分类', style: TextStyle(fontSize: 14.sp)),
      subtitle: Text(
        DateFormat('HH:mm').format(t.date) + 
          (t.note != null && t.note!.isNotEmpty ? ' · ${t.note}' : ''),
        style: TextStyle(fontSize: 12.sp, color: _textSecondary),
      ),
      trailing: Text(
        '${t.type == TransactionType.income ? '+' : '-'}'
        '${controller.currency}${t.amount.toStringAsFixed(2)}',
        style: TextStyle(
          color: t.type == TransactionType.income ? _incomeColor : _expenseColor,
          fontWeight: FontWeight.w600,
          fontSize: 14.sp,
        ),
      ),
      onTap: () => Get.toNamed(Routes.transactionDetail, arguments: t),
    );
  }

单条交易记录显示分类图标、名称、时间、备注和金额。收入加 + 号用绿色,支出加 - 号用红色。点击跳转到交易详情页面。

dart 复制代码
  void _confirmDelete(AccountDetailController controller) {
    Get.dialog(
      AlertDialog(
        title: const Text('确认删除'),
        content: Text(
          '确定要删除账户"${controller.account.name}"吗?\n\n'
          '删除后,该账户下的所有交易记录也将被删除,此操作不可撤销。',
        ),
        actions: [
          TextButton(onPressed: () => Get.back(), child: const Text('取消')),
          TextButton(
            onPressed: () {
              Get.back();
              controller.deleteAccount();
            },
            style: TextButton.styleFrom(foregroundColor: Colors.red),
            child: const Text('删除'),
          ),
        ],
      ),
    );
  }
}

删除确认对话框说明删除后果,包括关联的交易记录也会被删除。确认后先关闭对话框,再调用控制器的删除方法。

设计要点总结

账户详情页面的设计考虑了以下几点:

  1. 账户卡片使用账户颜色,形成视觉关联,每个账户都有独特的外观
  2. 本月统计让用户快速了解账户收支情况,不需要手动计算
  3. 筛选功能方便查看特定类型的交易,减少信息干扰
  4. 交易记录按日期分组,更易浏览,每天的收支一目了然
  5. 支持下拉刷新更新数据,保持信息最新
  6. 删除操作有确认提示,防止误操作

小结

账户详情页面让用户可以查看单个账户的完整信息和交易历史。通过收支统计和筛选功能,帮助用户更好地了解账户的财务状况。渐变色卡片和分组列表让页面既美观又实用。

下一篇将实现账户编辑页面,支持添加和修改账户信息。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
计算机学姐2 小时前
基于SpringBoot的电影点评交流平台【协同过滤推荐算法+数据可视化统计】
java·vue.js·spring boot·spring·信息可视化·echarts·推荐算法
2601_949857432 小时前
Flutter for OpenHarmony Web开发助手App实战:快捷键参考
前端·flutter
福大大架构师每日一题2 小时前
ComfyUI v0.11.1正式发布:新增开发者专属节点支持、API节点强化、Python 3.14兼容性更新等全方位优化!
开发语言·python
wangdaoyin20102 小时前
若依vue2前后端分离集成flowable
开发语言·前端·javascript
Filotimo_2 小时前
Tomcat的概念
java·tomcat
天天进步20152 小时前
AI Agent 与流式处理:Motia 在生成式 AI 时代的后端范式
javascript
心柠2 小时前
vue3相关知识总结
前端·javascript·vue.js
索荣荣3 小时前
Java Session 全面指南:原理、应用与实践(含 Spring Boot 实战)
java·spring boot·后端