// Author: fruit.com.cn import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../core/data/models/wallet.dart'; import '../../core/data/services/wallet_controller.dart'; import '../../core/formatting/error_text.dart'; import '../../core/formatting/localized_date_time.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_theme.dart'; import '../../core/widgets/app_pull_to_refresh.dart'; import '../../core/widgets/empty_state.dart'; import '../../core/widgets/plain_page_scaffold.dart'; import '../store/recharge_page.dart'; import '../store/vip_page.dart'; import '../../l10n/app_localizations.dart'; /// 金币钱包:余额、累计收支与最近流水。 class WalletPage extends StatefulWidget { const WalletPage({super.key}); @override State createState() => _WalletPageState(); } class _WalletPageState extends State { bool _requested = false; @override void didChangeDependencies() { super.didChangeDependencies(); if (_requested) return; _requested = true; final wallet = context.read(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) unawaited(wallet.load()); }); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final wallet = context.watch(); return Theme( data: AppTheme.light(), child: PlainPageScaffold( pageKey: const Key('wallet-page'), backKey: const Key('wallet-back'), title: l10n.wallet, child: _body(l10n, wallet), ), ); } Widget _storeActions(AppLocalizations l10n, WalletController wallet) { return Row( children: [ Expanded( child: FilledButton.icon( key: const Key('wallet-top-up'), onPressed: () => _openRecharge(wallet), icon: const Icon(Icons.add_card_rounded, size: 16), label: Text(l10n.topUpWallet), style: FilledButton.styleFrom( backgroundColor: AppColors.blue, foregroundColor: AppColors.onPrimary, ), ), ), const SizedBox(width: 10), Expanded( child: OutlinedButton.icon( key: const Key('wallet-vip'), onPressed: () => _openVip(wallet), icon: const Icon(Icons.workspace_premium_outlined, size: 16), label: Text(l10n.vipMembership), style: OutlinedButton.styleFrom(foregroundColor: AppColors.gold), ), ), ], ); } void _openRecharge(WalletController wallet) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => RechargePage(onPurchased: () => unawaited(wallet.load())), ), ); } void _openVip(WalletController wallet) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => VipPage(onPurchased: () => unawaited(wallet.load())), ), ); } Widget _body(AppLocalizations l10n, WalletController wallet) { if (wallet.loading && wallet.snapshot == null) { return const Center(child: CircularProgressIndicator()); } final snapshot = wallet.snapshot; if (snapshot == null) { return EmptyState( key: const Key('wallet-error'), icon: Icons.account_balance_wallet_outlined, title: errorText(l10n, wallet.error, l10n.walletLoadFailed), actionLabel: l10n.retry, actionKey: const Key('wallet-retry'), onAction: () => unawaited(wallet.load()), ); } return AppPullToRefresh( onRefresh: wallet.load, child: ListView( physics: AppPullToRefresh.physics, padding: const EdgeInsets.fromLTRB(16, 8, 16, 28), children: [ _BalanceHero(wallet: snapshot.wallet), const SizedBox(height: 12), _storeActions(l10n, wallet), const SizedBox(height: 14), Row( children: [ Expanded( child: _SummaryTile( icon: Icons.add_card_rounded, label: l10n.totalRecharged, value: snapshot.wallet.totalRechargedCoin, color: AppColors.success, ), ), const SizedBox(width: 10), Expanded( child: _SummaryTile( icon: Icons.local_fire_department_rounded, label: l10n.totalConsumed, value: snapshot.wallet.totalConsumedCoin, color: AppColors.accent, ), ), ], ), const SizedBox(height: 22), Text( l10n.coinLedger, style: const TextStyle( color: AppColors.onSurface, fontSize: AppDimens.sectionTitle, fontWeight: FontWeight.w800, ), ), const SizedBox(height: 10), if (snapshot.ledger.isEmpty) _LedgerEmpty(message: l10n.noCoinLedger) else for (final entry in snapshot.ledger) ...[ _LedgerTile(entry: entry), const SizedBox(height: 8), ], ], ), ); } } class _BalanceHero extends StatelessWidget { const _BalanceHero({required this.wallet}); final CoinWallet wallet; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Container( key: const Key('wallet-balance-card'), padding: const EdgeInsets.fromLTRB(20, 22, 20, 20), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [AppColors.goldCard, AppColors.card], ), border: Border.all(color: AppColors.gold.withValues(alpha: 0.35)), boxShadow: [ BoxShadow( color: AppColors.gold.withValues(alpha: 0.08), blurRadius: 28, offset: const Offset(0, 14), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 38, height: 38, decoration: BoxDecoration( color: AppColors.gold.withValues(alpha: 0.15), shape: BoxShape.circle, ), child: const Icon( Icons.monetization_on_rounded, color: AppColors.gold, size: 21, ), ), const SizedBox(width: 10), Text( l10n.coinBalance, style: const TextStyle( color: AppColors.muted, fontSize: 13, fontWeight: FontWeight.w600, ), ), ], ), const SizedBox(height: 18), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '${wallet.coinBalance}', key: const Key('wallet-balance'), style: const TextStyle( color: AppColors.onSurface, fontSize: 42, height: 1, fontWeight: FontWeight.w800, ), ), const SizedBox(width: 8), Padding( padding: const EdgeInsets.only(bottom: 5), child: Text( l10n.coins, style: const TextStyle( color: AppColors.gold, fontSize: 14, fontWeight: FontWeight.w700, ), ), ), ], ), ], ), ); } } class _SummaryTile extends StatelessWidget { const _SummaryTile({ required this.icon, required this.label, required this.value, required this.color, }); final IconData icon; final String label; final int value; final Color color; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppColors.card, borderRadius: BorderRadius.circular(AppDimens.cardRadius), border: Border.all(color: AppColors.border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, color: color, size: 18), const SizedBox(height: 10), Text( '$value', style: const TextStyle( color: AppColors.onSurface, fontSize: 20, fontWeight: FontWeight.w800, ), ), const SizedBox(height: 2), Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: AppColors.subtle, fontSize: 11), ), ], ), ); } } class _LedgerTile extends StatelessWidget { const _LedgerTile({required this.entry}); final CoinLedgerEntry entry; @override Widget build(BuildContext context) { final positive = entry.changeAmount >= 0; final color = positive ? AppColors.success : AppColors.accent; final amount = positive ? '+${entry.changeAmount}' : '${entry.changeAmount}'; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppColors.card, borderRadius: BorderRadius.circular(AppDimens.cardRadius), border: Border.all(color: AppColors.border), ), child: Row( children: [ Container( width: 34, height: 34, alignment: Alignment.center, decoration: BoxDecoration( color: color.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(10), ), child: Icon( positive ? Icons.south_west_rounded : Icons.north_east_rounded, color: color, size: 18, ), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _ledgerLabel(context, entry.bizType), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.onSurface, fontSize: 13, fontWeight: FontWeight.w700, ), ), if (entry.remark.isNotEmpty) ...[ const SizedBox(height: 3), Text( entry.remark, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.subtle, fontSize: 11, ), ), ], if (entry.createdAt != null) ...[ const SizedBox(height: 3), Text( formatLocalizedDateTime(context, entry.createdAt!), style: const TextStyle( color: AppColors.subtle, fontSize: 10, ), ), ], ], ), ), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( amount, key: Key('wallet-ledger-${entry.id}'), style: TextStyle( color: color, fontSize: 14, fontWeight: FontWeight.w800, ), ), const SizedBox(height: 2), Text( '${entry.balanceAfter}', style: const TextStyle(color: AppColors.subtle, fontSize: 10), ), ], ), ], ), ); } String _ledgerLabel(BuildContext context, String bizType) { final l10n = AppLocalizations.of(context)!; return switch (bizType) { 'UNLOCK' => l10n.ledgerUnlock, 'RECHARGE' || 'ORDER' => l10n.ledgerRecharge, 'REFUND' => l10n.ledgerRefund, _ => l10n.ledgerAdjustment, }; } } class _LedgerEmpty extends StatelessWidget { const _LedgerEmpty({required this.message}); final String message; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 22), decoration: BoxDecoration( color: AppColors.card, borderRadius: BorderRadius.circular(AppDimens.cardRadius), border: Border.all(color: AppColors.border), ), child: Row( children: [ const Icon(Icons.receipt_long_outlined, color: AppColors.subtle), const SizedBox(width: 10), Expanded( child: Text( message, style: const TextStyle(color: AppColors.subtle, fontSize: 12), ), ), ], ), ); } }