import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/l10n/app_localizations.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/utils/top_toast.dart'; import '../../../core/utils/number_format.dart'; import '../../../data/models/asset/withdraw_record.dart'; import '../../../providers/withdraw_detail_provider.dart'; class WithdrawDetailScreen extends ConsumerStatefulWidget { const WithdrawDetailScreen({ super.key, required this.record, required this.isTransfer, }); final WithdrawRecord record; final bool isTransfer; @override ConsumerState createState() => _WithdrawDetailScreenState(); } class _WithdrawDetailScreenState extends ConsumerState { @override void initState() { super.initState(); Future.microtask(() { ref.read(withdrawDetailProvider.notifier).loadRecord( widget.record, isTransfer: widget.isTransfer, ); }); } @override Widget build(BuildContext context) { final state = ref.watch(withdrawDetailProvider); final record = state.record ?? widget.record; final isDark = Theme.of(context).brightness == Brightness.dark; final dividerColor = isDark ? AppColors.darkBg : AppColors.lightBgSecondary; return Scaffold( backgroundColor: isDark ? AppColors.darkBg : AppColors.lightBg, appBar: AppBar( backgroundColor: isDark ? AppColors.darkBg : AppColors.lightBg, leading: IconButton( icon: const Icon(Icons.chevron_left, size: 28), onPressed: () => context.pop(), ), title: Text( widget.isTransfer ? AppLocalizations.of(context)!.transferDetail : AppLocalizations.of(context)!.withdrawDetail, style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w600), ), centerTitle: true, ), body: state.isLoading ? const Center(child: CircularProgressIndicator()) : SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // ── 数量 ────────────────────────────────────── _AmountSection( record: record, isTransfer: widget.isTransfer), // ── 分隔 ────────────────────────────────────── Container(height: 8, color: dividerColor), // ── 提现进度 ────────────────────────────────── _ProgressSection( record: record, isTransfer: widget.isTransfer), // ── 分隔 ────────────────────────────────────── Container(height: 8, color: dividerColor), // ── 信息卡片(链路 / 类型 / 状态)────────────── _InfoCards( record: record, isTransfer: widget.isTransfer), // ── 分隔 ────────────────────────────────────── Container(height: 8, color: dividerColor), // ── 详细信息 ────────────────────────────────── _DetailRows( record: record, isTransfer: widget.isTransfer), // ── 取消按钮 ────────────────────────────────── if (record.canCancel && _within3Minutes(record.createTime)) Padding( padding: const EdgeInsets.fromLTRB(16, 24, 16, 40), child: _CancelButton( isCancelling: state.isCancelling, onCancel: () => _showCancelDialog(context), ), ) else const SizedBox(height: 40), ], ), ), ); } bool _within3Minutes(String createTime) { try { final dt = DateTime.parse(createTime.replaceFirst(' ', 'T')); return DateTime.now().difference(dt).inSeconds <= 180; } catch (_) { return false; } } void _showCancelDialog(BuildContext ctx) { showDialog( context: ctx, builder: (dialogCtx) => AlertDialog( title: Text(AppLocalizations.of(ctx)!.cancelWithdraw), content: Text(AppLocalizations.of(ctx)!.confirmCancelWithdraw), actions: [ TextButton( onPressed: () => Navigator.pop(dialogCtx), child: Text(AppLocalizations.of(ctx)!.cancel), ), TextButton( onPressed: () async { Navigator.pop(dialogCtx); final ok = await ref .read(withdrawDetailProvider.notifier) .cancel(isTransfer: widget.isTransfer); if (!mounted) return; final l10n = AppLocalizations.of(ctx)!; ScaffoldMessenger.of(ctx).showSnackBar( SnackBar(content: Text(ok ? l10n.withdrawCancelled : l10n.operationFailed)), ); }, child: Text(AppLocalizations.of(ctx)!.confirm), ), ], ), ); } } // ── 数量区块(居中,无卡片,与原型一致)────────────────────────────── class _AmountSection extends StatelessWidget { const _AmountSection({required this.record, required this.isTransfer}); final WithdrawRecord record; final bool isTransfer; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final coinUnit = record.coin?.baseCoinDisplay.isNotEmpty == true ? record.coin!.baseCoinDisplay : (record.coin?.coinName.isNotEmpty == true ? record.coin!.coinName : 'USDT'); final rawAmount = isTransfer ? record.transferAmount : record.amount; final amountStr = formatAmount(double.tryParse(rawAmount) ?? 0); return Padding( padding: const EdgeInsets.fromLTRB(16, 32, 16, 28), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( AppLocalizations.of(context)!.amountLabel, style: TextStyle( color: cs.onSurface.withAlpha(120), fontSize: 12), ), const SizedBox(height: 10), Text( '-$amountStr $coinUnit', style: const TextStyle( color: AppColors.fall, fontSize: 30, fontWeight: FontWeight.w700, letterSpacing: -0.5, ), ), ], ), ); } } // ── 提现进度(垂直步骤条,步骤居中对齐原型)──────────────────────────── class _ProgressSection extends StatelessWidget { const _ProgressSection( {required this.record, required this.isTransfer}); final WithdrawRecord record; final bool isTransfer; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final status = record.status; // 步骤 2(等待提现):有申请记录就已进入等待状态 const step2Done = true; // 步骤 3:失败/成功/取消 时完成 final step3Done = ['2', '3', '4'].contains(status); final isFailed = status == '2'; final isCancelled = status == '4'; final l10n = AppLocalizations.of(context)!; final String step3Title; if (isFailed) { step3Title = l10n.withdrawFailed; } else if (isCancelled) { step3Title = l10n.cancelWithdraw; } else { step3Title = l10n.withdrawSuccess; } return Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( AppLocalizations.of(context)!.withdrawProgress, style: TextStyle( color: cs.onSurface.withAlpha(120), fontSize: 12), ), const SizedBox(height: 16), // 步骤容器居中,宽 200 Center( child: SizedBox( width: 200, child: Column( children: [ _VStep( done: true, failed: false, hasLine: true, title: l10n.withdrawApplication, desc: record.createTime, ), _VStep( done: step2Done, failed: false, hasLine: true, title: l10n.waitingWithdraw, desc: record.canCancel ? AppLocalizations.of(context)!.cancelWithdrawHint : '', ), _VStep( done: step3Done, failed: isFailed, hasLine: false, title: step3Title, desc: step3Done && record.dealTime.isNotEmpty ? record.dealTime : '', ), ], ), ), ), ], ), ); } } class _VStep extends StatelessWidget { const _VStep({ required this.done, required this.failed, required this.hasLine, required this.title, required this.desc, }); final bool done; final bool failed; final bool hasLine; final String title; final String desc; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final Color borderColor = done ? (failed ? AppColors.fall : (isDark ? Colors.white : Colors.black)) : (isDark ? const Color(0xFF444444) : const Color(0xFFEEEEEE)); final Color iconColor = done ? (failed ? AppColors.fall : (isDark ? Colors.white : Colors.black)) : (isDark ? const Color(0xFF555555) : const Color(0xFFCCCCCC)); final Color lineColor = done ? (isDark ? Colors.white70 : Colors.black) : (isDark ? const Color(0xFF444444) : const Color(0xFFEEEEEE)); return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 24, child: Column( children: [ Container( width: 24, height: 24, decoration: BoxDecoration( shape: BoxShape.circle, color: isDark ? AppColors.darkBg : Colors.white, border: Border.all(color: borderColor, width: 2), ), child: Center( child: Icon( failed ? Icons.close : Icons.check, size: 13, color: iconColor, ), ), ), if (hasLine) Container( width: 2, height: 44, margin: const EdgeInsets.symmetric(vertical: 2), color: lineColor, ), ], ), ), const SizedBox(width: 14), Expanded( child: Padding( padding: EdgeInsets.only(top: 2, bottom: hasLine ? 20 : 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( color: done ? (isDark ? Colors.white : Colors.black) : cs.onSurface.withAlpha(100), fontSize: 14, fontWeight: FontWeight.w600, ), ), if (desc.isNotEmpty) ...[ const SizedBox(height: 4), Text( desc, style: TextStyle( color: cs.onSurface.withAlpha(100), fontSize: 10), ), ], ], ), ), ), ], ); } } // ── 信息卡片(3 个独立带边框卡片,间距 10,对齐原型)──────────────────── class _InfoCards extends StatelessWidget { const _InfoCards({required this.record, required this.isTransfer}); final WithdrawRecord record; final bool isTransfer; String _localizedStatus(AppLocalizations l10n) { switch (record.status) { case '0': return l10n.withdrawStatusReviewing; case '1': return l10n.withdrawStatusReleasing; case '2': return l10n.withdrawStatusFailed; case '3': return l10n.withdrawStatusSuccess; case '4': return l10n.withdrawStatusCancelled; default: return l10n.unknown; } } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final baseCoin = record.coin?.baseCoinDisplay.isNotEmpty == true ? record.coin!.baseCoinDisplay : (record.coin?.coinName.isNotEmpty == true ? record.coin!.coinName : 'USDT'); final network = record.coin?.networkName ?? ''; final l10n = AppLocalizations.of(context)!; final chainLabel = isTransfer ? l10n.internalLabel : (network.isNotEmpty ? '$baseCoin-$network' : baseCoin); final typeLabel = isTransfer ? l10n.internalTransfer : l10n.onChainWithdraw; Color statusColor; switch (record.status) { case '3': statusColor = AppColors.rise; break; case '2': statusColor = AppColors.fall; break; case '4': statusColor = cs.onSurface.withAlpha(153); break; default: statusColor = AppColors.warning; } return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: Row( children: [ _InfoCard(label: AppLocalizations.of(context)!.withdrawNetwork, value: chainLabel, isDark: isDark), const SizedBox(width: 10), _InfoCard(label: AppLocalizations.of(context)!.typeLabel, value: typeLabel, isDark: isDark), const SizedBox(width: 10), _InfoCard( label: AppLocalizations.of(context)!.statusLabel, value: _localizedStatus(l10n), valueColor: statusColor, isDark: isDark, ), ], ), ); } } class _InfoCard extends StatelessWidget { const _InfoCard({ required this.label, required this.value, required this.isDark, this.valueColor, }); final String label; final String value; final bool isDark; final Color? valueColor; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final borderColor = isDark ? AppColors.darkDivider : AppColors.lightBorder; return Expanded( child: Container( decoration: BoxDecoration( border: Border.all(color: borderColor, width: 1), borderRadius: BorderRadius.circular(6), ), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( label, style: TextStyle( color: cs.onSurface.withAlpha(120), fontSize: 11), ), const SizedBox(height: 6), Text( value, textAlign: TextAlign.center, style: TextStyle( color: valueColor ?? cs.onSurface, fontSize: 12, fontWeight: FontWeight.w600, ), ), ], ), ), ); } } // ── 详细信息行(平铺,行间 1px 分隔,对齐原型)────────────────────────── class _DetailRows extends StatelessWidget { const _DetailRows({required this.record, required this.isTransfer}); final WithdrawRecord record; final bool isTransfer; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final coinUnit = record.coin?.baseCoinDisplay.isNotEmpty == true ? record.coin!.baseCoinDisplay : (record.coin?.coinName.isNotEmpty == true ? record.coin!.coinName : 'USDT'); final feeDouble = double.tryParse(record.coin?.fee ?? '0') ?? 0; final feeStr = formatAmount(feeDouble); final address = record.address; final txHash = record.transactionHashId; final divColor = isDark ? AppColors.darkDivider : AppColors.lightBgSecondary; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ // 手续费 Padding( padding: const EdgeInsets.symmetric(vertical: 14), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(AppLocalizations.of(context)!.fee, style: TextStyle( color: cs.onSurface.withAlpha(120), fontSize: 13)), Text('$feeStr $coinUnit', style: TextStyle( color: cs.onSurface, fontSize: 13, fontWeight: FontWeight.w500)), ], ), ), Divider( height: 0.5, thickness: 0.5, color: divColor), // 提币地址 / 转入账户 Padding( padding: const EdgeInsets.symmetric(vertical: 14), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( isTransfer ? AppLocalizations.of(context)!.transferUser : AppLocalizations.of(context)!.withdrawAddress, style: TextStyle( color: cs.onSurface.withAlpha(120), fontSize: 13), ), const SizedBox(width: 16), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.start, children: [ Flexible( child: Text( address, textAlign: TextAlign.right, style: TextStyle( color: cs.onSurface, fontSize: 13, fontWeight: FontWeight.w500), ), ), const SizedBox(width: 8), GestureDetector( onTap: () { Clipboard.setData(ClipboardData(text: address)); showTopToast(context, message: AppLocalizations.of(context)!.uidCopied, backgroundColor: AppColors.rise); }, child: Icon(Icons.content_copy, size: 15, color: cs.onSurface.withAlpha(120)), ), ], ), ), ], ), ), // 哈希(仅链上提现且有值时展示) if (!isTransfer && txHash.isNotEmpty) ...[ Divider(height: 0.5, thickness: 0.5, color: divColor), Padding( padding: const EdgeInsets.symmetric(vertical: 14), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( AppLocalizations.of(context)!.txHash, style: TextStyle( color: cs.onSurface.withAlpha(120), fontSize: 13), ), const SizedBox(width: 16), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.start, children: [ Flexible( child: Text( txHash, textAlign: TextAlign.right, style: TextStyle( color: cs.onSurface, fontSize: 13, fontWeight: FontWeight.w500), ), ), const SizedBox(width: 8), GestureDetector( onTap: () { Clipboard.setData(ClipboardData(text: txHash)); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(AppLocalizations.of(context)!.copied), duration: const Duration(seconds: 1)), ); }, child: Icon(Icons.content_copy, size: 15, color: cs.onSurface.withAlpha(120)), ), ], ), ), ], ), ), ], ], ), ); } } // ── 取消按钮 ───────────────────────────────────────────────────── class _CancelButton extends StatelessWidget { const _CancelButton( {required this.isCancelling, required this.onCancel}); final bool isCancelling; final VoidCallback onCancel; @override Widget build(BuildContext context) { return SizedBox( width: double.infinity, height: 48, child: OutlinedButton( onPressed: isCancelling ? null : onCancel, style: OutlinedButton.styleFrom( foregroundColor: AppColors.fall, side: const BorderSide(color: AppColors.fall, width: 1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8)), ), child: isCancelling ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(AppColors.fall), ), ) : Text(AppLocalizations.of(context)!.cancelWithdraw, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600)), ), ); } }