import 'package:flutter/material.dart'; import '../../../../app/theme/app_colors.dart'; import '../../../../app/i18n/app_localizations.dart'; import '../../../../core/services/issuer_finance_service.dart'; /// 财务管理页面 /// /// 法币展示,不暴露链上稳定币细节 /// 包含:销售收入、Breakage收入、手续费、保证金、冻结款、提现、对账报表 class FinancePage extends StatelessWidget { const FinancePage({super.key}); @override Widget build(BuildContext context) { return DefaultTabController( length: 3, child: Scaffold( appBar: AppBar( title: Text(context.t('finance_title')), actions: [ IconButton( icon: const Icon(Icons.download_rounded), onPressed: () => _showExportDialog(context), ), ], bottom: TabBar( tabs: [ Tab(text: context.t('finance_tab_overview')), Tab(text: context.t('finance_tab_transactions')), Tab(text: context.t('finance_tab_reconciliation')), ], ), ), body: const TabBarView( children: [ _OverviewTab(), _TransactionDetailTab(), _ReconciliationTab(), ], ), ), ); } void _showExportDialog(BuildContext context) { showDialog( context: context, builder: (ctx) => SimpleDialog( title: Text(context.t('finance_export_title')), children: [ SimpleDialogOption(onPressed: () => Navigator.pop(ctx), child: Text(context.t('finance_export_csv'))), SimpleDialogOption(onPressed: () => Navigator.pop(ctx), child: Text(context.t('finance_export_excel'))), SimpleDialogOption(onPressed: () => Navigator.pop(ctx), child: Text(context.t('finance_export_pdf'))), ], ), ); } } class _OverviewTab extends StatefulWidget { const _OverviewTab(); @override State<_OverviewTab> createState() => _OverviewTabState(); } class _OverviewTabState extends State<_OverviewTab> { final _financeService = IssuerFinanceService(); bool _isLoading = true; String? _error; FinanceBalance? _balance; FinanceStats? _stats; @override void initState() { super.initState(); _loadData(); } Future _loadData() async { setState(() { _isLoading = true; _error = null; }); try { final results = await Future.wait([ _financeService.getBalance(), _financeService.getStats(), ]); if (!mounted) return; setState(() { _balance = results[0] as FinanceBalance; _stats = results[1] as FinanceStats; _isLoading = false; }); } catch (e) { debugPrint('[FinancePage] loadData error: $e'); if (!mounted) return; setState(() { _isLoading = false; _error = e.toString(); }); } } Future _handleWithdraw() async { if (_balance == null || _balance!.withdrawable <= 0) return; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(context.t('finance_confirm_withdraw')), content: Text('${context.t('finance_withdraw_amount')}: \$${_balance!.withdrawable.toStringAsFixed(2)}'), actions: [ TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(context.t('cancel'))), ElevatedButton(onPressed: () => Navigator.pop(ctx, true), child: Text(context.t('confirm'))), ], ), ); if (confirmed != true) return; try { await _financeService.withdraw(_balance!.withdrawable); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.t('finance_withdraw_submitted')), backgroundColor: AppColors.success), ); _loadData(); } catch (e) { debugPrint('[FinancePage] withdraw error: $e'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('${context.t('finance_withdraw_failed')}: $e'), backgroundColor: AppColors.error), ); } } @override Widget build(BuildContext context) { if (_isLoading) { return const Center(child: CircularProgressIndicator()); } if (_error != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, size: 48, color: AppColors.textTertiary), const SizedBox(height: 16), Text(_error!, style: const TextStyle(color: AppColors.textSecondary)), const SizedBox(height: 16), ElevatedButton(onPressed: _loadData, child: Text(context.t('retry'))), ], ), ); } return RefreshIndicator( onRefresh: _loadData, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(20), child: Column( children: [ // Balance Card Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: AppColors.primaryGradient, borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(context.t('finance_withdrawable'), style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: 0.7))), const SizedBox(height: 4), Text( '\$${_balance?.withdrawable.toStringAsFixed(2) ?? '0.00'}', style: const TextStyle(fontSize: 32, fontWeight: FontWeight.w700, color: Colors.white), ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _handleWithdraw, style: ElevatedButton.styleFrom( backgroundColor: Colors.white, foregroundColor: AppColors.primary, ), child: Text(context.t('finance_withdraw')), ), ), ], ), ), const SizedBox(height: 20), // Financial Stats _buildFinanceStatsGrid(context), const SizedBox(height: 20), // Guarantee Fund _buildGuaranteeFundCard(context), const SizedBox(height: 20), // Revenue Trend Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.borderLight), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(context.t('finance_revenue_trend'), style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), const SizedBox(height: 16), Container( height: 160, decoration: BoxDecoration( color: AppColors.gray50, borderRadius: BorderRadius.circular(8), ), child: Center(child: Text(context.t('finance_revenue_chart'), style: const TextStyle(color: AppColors.textTertiary))), ), ], ), ), ], ), ), ); } Widget _buildFinanceStatsGrid(BuildContext context) { final s = _stats; final statItems = [ (context.t('finance_sales_income'), '\$${s?.salesAmount.toStringAsFixed(0) ?? '0'}', AppColors.success), (context.t('finance_breakage_income'), '\$${s?.breakageIncome.toStringAsFixed(0) ?? '0'}', AppColors.info), (context.t('finance_platform_fee'), '-\$${s?.platformFee.toStringAsFixed(0) ?? '0'}', AppColors.error), (context.t('finance_pending_settlement'), '\$${s?.pendingSettlement.toStringAsFixed(0) ?? '0'}', AppColors.warning), (context.t('finance_withdrawn'), '\$${s?.withdrawnAmount.toStringAsFixed(0) ?? '0'}', AppColors.textSecondary), (context.t('finance_total_income'), '\$${s?.totalRevenue.toStringAsFixed(0) ?? '0'}', AppColors.primary), ]; return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 12, crossAxisSpacing: 12, childAspectRatio: 2, ), itemCount: statItems.length, itemBuilder: (context, index) { final (label, value, color) = statItems[index]; return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.borderLight), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)), Text(value, style: TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: color)), ], ), ); }, ); } Widget _buildGuaranteeFundCard(BuildContext context) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.borderLight), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.shield_rounded, color: AppColors.info, size: 20), const SizedBox(width: 8), Text(context.t('finance_guarantee_title'), style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), ], ), const SizedBox(height: 16), _buildRow(context.t('finance_guarantee_deposit'), '\$${_balance?.pending.toStringAsFixed(0) ?? '0'}'), _buildRow(context.t('finance_frozen_sales'), '--'), _buildRow(context.t('finance_frozen_ratio'), '--'), const SizedBox(height: 12), SwitchListTile( title: Text(context.t('finance_auto_freeze'), style: const TextStyle(fontSize: 14)), subtitle: Text(context.t('finance_auto_freeze_desc')), value: true, onChanged: (_) { // TODO: Toggle auto-freeze setting }, activeColor: AppColors.primary, contentPadding: EdgeInsets.zero, ), ], ), ); } Widget _buildRow(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: const TextStyle(fontSize: 13, color: AppColors.textSecondary)), Text(value, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), ], ), ); } } class _TransactionDetailTab extends StatefulWidget { const _TransactionDetailTab(); @override State<_TransactionDetailTab> createState() => _TransactionDetailTabState(); } class _TransactionDetailTabState extends State<_TransactionDetailTab> { final _financeService = IssuerFinanceService(); bool _isLoading = true; String? _error; List _transactions = []; @override void initState() { super.initState(); _loadTransactions(); } Future _loadTransactions() async { setState(() { _isLoading = true; _error = null; }); try { final result = await _financeService.getTransactions(); if (!mounted) return; setState(() { _transactions = result.items; _isLoading = false; }); } catch (e) { debugPrint('[FinancePage] loadTransactions error: $e'); if (!mounted) return; setState(() { _isLoading = false; _error = e.toString(); }); } } @override Widget build(BuildContext context) { if (_isLoading) { return const Center(child: CircularProgressIndicator()); } if (_error != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, size: 48, color: AppColors.textTertiary), const SizedBox(height: 16), Text(_error!, style: const TextStyle(color: AppColors.textSecondary)), const SizedBox(height: 16), ElevatedButton(onPressed: _loadTransactions, child: Text(context.t('retry'))), ], ), ); } if (_transactions.isEmpty) { return Center( child: Text(context.t('finance_no_transactions'), style: const TextStyle(color: AppColors.textSecondary)), ); } return RefreshIndicator( onRefresh: _loadTransactions, child: ListView.separated( padding: const EdgeInsets.all(20), itemCount: _transactions.length, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, index) { final tx = _transactions[index]; final isPositive = tx.amount >= 0; final color = isPositive ? AppColors.success : AppColors.error; return ListTile( contentPadding: const EdgeInsets.symmetric(vertical: 6), title: Text(tx.description, style: const TextStyle(fontSize: 14)), subtitle: Text( _formatTime(tx.createdAt), style: const TextStyle(fontSize: 12, color: AppColors.textTertiary), ), trailing: Text( '${isPositive ? '+' : ''}\$${tx.amount.toStringAsFixed(2)}', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: color), ), ); }, ), ); } String _formatTime(DateTime dt) { final now = DateTime.now(); final isToday = dt.year == now.year && dt.month == now.month && dt.day == now.day; final timeStr = '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; if (isToday) return '${context.t('finance_today')} $timeStr'; final yesterday = now.subtract(const Duration(days: 1)); final isYesterday = dt.year == yesterday.year && dt.month == yesterday.month && dt.day == yesterday.day; if (isYesterday) return '${context.t('finance_yesterday')} $timeStr'; return '${dt.month}/${dt.day} $timeStr'; } } class _ReconciliationTab extends StatefulWidget { const _ReconciliationTab(); @override State<_ReconciliationTab> createState() => _ReconciliationTabState(); } class _ReconciliationTabState extends State<_ReconciliationTab> { final _financeService = IssuerFinanceService(); bool _isLoading = true; String? _error; Map _reconciliation = {}; @override void initState() { super.initState(); _loadReconciliation(); } Future _loadReconciliation() async { setState(() { _isLoading = true; _error = null; }); try { final data = await _financeService.getReconciliation(); if (!mounted) return; setState(() { _reconciliation = data; _isLoading = false; }); } catch (e) { debugPrint('[FinancePage] loadReconciliation error: $e'); if (!mounted) return; setState(() { _isLoading = false; _error = e.toString(); }); } } @override Widget build(BuildContext context) { if (_isLoading) { return const Center(child: CircularProgressIndicator()); } if (_error != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, size: 48, color: AppColors.textTertiary), const SizedBox(height: 16), Text(_error!, style: const TextStyle(color: AppColors.textSecondary)), const SizedBox(height: 16), ElevatedButton(onPressed: _loadReconciliation, child: Text(context.t('retry'))), ], ), ); } final reports = (_reconciliation['reports'] as List?)?.cast>() ?? []; return RefreshIndicator( onRefresh: _loadReconciliation, child: ListView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(20), children: [ // Generate New OutlinedButton.icon( onPressed: () { // TODO: Trigger reconciliation report generation }, icon: const Icon(Icons.add_rounded), label: Text(context.t('finance_generate_report')), style: OutlinedButton.styleFrom( minimumSize: const Size(double.infinity, 48), ), ), const SizedBox(height: 20), if (reports.isEmpty) Center( child: Padding( padding: const EdgeInsets.all(32), child: Text(context.t('finance_no_reports'), style: const TextStyle(color: AppColors.textSecondary)), ), ) else ...reports.map((r) { final title = r['title'] ?? ''; final summary = r['summary'] ?? ''; final status = r['status'] ?? '--'; return Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.borderLight), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: AppColors.successLight, borderRadius: BorderRadius.circular(999), ), child: Text(status, style: const TextStyle(fontSize: 11, color: AppColors.success)), ), ], ), const SizedBox(height: 6), Text(summary, style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)), const SizedBox(height: 12), Row( children: [ TextButton.icon( onPressed: () { // TODO: Navigate to reconciliation detail view }, icon: const Icon(Icons.visibility_rounded, size: 16), label: Text(context.t('view')), ), TextButton.icon( onPressed: () { // TODO: Export reconciliation report }, icon: const Icon(Icons.download_rounded, size: 16), label: Text(context.t('export')), ), ], ), ], ), ); }), ], ), ); } }