import 'package:flutter/material.dart'; import '../../../../app/theme/app_colors.dart'; import '../../../../app/theme/app_typography.dart'; import '../../../../app/theme/app_spacing.dart'; /// 对账与结算报表页面 /// /// 支持按日/周/月/季度查看对账数据 /// 包含:汇总卡片、对账明细表、自动对账状态、差异调查、导出功能 class ReconciliationPage extends StatefulWidget { const ReconciliationPage({super.key}); @override State createState() => _ReconciliationPageState(); } class _ReconciliationPageState extends State { String _selectedPeriod = '月'; final _periods = ['日', '周', '月', '季度']; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('对账与结算'), actions: [ IconButton( icon: const Icon(Icons.download_rounded), onPressed: () => _showExportDialog(context), tooltip: '导出报表', ), ], ), body: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Period Selector _buildPeriodSelector(), const SizedBox(height: 20), // Summary Cards _buildSummaryCards(), const SizedBox(height: 24), // Auto-reconciliation Status _buildAutoReconciliationStatus(), const SizedBox(height: 24), // Reconciliation Table _buildReconciliationTable(), const SizedBox(height: 24), // Discrepancy Details _buildDiscrepancySection(), const SizedBox(height: 24), // Export Buttons _buildExportButtons(), ], ), ), ); } // ============================================================ // Period Selector // ============================================================ Widget _buildPeriodSelector() { return Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: AppColors.gray50, borderRadius: BorderRadius.circular(AppSpacing.radiusSm), ), child: Row( children: _periods.map((p) { final isSelected = _selectedPeriod == p; return Expanded( child: GestureDetector( onTap: () => setState(() => _selectedPeriod = p), child: Container( padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( color: isSelected ? AppColors.surface : Colors.transparent, borderRadius: BorderRadius.circular(AppSpacing.radiusSm - 2), boxShadow: isSelected ? AppSpacing.shadowSm : null, ), child: Center( child: Text( p, style: AppTypography.labelMedium.copyWith( color: isSelected ? AppColors.primary : AppColors.textSecondary, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, ), ), ), ), ), ); }).toList(), ), ); } // ============================================================ // Summary Cards // ============================================================ Widget _buildSummaryCards() { final summaries = [ ('应结金额', '\$132,490', AppColors.primary, Icons.account_balance_rounded), ('已结金额', '\$118,200', AppColors.success, Icons.check_circle_rounded), ('待结金额', '\$12,180', AppColors.warning, Icons.schedule_rounded), ('差异金额', '\$2,110', AppColors.error, Icons.error_outline_rounded), ]; return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 12, crossAxisSpacing: 12, childAspectRatio: 1.6, ), itemCount: summaries.length, itemBuilder: (context, index) { final (label, value, color, icon) = summaries[index]; return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(AppSpacing.radiusMd), border: Border.all(color: AppColors.borderLight), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Icon(icon, color: color, size: 16), const SizedBox(width: 6), Text(label, style: AppTypography.bodySmall), ], ), Text( value, style: AppTypography.h2.copyWith(color: color), ), ], ), ); }, ); } // ============================================================ // Auto-reconciliation Status // ============================================================ Widget _buildAutoReconciliationStatus() { const matchRate = 96.8; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(AppSpacing.radiusMd), border: Border.all(color: AppColors.borderLight), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: AppColors.successLight, borderRadius: BorderRadius.circular(AppSpacing.radiusSm), ), child: const Icon(Icons.auto_fix_high_rounded, color: AppColors.success, size: 18), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('自动对账', style: AppTypography.labelMedium), Text('上次运行: 今天 06:00', style: AppTypography.caption), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: AppColors.successLight, borderRadius: BorderRadius.circular(AppSpacing.radiusFull), ), child: Text( '运行中', style: AppTypography.caption.copyWith( color: AppColors.success, fontWeight: FontWeight.w600, ), ), ), ], ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('匹配率', style: AppTypography.bodySmall), Text( '$matchRate%', style: AppTypography.labelMedium.copyWith(color: AppColors.success), ), ], ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: matchRate / 100, backgroundColor: AppColors.gray100, valueColor: const AlwaysStoppedAnimation(AppColors.success), minHeight: 8, ), ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildMiniStat('已匹配', '4,832', AppColors.success), _buildMiniStat('待核查', '158', AppColors.warning), _buildMiniStat('有差异', '12', AppColors.error), ], ), ], ), ); } Widget _buildMiniStat(String label, String value, Color color) { return Column( children: [ Text( value, style: AppTypography.labelLarge.copyWith(color: color), ), const SizedBox(height: 2), Text(label, style: AppTypography.caption), ], ); } // ============================================================ // Reconciliation Table // ============================================================ Widget _buildReconciliationTable() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('对账明细', style: AppTypography.h3), const SizedBox(height: 12), Container( decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(AppSpacing.radiusMd), border: Border.all(color: AppColors.borderLight), ), child: Column( children: [ // Table Header Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: const BoxDecoration( color: AppColors.gray50, borderRadius: BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12), ), ), child: Row( children: [ Expanded(flex: 2, child: Text('期间', style: AppTypography.labelSmall)), Expanded(flex: 2, child: Text('应结', style: AppTypography.labelSmall)), Expanded(flex: 2, child: Text('实结', style: AppTypography.labelSmall)), Expanded(flex: 2, child: Text('差异', style: AppTypography.labelSmall)), Expanded(flex: 2, child: Text('状态', style: AppTypography.labelSmall)), ], ), ), // Table Rows ..._mockReconciliationData.map((row) { final (period, expected, actual, diff, status) = row; return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: const BoxDecoration( border: Border( bottom: BorderSide(color: AppColors.borderLight, width: 0.5), ), ), child: Row( children: [ Expanded( flex: 2, child: Text(period, style: AppTypography.bodySmall.copyWith(color: AppColors.textPrimary)), ), Expanded( flex: 2, child: Text(expected, style: AppTypography.bodySmall.copyWith(color: AppColors.textPrimary)), ), Expanded( flex: 2, child: Text(actual, style: AppTypography.bodySmall.copyWith(color: AppColors.textPrimary)), ), Expanded( flex: 2, child: Text( diff, style: AppTypography.bodySmall.copyWith( color: diff == '\$0' ? AppColors.textTertiary : AppColors.error, ), ), ), Expanded( flex: 2, child: _buildReconciliationStatus(status), ), ], ), ); }), ], ), ), ], ); } Widget _buildReconciliationStatus(String status) { Color color; Color bgColor; switch (status) { case '已对账': color = AppColors.success; bgColor = AppColors.successLight; break; case '有差异': color = AppColors.error; bgColor = AppColors.errorLight; break; default: color = AppColors.warning; bgColor = AppColors.warningLight; } return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(AppSpacing.radiusFull), ), child: Text( status, style: TextStyle(fontSize: 10, color: color, fontWeight: FontWeight.w600), textAlign: TextAlign.center, ), ); } // ============================================================ // Discrepancy Details // ============================================================ Widget _buildDiscrepancySection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('差异调查', style: AppTypography.h3), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: AppColors.errorLight, borderRadius: BorderRadius.circular(AppSpacing.radiusFull), ), child: Text( '3 项待处理', style: AppTypography.caption.copyWith( color: AppColors.error, fontWeight: FontWeight.w600, ), ), ), ], ), const SizedBox(height: 12), ..._mockDiscrepancies.map((d) { final (desc, amount, investigationStatus, date) = d; Color statusColor; switch (investigationStatus) { case '调查中': statusColor = AppColors.warning; break; case '已解决': statusColor = AppColors.success; break; default: statusColor = AppColors.error; } return Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(AppSpacing.radiusMd), border: Border.all(color: AppColors.borderLight), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text(desc, style: AppTypography.labelMedium), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: statusColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(AppSpacing.radiusFull), ), child: Text( investigationStatus, style: AppTypography.caption.copyWith( color: statusColor, fontWeight: FontWeight.w600, ), ), ), ], ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '差异金额: $amount', style: AppTypography.bodySmall.copyWith(color: AppColors.error), ), Text(date, style: AppTypography.caption), ], ), ], ), ); }), ], ); } // ============================================================ // Export Buttons // ============================================================ Widget _buildExportButtons() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(AppSpacing.radiusMd), border: Border.all(color: AppColors.borderLight), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('导出报表', style: AppTypography.h3), const SizedBox(height: 14), Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: () { // TODO: Export reconciliation report as PDF }, icon: const Icon(Icons.picture_as_pdf_rounded, size: 18), label: const Text('导出 PDF'), style: OutlinedButton.styleFrom( minimumSize: const Size(0, 48), foregroundColor: AppColors.error, side: const BorderSide(color: AppColors.error, width: 1), ), ), ), const SizedBox(width: 12), Expanded( child: OutlinedButton.icon( onPressed: () { // TODO: Export reconciliation report as Excel }, icon: const Icon(Icons.table_chart_rounded, size: 18), label: const Text('导出 Excel'), style: OutlinedButton.styleFrom( minimumSize: const Size(0, 48), foregroundColor: AppColors.success, side: const BorderSide(color: AppColors.success, width: 1), ), ), ), ], ), ], ), ); } // ============================================================ // Export Dialog // ============================================================ void _showExportDialog(BuildContext context) { showDialog( context: context, builder: (ctx) => SimpleDialog( title: const Text('导出对账报表'), children: [ SimpleDialogOption( onPressed: () => Navigator.pop(ctx), child: const Row( children: [ Icon(Icons.picture_as_pdf_rounded, color: AppColors.error, size: 20), SizedBox(width: 12), Text('导出 PDF'), ], ), ), SimpleDialogOption( onPressed: () => Navigator.pop(ctx), child: const Row( children: [ Icon(Icons.table_chart_rounded, color: AppColors.success, size: 20), SizedBox(width: 12), Text('导出 Excel'), ], ), ), SimpleDialogOption( onPressed: () => Navigator.pop(ctx), child: const Row( children: [ Icon(Icons.description_rounded, color: AppColors.info, size: 20), SizedBox(width: 12), Text('导出 CSV'), ], ), ), ], ), ); } } // ============================================================ // Mock Data // ============================================================ const _mockReconciliationData = [ ('2026年1月', '\$32,100', '\$32,100', '\$0', '已对账'), ('2025年12月', '\$28,450', '\$28,450', '\$0', '已对账'), ('2025年11月', '\$25,800', '\$24,690', '\$1,110', '有差异'), ('2025年10月', '\$30,200', '\$30,200', '\$0', '已对账'), ('2025年9月', '\$22,940', '\$22,940', '\$0', '已对账'), ('2025年8月', '\$18,600', '\$17,600', '\$1,000', '有差异'), ('2025年7月', '\$15,400', '\$15,400', '\$0', '待对账'), ]; const _mockDiscrepancies = [ ('11月退款差异 - 部分退款未入账', '\$780', '调查中', '2025-12-15'), ('11月手续费差异 - 费率计算偏差', '\$330', '调查中', '2025-12-12'), ('8月核销结算延迟', '\$1,000', '已解决', '2025-09-20'), ];