import 'package:decimal/decimal.dart'; 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/dialog_utils.dart' show extractErrorMessage; import '../../../core/utils/top_toast.dart'; import '../../../providers/withdraw_provider.dart'; class WithdrawScreen extends ConsumerStatefulWidget { const WithdrawScreen({super.key}); @override ConsumerState createState() => _WithdrawScreenState(); } class _WithdrawScreenState extends ConsumerState with SingleTickerProviderStateMixin { bool _obscureFundPwd = true; late TabController _tabController; late PageController _pageController; final _addressController = TextEditingController(); final _amountController = TextEditingController(); final _fundPwdController = TextEditingController(); final _emailCodeController = TextEditingController(); final _googleCodeController = TextEditingController(); @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); _pageController = PageController(); _tabController.addListener(() { if (!mounted) return; if (_tabController.indexIsChanging) { _pageController.animateToPage( _tabController.index, duration: const Duration(milliseconds: 280), curve: Curves.easeOut, ); } else { _amountController.clear(); ref.read(withdrawProvider.notifier).setTab(_tabController.index); } }); _pageController.addListener(() { if (!mounted) return; if (!_pageController.hasClients) return; final page = _pageController.page!; final offset = page - _tabController.index; if (offset.abs() <= 1.0 && !_tabController.indexIsChanging) { _tabController.offset = offset.clamp(-1.0, 1.0); } }); // 每次进入页面重置 tab 到链上提币,并刷新数据 WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(withdrawProvider.notifier).setTab(0); ref.read(withdrawProvider.notifier).refresh(); }); } @override void dispose() { _tabController.dispose(); _pageController.dispose(); _addressController.dispose(); _amountController.dispose(); _fundPwdController.dispose(); _emailCodeController.dispose(); _googleCodeController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final state = ref.watch(withdrawProvider); final notifier = ref.read(withdrawProvider.notifier); return Scaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.chevron_left, size: 28), onPressed: () => context.pop(), ), title: Text(AppLocalizations.of(context)!.withdrawCoin, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), centerTitle: true, actions: [ TextButton( onPressed: () => context.push('/asset/withdraw/history'), child: Text(AppLocalizations.of(context)!.withdrawRecord, style: TextStyle(color: cs.onSurface, fontSize: 14)), ), ], ), body: _buildBody(context, state, notifier), ); } Widget _buildBody(BuildContext context, WithdrawState state, WithdrawNotifier notifier) { final isDark = Theme.of(context).brightness == Brightness.dark; return Column( children: [ // ── 链上提币 / 内部转账 Tab ──────────────────── Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: Container( height: 44, decoration: BoxDecoration(color: isDark ? AppColors.darkBgTertiary : AppColors.lightBgTertiary, borderRadius: BorderRadius.circular(22)), child: Row( children: [ _buildTab(context, state, notifier, 0, AppLocalizations.of(context)!.onChainWithdraw), _buildTab(context, state, notifier, 1, AppLocalizations.of(context)!.internalTransfer), ], ), ), ), // ── 内容区 ──────────────────────────────────── Expanded( child: PageView( controller: _pageController, onPageChanged: (index) { _addressController.clear(); _amountController.clear(); notifier.setTab(index); }, children: [ SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 0, 16, 32), child: _buildOnChain(context, state, notifier), ), SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 0, 16, 32), child: _buildInternal(context, state, notifier), ), ], ), ), ], ); } Widget _buildTab(BuildContext context, WithdrawState state, WithdrawNotifier notifier, int index, String label) { final cs = Theme.of(context).colorScheme; final selected = index == state.tabIndex; return Expanded( child: GestureDetector( onTap: () { _tabController.animateTo(index); }, child: Container( margin: const EdgeInsets.all(3), decoration: BoxDecoration( color: selected ? AppColors.brand : Colors.transparent, borderRadius: BorderRadius.circular(20), ), child: Center( child: Text(label, style: TextStyle( color: selected ? Colors.black : cs.onSurface.withAlpha(153), fontSize: 14, fontWeight: selected ? FontWeight.w600 : FontWeight.w400, )), ), ), ), ); } // ══════════════════════════════════════════════════════════ // 链上提币 // ══════════════════════════════════════════════════════════ void _showNetworkTipDialog(BuildContext context) { final l10n = AppLocalizations.of(context)!; final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; showDialog( context: context, builder: (ctx) => Dialog( backgroundColor: isDark ? AppColors.darkBgSecondary : Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Padding( padding: const EdgeInsets.fromLTRB(20, 16, 16, 20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题行 Row( children: [ Expanded( child: Text( l10n.withdrawNetwork, style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w600, ), ), ), GestureDetector( onTap: () => Navigator.of(ctx).pop(), child: Icon(Icons.close, size: 20, color: cs.onSurface.withAlpha(153)), ), ], ), const SizedBox(height: 12), // 内容 Text( l10n.withdrawNetworkTip, style: TextStyle( color: cs.onSurface.withAlpha(179), fontSize: 14, height: 1.6, ), ), const SizedBox(height: 20), // 确认按钮 SizedBox( width: double.infinity, height: 44, child: ElevatedButton( onPressed: () => Navigator.of(ctx).pop(), style: ElevatedButton.styleFrom( backgroundColor: AppColors.brand, foregroundColor: Colors.black, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), elevation: 0, ), child: Text(l10n.confirm, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), ), ), ], ), ), ), ); } Widget _buildOnChain(BuildContext context, WithdrawState state, WithdrawNotifier notifier) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final fee = state.fee; final minAmount = state.minWithdrawAmount; final available = state.withdrawableBalance; // 到账金额计算 final inputAmount = Decimal.tryParse(_amountController.text) ?? Decimal.zero; final receiveAmount = inputAmount - fee; final receiveDisplay = receiveAmount > Decimal.zero ? receiveAmount.toString() : '0.00'; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 选择币种 _Label(AppLocalizations.of(context)!.selectCoin), const SizedBox(height: 8), _FixedCoinField(coin: 'USDT'), const SizedBox(height: 16), // 提币网络 Row( children: [ _Label(AppLocalizations.of(context)!.withdrawNetwork), const SizedBox(width: 4), GestureDetector( onTap: () => _showNetworkTipDialog(context), child: Icon(Icons.info_outline, size: 14, color: cs.onSurface.withAlpha(120)), ), ], ), const SizedBox(height: 8), Row( children: List.generate(state.networkNames.length, (i) { final selected = i == state.selectedNetworkIndex; return Padding( padding: const EdgeInsets.only(right: 10), child: GestureDetector( onTap: () => notifier.selectNetwork(i), child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), decoration: BoxDecoration( color: selected ? AppColors.brand : Colors.transparent, border: Border.all(color: selected ? AppColors.brand : cs.outline.withAlpha(80)), borderRadius: BorderRadius.circular(8), ), child: Text(state.networkNames[i], style: TextStyle( color: selected ? Colors.black : cs.onSurface, fontSize: 14, fontWeight: selected ? FontWeight.w600 : FontWeight.w400, )), ), ), ); }), ), const SizedBox(height: 16), // 提币地址 _Label(AppLocalizations.of(context)!.withdrawAddress), const SizedBox(height: 8), _InputField( controller: _addressController, hint: AppLocalizations.of(context)!.enterWithdrawAddress, suffixIcon: Icon(Icons.qr_code_scanner, size: 20, color: cs.onSurface.withAlpha(153)), onSuffixIconTap: _scanQrCode, ), const SizedBox(height: 16), // 提币金额 Row( children: [ _Label(AppLocalizations.of(context)!.withdrawAmount), const SizedBox(width: 4), Icon(Icons.info_outline, size: 14, color: cs.onSurface.withAlpha(120)), ], ), const SizedBox(height: 8), _InputField( controller: _amountController, hint: AppLocalizations.of(context)!.withdrawMinAmountHint(minAmount), keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,4}'))], onChanged: (_) => setState(() {}), suffix: Row( mainAxisSize: MainAxisSize.min, children: [ Text('USDT', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 14)), const SizedBox(width: 8), GestureDetector( onTap: () { _amountController.text = available.toString(); setState(() {}); }, child: Text(AppLocalizations.of(context)!.max, style: const TextStyle(color: AppColors.brand, fontSize: 14, fontWeight: FontWeight.w600)), ), ], ), ), const SizedBox(height: 6), Row( children: [ Text('${AppLocalizations.of(context)!.fundAccountAvailable}:', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 12)), Flexible(child: Text('$available USDT ', overflow: TextOverflow.ellipsis, style: TextStyle(color: cs.onSurface, fontSize: 12, fontWeight: FontWeight.w600))), GestureDetector( onTap: () async { await context.push('/asset/transfer'); if (mounted) ref.read(withdrawProvider.notifier).refresh(); }, child: Text(AppLocalizations.of(context)!.transfer, style: const TextStyle(color: AppColors.brand, fontSize: 12, fontWeight: FontWeight.w500)), ), ], ), const SizedBox(height: 20), // 到账数量 _Label(AppLocalizations.of(context)!.receivedAmount), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: isDark ? AppColors.darkBgTertiary : AppColors.lightBgTertiary, borderRadius: BorderRadius.circular(8)), child: Text('$receiveDisplay USDT', style: TextStyle(color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w600)), ), const SizedBox(height: 16), // 手续费 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _Label(AppLocalizations.of(context)!.fee), Text('$fee USDT', style: TextStyle(color: cs.onSurface, fontSize: 14, fontWeight: FontWeight.w600)), ], ), const SizedBox(height: 20), // 安全验证 _buildSecurityFields(context, state, notifier), const SizedBox(height: 24), // 提币按钮 SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: state.isSubmitting ? null : () => _submitOnChain(notifier), style: ElevatedButton.styleFrom( backgroundColor: AppColors.brand, disabledBackgroundColor: AppColors.brand.withAlpha(80), foregroundColor: Colors.black, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)), ), child: state.isSubmitting ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black)) : Text(AppLocalizations.of(context)!.withdrawCoin, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), ), ), const SizedBox(height: 16), ..._notices(context).map((n) => Padding( padding: const EdgeInsets.only(bottom: 4), child: Text('· $n', style: TextStyle(color: cs.onSurface.withAlpha(100), fontSize: 11, height: 1.4)), )), ], ); } // ══════════════════════════════════════════════════════════ // 内部转账 // ══════════════════════════════════════════════════════════ Widget _buildInternal(BuildContext context, WithdrawState state, WithdrawNotifier notifier) { final cs = Theme.of(context).colorScheme; final available = state.transferableBalance; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _Label(AppLocalizations.of(context)!.selectCoin), const SizedBox(height: 8), _FixedCoinField(coin: 'USDT'), const SizedBox(height: 16), _Label(AppLocalizations.of(context)!.recipientUidOrAccount), const SizedBox(height: 8), _InputField( controller: _addressController, hint: AppLocalizations.of(context)!.enterRecipientUid, ), const SizedBox(height: 16), _Label(AppLocalizations.of(context)!.withdrawAmount), const SizedBox(height: 8), _InputField( controller: _amountController, hint: AppLocalizations.of(context)!.transferMinAmountHint(state.transferMinAmount), keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,4}'))], suffix: Row( mainAxisSize: MainAxisSize.min, children: [ Text('USDT', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 14)), const SizedBox(width: 8), GestureDetector( onTap: () => _amountController.text = available.toString(), child: Text(AppLocalizations.of(context)!.max, style: const TextStyle(color: AppColors.brand, fontSize: 14, fontWeight: FontWeight.w600)), ), ], ), ), const SizedBox(height: 6), Row( children: [ Text('${AppLocalizations.of(context)!.fundAccountAvailable}:', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 12)), Flexible(child: Text('$available USDT ', overflow: TextOverflow.ellipsis, style: TextStyle(color: cs.onSurface, fontSize: 12, fontWeight: FontWeight.w600))), GestureDetector( onTap: () async { await context.push('/asset/transfer'); if (mounted) ref.read(withdrawProvider.notifier).refresh(); }, child: Text(AppLocalizations.of(context)!.transfer, style: const TextStyle(color: AppColors.brand, fontSize: 12, fontWeight: FontWeight.w500)), ), ], ), const SizedBox(height: 20), _buildSecurityFields(context, state, notifier), const SizedBox(height: 24), SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: state.isSubmitting ? null : () => _submitTransfer(notifier), style: ElevatedButton.styleFrom( backgroundColor: AppColors.brand, foregroundColor: Colors.black, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)), ), child: state.isSubmitting ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black)) : Text(AppLocalizations.of(context)!.transfer, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), ), ), const SizedBox(height: 16), ..._notices(context).map((n) => Padding( padding: const EdgeInsets.only(bottom: 4), child: Text('· $n', style: TextStyle(color: cs.onSurface.withAlpha(100), fontSize: 11, height: 1.4)), )), ], ); } // ── 安全验证字段(链上/内部共用)─────────────────────── Widget _buildSecurityFields(BuildContext context, WithdrawState state, WithdrawNotifier notifier) { final cs = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(AppLocalizations.of(context)!.securityVerification, style: TextStyle(color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w600)), const SizedBox(height: 12), _InputField( controller: _fundPwdController, hint: AppLocalizations.of(context)!.fundPassword, obscure: _obscureFundPwd, maxLength: 16, suffixIcon: GestureDetector( onTap: () => setState(() => _obscureFundPwd = !_obscureFundPwd), child: Icon( _obscureFundPwd ? Icons.visibility_off_outlined : Icons.visibility_outlined, size: 20, color: cs.onSurface.withAlpha(153), ), ), ), const SizedBox(height: 10), _InputField( controller: _emailCodeController, hint: AppLocalizations.of(context)!.emailCode, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], maxLength: 6, suffix: GestureDetector( onTap: state.codeCountdown > 0 ? null : () async { final error = await notifier.sendEmailCode( address: _addressController.text, amount: _amountController.text, ); if (error != null && mounted) { final l10n = AppLocalizations.of(context)!; showTopToast(context, message: resolveProviderError(error, l10n) ?? error, backgroundColor: AppColors.fall); } }, child: Text( state.codeCountdown > 0 ? '${state.codeCountdown}s' : AppLocalizations.of(context)!.sendCode, style: TextStyle( color: state.codeCountdown > 0 ? cs.onSurface.withAlpha(100) : AppColors.brand, fontSize: 13, ), ), ), ), const SizedBox(height: 10), _InputField( controller: _googleCodeController, hint: AppLocalizations.of(context)!.googleCode, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], maxLength: 6, suffix: GestureDetector( onTap: () async { final data = await Clipboard.getData('text/plain'); if (data?.text != null) { _googleCodeController.text = data!.text!; } }, child: Text(AppLocalizations.of(context)!.paste, style: const TextStyle(color: AppColors.brand, fontSize: 13)), ), ), ], ); } // ── 提交逻辑 ──────────────────────────────────────── void _submitOnChain(WithdrawNotifier notifier) async { final error = notifier.validate( address: _addressController.text, amount: _amountController.text, jyPassword: _fundPwdController.text, vcode: _emailCodeController.text, vcode2: _googleCodeController.text, ); if (error != null) { final l10n = AppLocalizations.of(context)!; showTopToast(context, message: resolveProviderError(error, l10n) ?? error, backgroundColor: AppColors.fall); return; } try { final success = await notifier.submitOnChainWithdraw( address: _addressController.text, amount: _amountController.text, jyPassword: _fundPwdController.text, vcode: _emailCodeController.text, vcode2: _googleCodeController.text, ); if (!mounted) return; if (success) { showTopToast(context, message: AppLocalizations.of(context)!.withdrawSubmitted, backgroundColor: AppColors.rise); _clearFields(); notifier.refresh(); } else { final state = ref.read(withdrawProvider); if (state.errorMessage != null) { final l10nE = AppLocalizations.of(context)!; showTopToast(context, message: resolveProviderError(state.errorMessage!, l10nE) ?? state.errorMessage!, backgroundColor: AppColors.fall); } } } catch (e) { if (mounted) showTopToast(context, message: extractErrorMessage(e), backgroundColor: AppColors.fall); } } void _submitTransfer(WithdrawNotifier notifier) async { final error = notifier.validate( address: _addressController.text, amount: _amountController.text, jyPassword: _fundPwdController.text, vcode: _emailCodeController.text, vcode2: _googleCodeController.text, ); if (error != null) { final l10n = AppLocalizations.of(context)!; showTopToast(context, message: resolveProviderError(error, l10n) ?? error, backgroundColor: AppColors.fall); return; } try { final success = await notifier.submitInternalTransfer( address: _addressController.text, amount: _amountController.text, jyPassword: _fundPwdController.text, vcode: _emailCodeController.text, vcode2: _googleCodeController.text, ); if (!mounted) return; if (success) { showTopToast(context, message: AppLocalizations.of(context)!.transferSuccess2, backgroundColor: AppColors.rise); _clearFields(); notifier.refresh(); } else { final state = ref.read(withdrawProvider); if (state.errorMessage != null) { final l10nE = AppLocalizations.of(context)!; showTopToast(context, message: resolveProviderError(state.errorMessage!, l10nE) ?? state.errorMessage!, backgroundColor: AppColors.fall); } } } catch (e) { if (mounted) showTopToast(context, message: extractErrorMessage(e), backgroundColor: AppColors.fall); } } void _clearFields() { _addressController.clear(); _amountController.clear(); _fundPwdController.clear(); _emailCodeController.clear(); _googleCodeController.clear(); } /// 扫描二维码 Future _scanQrCode() async { try { final result = await context.push('/qr-scanner'); if (result != null && result.isNotEmpty) { _addressController.text = result; } } catch (e) { showTopToast(context, message: AppLocalizations.of(context)!.scannerFailed, backgroundColor: AppColors.fall); } } List _notices(BuildContext context) { final l10n = AppLocalizations.of(context)!; return [l10n.withdrawNotice1, l10n.withdrawNotice2]; } } // ── 共享组件 ───────────────────────────────────────────────── class _FixedCoinField extends StatelessWidget { const _FixedCoinField({required this.coin}); final String coin; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), decoration: BoxDecoration( border: Border.all(color: cs.outline.withAlpha(60)), borderRadius: BorderRadius.circular(10), ), child: Row( children: [ Text(coin, style: TextStyle(color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w500)), const Spacer(), Icon(Icons.keyboard_arrow_down, color: cs.onSurface.withAlpha(153)), ], ), ); } } class _Label extends StatelessWidget { const _Label(this.text); final String text; @override Widget build(BuildContext context) { return Text(text, style: TextStyle(color: Theme.of(context).colorScheme.onSurface.withAlpha(153), fontSize: 13)); } } class _InputField extends StatelessWidget { const _InputField({ required this.controller, required this.hint, this.obscure = false, this.keyboardType, this.suffix, this.suffixIcon, this.onSuffixIconTap, this.onChanged, this.inputFormatters, this.maxLength, }); final TextEditingController controller; final String hint; final bool obscure; final TextInputType? keyboardType; final Widget? suffix; final Widget? suffixIcon; final VoidCallback? onSuffixIconTap; final ValueChanged? onChanged; final List? inputFormatters; final int? maxLength; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return TextField( controller: controller, obscureText: obscure, keyboardType: keyboardType, onChanged: onChanged, inputFormatters: inputFormatters, maxLength: maxLength, style: TextStyle(color: cs.onSurface, fontSize: 14), decoration: InputDecoration( hintText: hint, hintStyle: TextStyle(color: cs.onSurface.withAlpha(100), fontSize: 14), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), suffixIcon: suffixIcon != null ? GestureDetector( onTap: onSuffixIconTap, child: Padding(padding: const EdgeInsets.only(right: 12), child: suffixIcon), ) : null, suffix: suffix, suffixIconConstraints: const BoxConstraints(minHeight: 20), ), ); } }