import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/network/api_exception.dart'; import '../../models/bill.dart'; import '../../models/statistics_response.dart'; import '../../providers/bill_provider.dart'; import '../../providers/bill_refresh.dart'; import '../../providers/budget_provider.dart'; import '../../utils/amount_util.dart'; import '../../utils/bill_grouping.dart'; import '../../utils/date_util.dart'; import '../../widgets/state_views.dart'; import '../bill/bill_edit_page.dart'; import 'widgets/bill_group_card.dart'; import 'widgets/budget_card.dart'; import 'widgets/month_switcher.dart'; /// 首页:月份切换 + 本月汇总 + 预算环 + 按日期分组的账单列表 class HomePage extends ConsumerWidget { const HomePage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final anchor = ref.watch(monthAnchorProvider); final billsAsync = ref.watch(billsProvider(anchor)); final statsAsync = ref.watch(monthlyStatisticsProvider(anchor)); final budgetAsync = ref.watch(budgetProvider); final isCurrentMonth = isSameMonth(anchor, DateTime.now()); return Scaffold( body: Container( width: double.infinity, height: double.infinity, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFFFE5CC), Color(0xFFFFD4A8), Color(0xFFFFC08A)], ), ), child: SafeArea( child: RefreshIndicator( color: const Color(0xFFC8956E), onRefresh: () => _onRefresh(ref), child: ListView( // 内容不足一屏时也要能下拉刷新 physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), children: [ _header(anchor), const SizedBox(height: 14), _summaryCard(anchor, statsAsync), const SizedBox(height: 12), // 后端预算接口只提供当月数据,历史月份不展示,避免串数据 if (isCurrentMonth) BudgetCard(budget: budgetAsync) else _budgetUnavailableTip(), const SizedBox(height: 16), ..._billSection(context, ref, billsAsync), const SizedBox(height: 90), // 底部导航避让 ], ), ), ), ), ); } // ---- 顶部标题 + 月份切换 ---- Widget _header(DateTime anchor) { return Row( children: [ const Text( '我的账单', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Color(0xFF5D4037), ), ), const Spacer(), const MonthSwitcher(), ], ); } // ---- 本月汇总 ---- Widget _summaryCard(DateTime anchor, AsyncValue statsAsync) { final stats = statsAsync.value; return Container( padding: const EdgeInsets.symmetric(vertical: 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), ), child: Row( children: [ _summaryItem('${anchor.month}月收入', stats?.totalIncome, const Color(0xFF19BE6B)), _vDivider(), _summaryItem('${anchor.month}月支出', stats?.totalExpense, const Color(0xFFFA3534)), _vDivider(), _summaryItem('结余', stats?.balance, const Color(0xFF5D4037)), ], ), ); } Widget _summaryItem(String label, double? amount, Color color) { return Expanded( child: Column( children: [ Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF8D6E63))), const SizedBox(height: 6), Text( // 统计未加载完时显示占位,避免整块转圈导致头部抖动 amount == null ? '--' : '¥${formatAmount(amount)}', style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: color), ), ], ), ); } Widget _vDivider() => Container(width: 1, height: 28, color: const Color(0xFFF0E6D8)); Widget _budgetUnavailableTip() { return Container( padding: const EdgeInsets.symmetric(vertical: 14), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.7), borderRadius: BorderRadius.circular(16), ), child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('🎯 ', style: TextStyle(fontSize: 14)), Text( '仅可查看本月预算', style: TextStyle(fontSize: 13, color: Color(0xFF8D6E63)), ), ], ), ); } // ---- 账单列表 ---- List _billSection( BuildContext context, WidgetRef ref, AsyncValue> billsAsync, ) { return [ billsAsync.when( loading: () => const LoadingView(), error: (e, _) => ErrorView( message: asApiException(e).message, onRetry: () => ref.invalidate(billsProvider), ), data: (bills) { if (bills.isEmpty) { return const EmptyView( title: '本月暂无账单记录', tip: '点击底部「记账」添加,或用 OCR 识别小票', ); } return Column( children: groupBillsByDate(bills) .map((group) => BillGroupCard( group: group, onTapBill: (bill) => Navigator.of(context).push( MaterialPageRoute(builder: (_) => BillEditPage(billId: bill.id)), ), )) .toList(), ); }, ), ]; } // ---- 下拉刷新 ---- Future _onRefresh(WidgetRef ref) async { final anchor = ref.read(monthAnchorProvider); invalidateBillDataFromWidget(ref); // 等三个请求结束再收起下拉动画;异常吞掉,由各自的 AsyncValue 渲染错误态 await Future.wait([ _silent(ref.read(billsProvider(anchor).future)), _silent(ref.read(monthlyStatisticsProvider(anchor).future)), _silent(ref.read(budgetProvider.future)), ]); } Future _silent(Future future) => future.then((_) {}, onError: (_, _) {}); }