591 lines
20 KiB
Dart
591 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: () {
|
|
// 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'),
|
|
];
|