gcx/frontend/admin-app/lib/features/finance/presentation/pages/reconciliation_page.dart

587 lines
20 KiB
Dart

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<ReconciliationPage> createState() => _ReconciliationPageState();
}
class _ReconciliationPageState extends State<ReconciliationPage> {
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: () {},
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: () {},
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'),
];