import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/event/app_event_bus.dart'; import '../../../providers/asset_provider.dart'; import 'components/account_tab_switcher.dart'; import 'components/action_buttons_row.dart'; import 'components/asset_dialogs.dart'; import 'components/balance_card.dart'; import 'components/holdings_section.dart'; import 'components/records_link_row.dart'; import '../orders/fund_orders_page.dart'; import 'transfer_page.dart'; /// 资产页面 - Matching .pen design spec (CMcqs) class AssetPage extends StatefulWidget { const AssetPage({super.key}); @override State createState() => _AssetPageState(); } class _AssetPageState extends State with AutomaticKeepAliveClientMixin { int _activeTab = 0; StreamSubscription? _eventSub; @override bool get wantKeepAlive => true; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _loadData(); _listenEvents(); }); } @override void dispose() { _eventSub?.cancel(); super.dispose(); } void _listenEvents() { final eventBus = context.read(); _eventSub = eventBus.on(AppEventType.assetChanged, (_) { if (mounted) { context.read().refreshAll(force: true); } }); } void _loadData() { context.read().refreshAll(force: true); } @override Widget build(BuildContext context) { super.build(context); final colorScheme = Theme.of(context).colorScheme; return Scaffold( backgroundColor: colorScheme.background, body: Consumer( builder: (context, provider, _) { return RefreshIndicator( onRefresh: () => provider.refreshAll(force: true), color: colorScheme.primary, backgroundColor: colorScheme.surfaceContainerHighest, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB(AppSpacing.md, AppSpacing.md + 8, AppSpacing.md, 32), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Page title: "资产" 22px bold — matching .pen titleFrame padding [16,0,8,0] Padding( padding: const EdgeInsets.only(top: 16, bottom: 8), child: Text( '资产', style: AppTextStyles.displaySmall(context), ), ), const SizedBox(height: AppSpacing.sm), // Account tab switcher — pill-style matching .pen UE6xC AccountTabSwitcher( selectedIndex: _activeTab, onChanged: (index) => setState(() => _activeTab = index), ), const SizedBox(height: AppSpacing.md), // Balance card — matching .pen 59637 (cornerRadius lg, stroke, padding 20, gap 12) BalanceCard( provider: provider, activeTab: _activeTab, ), const SizedBox(height: AppSpacing.md), // Action buttons row — matching .pen pIpHe (gap 12) ActionButtonsRow( onDeposit: () => showDepositDialog(context), onWithdraw: () => showWithdrawDialog(context, provider.fundAccount?.balance), onTransfer: () => _navigateToTransfer(context), ), const SizedBox(height: AppSpacing.md), // Records link row — matching .pen fLHtq (cornerRadius lg, padding [14,16], stroke) RecordsLinkRow( onTap: () => Navigator.push( context, MaterialPageRoute(builder: (_) => const FundOrdersPage()), ), ), const SizedBox(height: AppSpacing.md), // Holdings section — matching .pen th9BG + 6X6tC HoldingsSection(holdings: _activeTab == 1 ? provider.holdings : []), ], ), ), ); }, ), ); } void _navigateToTransfer(BuildContext context) async { final result = await Navigator.push( context, MaterialPageRoute(builder: (_) => const TransferPage()), ); if (result == true && context.mounted) { context.read().refreshAll(force: true); } } } /// 交易账户卡片 - Glass Panel 风格 class _TradeAccountCard extends StatelessWidget { final List holdings; final String? tradeBalance; const _TradeAccountCard({required this.holdings, this.tradeBalance}); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; // 计算总市值(所有持仓折算成USDT) double totalValue = 0; for (var h in holdings) { final value = double.tryParse(h.currentValue?.toString() ?? '0') ?? 0; totalValue += value; } // 对持仓进行排序:USDT 放在最上面 final sortedHoldings = List.from(holdings); sortedHoldings.sort((a, b) { final codeA = (a.coinCode ?? a['coinCode'] ?? '').toString().toUpperCase(); final codeB = (b.coinCode ?? b['coinCode'] ?? '').toString().toUpperCase(); if (codeA == 'USDT') return -1; if (codeB == 'USDT') return 1; return 0; }); return GlassPanel( padding: AppSpacing.cardPadding, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题行 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: colorScheme.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(AppRadius.md), ), child: Icon( LucideIcons.trendingUp, size: 18, color: colorScheme.primary, ), ), SizedBox(width: AppSpacing.sm), Text( '交易账户', style: GoogleFonts.spaceGrotesk( fontSize: 16, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), ], ), Icon( LucideIcons.chevronRight, size: 14, color: colorScheme.primary, ), ], ), SizedBox(height: AppSpacing.md), // 余额 Text( '余额 (USDT)', style: TextStyle( fontSize: 11, color: colorScheme.onSurfaceVariant, ), ), SizedBox(height: AppSpacing.xs), Text( totalValue.toStringAsFixed(2), style: GoogleFonts.spaceGrotesk( fontSize: 20, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), SizedBox(height: AppSpacing.lg), // 持仓列表标题 Text( '持仓列表', style: GoogleFonts.spaceGrotesk( fontSize: 14, fontWeight: FontWeight.w600, color: colorScheme.onSurfaceVariant, ), ), SizedBox(height: AppSpacing.md), if (sortedHoldings.isEmpty) const _EmptyState(icon: LucideIcons.wallet, message: '暂无持仓') else ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: sortedHoldings.length, separatorBuilder: (_, __) => Container( margin: EdgeInsets.only(left: 56), height: 1, color: AppColorScheme.glassPanelBorder, ), itemBuilder: (context, index) => _HoldingItem(holding: sortedHoldings[index]), ), ], ), ); } } /// 空状态 class _EmptyState extends StatelessWidget { final IconData icon; final String message; const _EmptyState({required this.icon, required this.message}); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Center( child: Padding( padding: EdgeInsets.all(AppSpacing.xl), child: Column( children: [ Icon( icon, size: 48, color: colorScheme.onSurfaceVariant, ), SizedBox(height: AppSpacing.sm + AppSpacing.xs), Text( message, style: TextStyle(color: colorScheme.onSurfaceVariant), ), ], ), ), ); } } /// 持仓项 class _HoldingItem extends StatelessWidget { final dynamic holding; const _HoldingItem({required this.holding}); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; return Padding( padding: EdgeInsets.symmetric(vertical: AppSpacing.sm), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: colorScheme.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(AppRadius.md), ), child: Center( child: Text( holding.coinCode.substring(0, 1), style: TextStyle( color: colorScheme.primary, fontWeight: FontWeight.bold, ), ), ), ), SizedBox(width: AppSpacing.sm + AppSpacing.xs), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( holding.coinCode, style: GoogleFonts.spaceGrotesk( fontSize: 14, fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), Text( '数量: ${holding.quantity}', style: TextStyle( fontSize: 12, color: colorScheme.onSurfaceVariant, ), ), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '${holding.currentValue} USDT', style: TextStyle( fontSize: 12, color: colorScheme.onSurface, ), ), Text( holding.formattedProfitRate, style: TextStyle( color: holding.isProfit ? AppColorScheme.getUpColor(isDark) : AppColorScheme.down, fontSize: 12, fontWeight: FontWeight.w600, ), ), ], ), ], ), ); } } // ============================================ // Dialogs - Glass Panel 风格 // ============================================ void _showDepositDialog(BuildContext context) { final amountController = TextEditingController(); final formKey = GlobalKey(); final colorScheme = Theme.of(context).colorScheme; showShadDialog( context: context, builder: (ctx) => Dialog( backgroundColor: Colors.transparent, child: GlassPanel( borderRadius: BorderRadius.circular(AppRadius.lg), padding: EdgeInsets.all(AppSpacing.lg), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Deposit (充值)', style: GoogleFonts.spaceGrotesk( fontSize: 16, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), SizedBox(height: AppSpacing.xs), Text( 'Asset: USDT', style: TextStyle( fontSize: 12, letterSpacing: 0.1, color: colorScheme.onSurfaceVariant, ), ), ], ), Container( padding: EdgeInsets.all(AppSpacing.sm), decoration: BoxDecoration( color: colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(AppRadius.md), ), child: Icon( LucideIcons.wallet, color: colorScheme.secondary, ), ), ], ), SizedBox(height: AppSpacing.lg), ShadForm( key: formKey, child: ShadInputFormField( id: 'amount', controller: amountController, label: const Text('充值金额'), placeholder: const Text('最低 1000 USDT'), keyboardType: const TextInputType.numberWithOptions(decimal: true), validator: (v) { if (v == null || v.isEmpty) return '请输入金额'; final n = double.tryParse(v); if (n == null || n <= 0) return '请输入有效金额'; if (n < 1000) return '单笔最低充值1000 USDT'; return null; }, ), ), SizedBox(height: AppSpacing.lg), Row( children: [ Expanded( child: NeonButton( text: '取消', type: NeonButtonType.outline, onPressed: () => Navigator.of(ctx).pop(), height: 48, showGlow: false, ), ), SizedBox(width: AppSpacing.sm), Expanded( child: NeonButton( text: '下一步', type: NeonButtonType.primary, onPressed: () async { if (formKey.currentState!.saveAndValidate()) { Navigator.of(ctx).pop(); final response = await context.read().deposit( amount: amountController.text, ); if (context.mounted) { if (response.success && response.data != null) { _showDepositResultDialog(context, response.data!); } else { _showResultDialog(context, '申请失败', response.message); } } } }, height: 48, showGlow: true, ), ), ], ), ], ), ), ), ); } void _showDepositResultDialog(BuildContext context, Map data) { final orderNo = data['orderNo'] as String? ?? ''; final amount = data['amount']?.toString() ?? '0.00'; final walletAddress = data['walletAddress'] as String? ?? ''; final walletNetwork = data['walletNetwork'] as String? ?? 'TRC20'; final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; showShadDialog( context: context, builder: (ctx) => Dialog( backgroundColor: Colors.transparent, child: GlassPanel( borderRadius: BorderRadius.circular(AppRadius.lg), padding: EdgeInsets.all(AppSpacing.lg), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ NeonIcon( icon: Icons.check_circle, color: AppColorScheme.getUpColor(isDark), size: 24, ), SizedBox(width: AppSpacing.sm), Text( '充值申请成功', style: GoogleFonts.spaceGrotesk( fontSize: 16, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), ], ), SizedBox(height: AppSpacing.lg), _InfoRow(label: '订单号', value: orderNo), SizedBox(height: AppSpacing.sm), _InfoRow(label: '充值金额', value: '$amount USDT', isBold: true), SizedBox(height: AppSpacing.lg), Text( '请向以下地址转账:', style: TextStyle( fontSize: 12, color: colorScheme.onSurfaceVariant, ), ), SizedBox(height: AppSpacing.sm), _WalletAddressCard(address: walletAddress, network: walletNetwork), SizedBox(height: AppSpacing.md), Container( padding: EdgeInsets.all(AppSpacing.sm), decoration: BoxDecoration( color: AppColorScheme.warning.withOpacity(0.1), borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all( color: AppColorScheme.warning.withOpacity(0.2), ), ), child: Row( children: [ Icon(Icons.info_outline, size: 16, color: AppColorScheme.warning), SizedBox(width: AppSpacing.sm), Expanded( child: Text( '转账完成后请点击"已打款"按钮确认', style: TextStyle(fontSize: 12, color: AppColorScheme.warning), ), ), ], ), ), SizedBox(height: AppSpacing.lg), Row( children: [ Expanded( child: NeonButton( text: '稍后确认', type: NeonButtonType.outline, onPressed: () => Navigator.of(ctx).pop(), height: 44, showGlow: false, ), ), SizedBox(width: AppSpacing.sm), Expanded( child: NeonButton( text: '已打款', type: NeonButtonType.primary, onPressed: () async { Navigator.of(ctx).pop(); final response = await context.read().confirmPay(orderNo); if (context.mounted) { _showResultDialog( context, response.success ? '确认成功' : '确认失败', response.success ? '请等待管理员审核' : response.message, ); } }, height: 44, showGlow: true, ), ), ], ), ], ), ), ), ); } class _InfoRow extends StatelessWidget { final String label; final String value; final bool isBold; const _InfoRow({required this.label, required this.value, this.isBold = false}); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: TextStyle( fontSize: 12, color: colorScheme.onSurfaceVariant, ), ), Text( value, style: TextStyle( fontSize: 12, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, color: colorScheme.onSurface, ), ), ], ); } } class _WalletAddressCard extends StatelessWidget { final String address; final String network; const _WalletAddressCard({required this.address, required this.network}); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Container( padding: EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all( color: colorScheme.outlineVariant.withOpacity(0.3), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( address, style: TextStyle( fontFamily: 'monospace', fontSize: 12, color: colorScheme.onSurface, ), ), ), GestureDetector( onTap: () { Clipboard.setData(ClipboardData(text: address)); ToastUtils.show('地址已复制到剪贴板'); }, child: Container( padding: EdgeInsets.all(AppSpacing.xs), decoration: BoxDecoration( color: colorScheme.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Icon( LucideIcons.copy, size: 16, color: colorScheme.primary, ), ), ), ], ), SizedBox(height: AppSpacing.sm), Text( '网络: $network', style: TextStyle( fontSize: 11, color: colorScheme.onSurfaceVariant, ), ), ], ), ); } } void _showWithdrawDialog(BuildContext context, String? balance) { final amountController = TextEditingController(); final addressController = TextEditingController(); final contactController = TextEditingController(); final formKey = GlobalKey(); final feeNotifier = ValueNotifier('提现将扣除10%手续费'); final colorScheme = Theme.of(context).colorScheme; final networksNotifier = ValueNotifier>([]); final selectedNetworkNotifier = ValueNotifier(null); amountController.addListener(() { final amount = double.tryParse(amountController.text) ?? 0; if (amount > 0) { final fee = amount * 0.1; final receivable = amount - fee; feeNotifier.value = '手续费(10%): -${fee.toStringAsFixed(2)} USDT | 应付款: ${receivable.toStringAsFixed(2)} USDT'; } else { feeNotifier.value = '提现将扣除10%手续费'; } }); // 获取网络列表 context.read().getWalletNetworks().then((list) { networksNotifier.value = list; if (list.isNotEmpty) { selectedNetworkNotifier.value = list.first; } }); showShadDialog( context: context, builder: (ctx) => StatefulBuilder( builder: (ctx, setState) => Dialog( backgroundColor: Colors.transparent, child: GlassPanel( borderRadius: BorderRadius.circular(AppRadius.lg), padding: EdgeInsets.all(AppSpacing.lg), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: EdgeInsets.all(AppSpacing.sm), decoration: BoxDecoration( color: colorScheme.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(AppRadius.md), ), child: Icon( LucideIcons.wallet, color: colorScheme.primary, ), ), SizedBox(width: AppSpacing.sm), Text( '提现 (Withdraw)', style: GoogleFonts.spaceGrotesk( fontSize: 16, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), ], ), SizedBox(height: AppSpacing.xs), Text( 'Securely transfer your assets to an external wallet address.', style: TextStyle( fontSize: 12, color: colorScheme.onSurfaceVariant, ), ), if (balance != null) ...[ SizedBox(height: AppSpacing.md), Container( padding: EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.sm, ), decoration: BoxDecoration( color: AppColorScheme.up.withOpacity(0.1), borderRadius: BorderRadius.circular(AppRadius.full), border: Border.all( color: AppColorScheme.up.withOpacity(0.2), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( '可用余额: ', style: TextStyle( fontSize: 10, letterSpacing: 0.1, color: colorScheme.onSurfaceVariant, ), ), Text( '$balance USDT', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: AppColorScheme.up, ), ), ], ), ), ], SizedBox(height: AppSpacing.lg), ShadForm( key: formKey, child: Column( children: [ ShadInputFormField( id: 'amount', controller: amountController, label: const Text('提现金额'), placeholder: const Text('请输入提现金额(USDT)'), keyboardType: const TextInputType.numberWithOptions(decimal: true), validator: Validators.amount, ), SizedBox(height: AppSpacing.xs), Container( padding: EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.sm, ), decoration: BoxDecoration( color: Colors.orange.withOpacity(0.1), borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: Colors.orange.withOpacity(0.3)), ), child: Row( children: [ Icon(Icons.info_outline, size: 14, color: Colors.orange), SizedBox(width: AppSpacing.xs), Expanded( child: ValueListenableBuilder( valueListenable: feeNotifier, builder: (_, text, __) => Text( text, style: TextStyle( fontSize: 11, color: Colors.orange.shade800, ), ), ), ), ], ), ), SizedBox(height: AppSpacing.md), // 提现网络选择 ValueListenableBuilder>( valueListenable: networksNotifier, builder: (_, networks, __) { if (networks.isEmpty) return const SizedBox.shrink(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '提现网络', style: TextStyle( fontSize: 12, color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 4), SizedBox( width: double.infinity, child: ValueListenableBuilder( valueListenable: selectedNetworkNotifier, builder: (_, selected, __) => ShadSelect( initialValue: selected ?? networks.first, placeholder: const Text('选择提现网络'), onChanged: (value) { selectedNetworkNotifier.value = value; }, selectedOptionBuilder: (context, value) => Text(value), options: networks.map((network) => ShadOption( value: network, child: Text(network), )), ), ), ), ], ); }, ), SizedBox(height: AppSpacing.md), ShadInputFormField( id: 'address', controller: addressController, label: const Text('目标地址'), placeholder: const Text('请输入提现地址'), validator: (v) => Validators.required(v, '提现地址'), ), SizedBox(height: AppSpacing.md), ShadInputFormField( id: 'contact', controller: contactController, label: const Text('联系方式(可选)'), placeholder: const Text('联系方式'), ), ], ), ), SizedBox(height: AppSpacing.lg), Row( children: [ Expanded( child: NeonButton( text: '取消', type: NeonButtonType.outline, onPressed: () => Navigator.of(ctx).pop(), height: 44, showGlow: false, ), ), SizedBox(width: AppSpacing.sm), Expanded( child: NeonButton( text: '提交', type: NeonButtonType.primary, onPressed: () async { if (formKey.currentState!.saveAndValidate()) { Navigator.of(ctx).pop(); final response = await context.read().withdraw( amount: amountController.text, withdrawAddress: addressController.text, network: selectedNetworkNotifier.value, withdrawContact: contactController.text.isNotEmpty ? contactController.text : null, ); if (context.mounted) { _showResultDialog( context, response.success ? '申请成功' : '申请失败', response.success ? '请等待管理员审批' : response.message, ); } } }, height: 44, showGlow: true, ), ), ], ), SizedBox(height: AppSpacing.md), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.verified_user, size: 12, color: colorScheme.onSurfaceVariant.withOpacity(0.5), ), SizedBox(width: AppSpacing.xs), Text( 'End-to-End Encrypted Transaction', style: TextStyle( fontSize: 10, letterSpacing: 0.1, color: colorScheme.onSurfaceVariant.withOpacity(0.5), ), ), ], ), ], ), ), ), ), ), ); }