diff --git a/frontend/mobile-app/lib/features/trading/presentation/pages/trading_page.dart b/frontend/mobile-app/lib/features/trading/presentation/pages/trading_page.dart index 19cafc2b..f1044039 100644 --- a/frontend/mobile-app/lib/features/trading/presentation/pages/trading_page.dart +++ b/frontend/mobile-app/lib/features/trading/presentation/pages/trading_page.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import '../../../../core/di/injection_container.dart'; +import '../../../../routes/route_paths.dart'; /// 结算币种枚举 enum SettlementCurrency { bnb, og, usdt, dst } @@ -21,6 +23,7 @@ class _TradingPageState extends ConsumerState { // 钱包数据(从 wallet-service 获取) double _settleableAmount = 0.0; double _dstBalance = 0.0; + double _usdtBalance = 0.0; bool _isLoading = true; bool _isSettling = false; @@ -47,11 +50,13 @@ class _TradingPageState extends ConsumerState { setState(() { _settleableAmount = summary.settleableUsdt; _dstBalance = wallet.balances.dst.available; + _usdtBalance = wallet.balances.usdt.available; _isLoading = false; }); debugPrint('[TradingPage] 数据加载成功:'); debugPrint('[TradingPage] 可结算 USDT: $_settleableAmount (from reward-service)'); debugPrint('[TradingPage] DST 余额: $_dstBalance (from wallet-service)'); + debugPrint('[TradingPage] USDT 余额: $_usdtBalance (from wallet-service)'); } } catch (e, stackTrace) { debugPrint('[TradingPage] 加载数据失败: $e'); @@ -253,6 +258,15 @@ class _TradingPageState extends ConsumerState { const SizedBox(height: 8), // DST余额显示 _buildDstBalance(), + const SizedBox(height: 24), + // 分隔线 + _buildDivider(), + const SizedBox(height: 24), + // 提款/转账按钮 + _buildWithdrawButton(), + const SizedBox(height: 8), + // USDT余额显示 + _buildUsdtBalance(), ], ), ), @@ -509,4 +523,77 @@ class _TradingPageState extends ConsumerState { ), ); } + + /// 构建提款/转账按钮 + Widget _buildWithdrawButton() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: GestureDetector( + onTap: () { + context.push(RoutePaths.withdrawUsdt); + }, + child: Container( + width: double.infinity, + height: 56, + decoration: BoxDecoration( + color: const Color(0xFFD4AF37), + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x4DD4AF37), + blurRadius: 14, + offset: Offset(0, 4), + ), + ], + ), + child: const Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.account_balance_wallet_outlined, + color: Colors.white, + size: 20, + ), + SizedBox(width: 8), + Text( + '提款 / 转账', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + height: 1.5, + letterSpacing: 0.24, + color: Colors.white, + ), + ), + ], + ), + ), + ), + ), + ); + } + + /// 构建USDT余额显示 + Widget _buildUsdtBalance() { + return _isLoading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Color(0xFFD4AF37)), + ), + ) + : Text( + 'USDT 余额: ${_formatNumber(_usdtBalance)}', + style: const TextStyle( + fontSize: 14, + fontFamily: 'Inter', + height: 1.5, + color: Color(0x995D4037), + ), + ); + } } diff --git a/frontend/mobile-app/lib/features/withdraw/presentation/pages/withdraw_confirm_page.dart b/frontend/mobile-app/lib/features/withdraw/presentation/pages/withdraw_confirm_page.dart new file mode 100644 index 00000000..5f8e07b8 --- /dev/null +++ b/frontend/mobile-app/lib/features/withdraw/presentation/pages/withdraw_confirm_page.dart @@ -0,0 +1,553 @@ +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 'withdraw_usdt_page.dart'; + +/// 提款确认页面 +/// 显示提款详情并进行谷歌验证器验证 +class WithdrawConfirmPage extends ConsumerStatefulWidget { + final WithdrawUsdtParams params; + + const WithdrawConfirmPage({ + super.key, + required this.params, + }); + + @override + ConsumerState createState() => _WithdrawConfirmPageState(); +} + +class _WithdrawConfirmPageState extends ConsumerState { + /// 验证码输入控制器列表 + final List _codeControllers = List.generate( + 6, + (index) => TextEditingController(), + ); + + /// 焦点节点列表 + final List _focusNodes = List.generate( + 6, + (index) => FocusNode(), + ); + + /// 是否正在提交 + bool _isSubmitting = false; + + /// 手续费率 + final double _feeRate = 0.001; + + @override + void dispose() { + for (final controller in _codeControllers) { + controller.dispose(); + } + for (final node in _focusNodes) { + node.dispose(); + } + super.dispose(); + } + + /// 返回上一页 + void _goBack() { + context.pop(); + } + + /// 获取验证码 + String _getCode() { + return _codeControllers.map((c) => c.text).join(); + } + + /// 计算手续费 + double _calculateFee() { + return widget.params.amount * _feeRate; + } + + /// 计算实际到账 + double _calculateActualAmount() { + return widget.params.amount - _calculateFee(); + } + + /// 获取网络名称 + String _getNetworkName(WithdrawNetwork network) { + switch (network) { + case WithdrawNetwork.kava: + return 'KAVA'; + case WithdrawNetwork.bsc: + return 'BSC (BNB Chain)'; + } + } + + /// 格式化地址(显示前后部分) + String _formatAddress(String address) { + if (address.length <= 16) return address; + return '${address.substring(0, 8)}...${address.substring(address.length - 8)}'; + } + + /// 提交提款 + Future _onSubmit() async { + final code = _getCode(); + + // 验证验证码 + if (code.length != 6) { + _showErrorSnackBar('请输入完整的6位验证码'); + return; + } + + setState(() { + _isSubmitting = true; + }); + + try { + debugPrint('[WithdrawConfirmPage] 开始提款...'); + debugPrint('[WithdrawConfirmPage] 金额: ${widget.params.amount} USDT'); + debugPrint('[WithdrawConfirmPage] 地址: ${widget.params.address}'); + debugPrint('[WithdrawConfirmPage] 网络: ${_getNetworkName(widget.params.network)}'); + debugPrint('[WithdrawConfirmPage] 验证码: $code'); + + // TODO: 调用 API 提交提款请求 + // final walletService = ref.read(walletServiceProvider); + // await walletService.withdrawUsdt( + // amount: widget.params.amount, + // address: widget.params.address, + // network: widget.params.network.name, + // totpCode: code, + // ); + + // 模拟请求 + await Future.delayed(const Duration(seconds: 2)); + + if (mounted) { + setState(() { + _isSubmitting = false; + }); + + // 显示成功弹窗 + _showSuccessDialog(); + } + } catch (e) { + debugPrint('[WithdrawConfirmPage] 提款失败: $e'); + if (mounted) { + setState(() { + _isSubmitting = false; + }); + _showErrorSnackBar('提款失败: ${e.toString()}'); + } + } + } + + /// 显示成功弹窗 + void _showSuccessDialog() { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 64, + height: 64, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Color(0xFF4CAF50), + ), + child: const Icon( + Icons.check, + size: 40, + color: Colors.white, + ), + ), + const SizedBox(height: 16), + const Text( + '提款申请已提交', + style: TextStyle( + fontSize: 18, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(height: 8), + const Text( + '预计 1-30 分钟内到账', + style: TextStyle( + fontSize: 14, + fontFamily: 'Inter', + color: Color(0xFF8B5A2B), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + // 返回交易页面 + context.go('/trading'); + }, + child: const Text( + '确定', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + color: Color(0xFFD4AF37), + ), + ), + ), + ], + ), + ); + } + + /// 显示错误提示 + void _showErrorSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: Container( + width: double.infinity, + height: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFFFFF5E6), + Color(0xFFFFE4B5), + ], + ), + ), + child: SafeArea( + child: Column( + children: [ + // 顶部导航栏 + _buildAppBar(), + // 内容区域 + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 提款详情卡片 + _buildDetailsCard(), + const SizedBox(height: 24), + + // 谷歌验证器验证 + _buildAuthenticatorSection(), + const SizedBox(height: 32), + + // 提交按钮 + _buildSubmitButton(), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + /// 构建顶部导航栏 + Widget _buildAppBar() { + return Container( + height: 64, + padding: const EdgeInsets.only(top: 16, left: 16, right: 16), + child: Row( + children: [ + // 返回按钮 + GestureDetector( + onTap: _goBack, + child: Container( + width: 48, + height: 48, + alignment: Alignment.center, + child: const Icon( + Icons.arrow_back, + size: 24, + color: Color(0xFF5D4037), + ), + ), + ), + // 标题 + const Expanded( + child: Text( + '确认提款', + style: TextStyle( + fontSize: 18, + fontFamily: 'Inter', + fontWeight: FontWeight.w500, + height: 1.25, + letterSpacing: -0.27, + color: Color(0xFF5D4037), + ), + textAlign: TextAlign.center, + ), + ), + // 占位 + const SizedBox(width: 48), + ], + ), + ); + } + + /// 构建提款详情卡片 + Widget _buildDetailsCard() { + final fee = _calculateFee(); + final actual = _calculateActualAmount(); + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0x80FFFFFF), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0x33D4AF37), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '提款详情', + style: TextStyle( + fontSize: 18, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(height: 16), + _buildDetailRow('提款网络', _getNetworkName(widget.params.network)), + const SizedBox(height: 12), + _buildDetailRow('提款地址', _formatAddress(widget.params.address)), + const SizedBox(height: 12), + _buildDetailRow('提款金额', '${widget.params.amount.toStringAsFixed(2)} USDT'), + const SizedBox(height: 12), + _buildDetailRow('手续费', '${fee.toStringAsFixed(2)} USDT'), + const Divider(color: Color(0x33D4AF37), height: 24), + _buildDetailRow( + '实际到账', + '${actual.toStringAsFixed(2)} USDT', + isHighlight: true, + ), + ], + ), + ); + } + + /// 构建详情行 + Widget _buildDetailRow(String label, String value, {bool isHighlight = false}) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 14, + fontFamily: 'Inter', + color: isHighlight ? const Color(0xFF5D4037) : const Color(0xFF745D43), + fontWeight: isHighlight ? FontWeight.w600 : FontWeight.normal, + ), + ), + const SizedBox(width: 16), + Flexible( + child: Text( + value, + style: TextStyle( + fontSize: isHighlight ? 18 : 14, + fontFamily: 'Inter', + fontWeight: isHighlight ? FontWeight.w700 : FontWeight.w500, + color: isHighlight ? const Color(0xFFD4AF37) : const Color(0xFF5D4037), + ), + textAlign: TextAlign.right, + ), + ), + ], + ); + } + + /// 构建谷歌验证器验证区域 + Widget _buildAuthenticatorSection() { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0x80FFFFFF), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0x33D4AF37), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: const [ + Icon( + Icons.security, + size: 24, + color: Color(0xFFD4AF37), + ), + SizedBox(width: 8), + Text( + '谷歌验证器', + style: TextStyle( + fontSize: 18, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + ), + ], + ), + const SizedBox(height: 8), + const Text( + '请输入谷歌验证器中的6位验证码', + style: TextStyle( + fontSize: 14, + fontFamily: 'Inter', + color: Color(0xFF8B5A2B), + ), + ), + const SizedBox(height: 20), + // 验证码输入框 + _buildCodeInput(), + ], + ), + ); + } + + /// 构建验证码输入框 + Widget _buildCodeInput() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(6, (index) { + return SizedBox( + width: 45, + height: 54, + child: TextField( + controller: _codeControllers[index], + focusNode: _focusNodes[index], + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + maxLength: 1, + style: const TextStyle( + fontSize: 24, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + color: Color(0xFF5D4037), + ), + decoration: InputDecoration( + counterText: '', + filled: true, + fillColor: const Color(0xFFFFF5E6), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide( + color: Color(0x33D4AF37), + width: 1, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide( + color: Color(0x33D4AF37), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide( + color: Color(0xFFD4AF37), + width: 2, + ), + ), + contentPadding: const EdgeInsets.symmetric(vertical: 12), + ), + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + onChanged: (value) { + if (value.isNotEmpty && index < 5) { + _focusNodes[index + 1].requestFocus(); + } + setState(() {}); + }, + ), + ); + }), + ); + } + + /// 构建提交按钮 + Widget _buildSubmitButton() { + final code = _getCode(); + final isValid = code.length == 6; + + return GestureDetector( + onTap: (isValid && !_isSubmitting) ? _onSubmit : null, + child: Container( + width: double.infinity, + height: 56, + decoration: BoxDecoration( + color: (isValid && !_isSubmitting) + ? const Color(0xFFD4AF37) + : const Color(0x80D4AF37), + borderRadius: BorderRadius.circular(12), + boxShadow: (isValid && !_isSubmitting) + ? const [ + BoxShadow( + color: Color(0x4DD4AF37), + blurRadius: 14, + offset: Offset(0, 4), + ), + ] + : null, + ), + child: Center( + child: _isSubmitting + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : const Text( + '确认提款', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + height: 1.5, + letterSpacing: 0.24, + color: Colors.white, + ), + ), + ), + ), + ); + } +} diff --git a/frontend/mobile-app/lib/features/withdraw/presentation/pages/withdraw_usdt_page.dart b/frontend/mobile-app/lib/features/withdraw/presentation/pages/withdraw_usdt_page.dart new file mode 100644 index 00000000..aa54cd6c --- /dev/null +++ b/frontend/mobile-app/lib/features/withdraw/presentation/pages/withdraw_usdt_page.dart @@ -0,0 +1,838 @@ +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/di/injection_container.dart'; +import '../../../../routes/route_paths.dart'; + +/// 提款网络枚举 +enum WithdrawNetwork { kava, bsc } + +/// 提款参数 +class WithdrawUsdtParams { + final double amount; + final String address; + final WithdrawNetwork network; + + WithdrawUsdtParams({ + required this.amount, + required this.address, + required this.network, + }); +} + +/// USDT 提款页面 +/// 支持选择 KAVA 或 BSC 网络进行 USDT 提款 +class WithdrawUsdtPage extends ConsumerStatefulWidget { + const WithdrawUsdtPage({super.key}); + + @override + ConsumerState createState() => _WithdrawUsdtPageState(); +} + +class _WithdrawUsdtPageState extends ConsumerState { + /// 提款地址控制器 + final TextEditingController _addressController = TextEditingController(); + + /// 提款金额控制器 + final TextEditingController _amountController = TextEditingController(); + + /// 当前选中的网络 + WithdrawNetwork _selectedNetwork = WithdrawNetwork.kava; + + /// USDT 余额 + double _usdtBalance = 0.0; + + /// 是否正在加载 + bool _isLoading = true; + + /// 手续费率 + final double _feeRate = 0.001; // 0.1% + + /// 最小提款金额 + final double _minAmount = 10.0; + + @override + void initState() { + super.initState(); + _loadWalletData(); + } + + @override + void dispose() { + _addressController.dispose(); + _amountController.dispose(); + super.dispose(); + } + + /// 加载钱包数据 + Future _loadWalletData() async { + try { + debugPrint('[WithdrawUsdtPage] 开始加载钱包数据...'); + + final walletService = ref.read(walletServiceProvider); + final wallet = await walletService.getMyWallet(); + + if (mounted) { + setState(() { + _usdtBalance = wallet.balances.usdt.available; + _isLoading = false; + }); + debugPrint('[WithdrawUsdtPage] USDT 余额: $_usdtBalance'); + } + } catch (e, stackTrace) { + debugPrint('[WithdrawUsdtPage] 加载数据失败: $e'); + debugPrint('[WithdrawUsdtPage] 堆栈: $stackTrace'); + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + /// 返回上一页 + void _goBack() { + context.pop(); + } + + /// 选择网络 + void _selectNetwork(WithdrawNetwork network) { + setState(() { + _selectedNetwork = network; + }); + } + + /// 设置全部金额 + void _setMaxAmount() { + _amountController.text = _usdtBalance.toStringAsFixed(2); + setState(() {}); + } + + /// 计算手续费 + double _calculateFee() { + final amount = double.tryParse(_amountController.text) ?? 0; + return amount * _feeRate; + } + + /// 计算实际到账 + double _calculateActualAmount() { + final amount = double.tryParse(_amountController.text) ?? 0; + return amount - _calculateFee(); + } + + /// 验证并提交 + void _onSubmit() { + final address = _addressController.text.trim(); + final amountText = _amountController.text.trim(); + + // 验证地址 + if (address.isEmpty) { + _showErrorSnackBar('请输入提款地址'); + return; + } + + // 验证地址格式 + if (!_isValidAddress(address)) { + _showErrorSnackBar('请输入有效的钱包地址'); + return; + } + + // 验证金额 + if (amountText.isEmpty) { + _showErrorSnackBar('请输入提款金额'); + return; + } + + final amount = double.tryParse(amountText); + if (amount == null || amount <= 0) { + _showErrorSnackBar('请输入有效的金额'); + return; + } + + if (amount < _minAmount) { + _showErrorSnackBar('最小提款金额为 $_minAmount USDT'); + return; + } + + if (amount > _usdtBalance) { + _showErrorSnackBar('余额不足'); + return; + } + + // 跳转到确认页面 + context.push( + RoutePaths.withdrawConfirm, + extra: WithdrawUsdtParams( + amount: amount, + address: address, + network: _selectedNetwork, + ), + ); + } + + /// 验证地址格式 + bool _isValidAddress(String address) { + // 简单的地址格式验证 + // KAVA 和 BSC 地址都是以 0x 开头的 42 位十六进制字符串 + if (_selectedNetwork == WithdrawNetwork.kava) { + // KAVA 地址格式:kava1... 或 0x... + return address.startsWith('kava1') || + (address.startsWith('0x') && address.length == 42); + } else { + // BSC 地址格式:0x... + return address.startsWith('0x') && address.length == 42; + } + } + + /// 显示错误提示 + void _showErrorSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red, + ), + ); + } + + /// 格式化数字(添加千分位) + String _formatNumber(double number) { + final parts = number.toStringAsFixed(2).split('.'); + final intPart = parts[0].replaceAllMapped( + RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), + (Match m) => '${m[1]},', + ); + return '$intPart.${parts[1]}'; + } + + /// 获取网络名称 + String _getNetworkName(WithdrawNetwork network) { + switch (network) { + case WithdrawNetwork.kava: + return 'KAVA'; + case WithdrawNetwork.bsc: + return 'BSC (BNB Chain)'; + } + } + + /// 获取网络描述 + String _getNetworkDescription(WithdrawNetwork network) { + switch (network) { + case WithdrawNetwork.kava: + return 'Kava EVM 网络'; + case WithdrawNetwork.bsc: + return 'BNB Smart Chain 网络'; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: Container( + width: double.infinity, + height: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFFFFF5E6), + Color(0xFFFFE4B5), + ], + ), + ), + child: SafeArea( + child: Column( + children: [ + // 顶部导航栏 + _buildAppBar(), + // 内容区域 + Expanded( + child: _isLoading + ? const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Color(0xFFD4AF37)), + ), + ) + : SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 余额卡片 + _buildBalanceCard(), + const SizedBox(height: 24), + + // 网络选择 + _buildNetworkSelector(), + const SizedBox(height: 24), + + // 提款地址 + _buildAddressInput(), + const SizedBox(height: 24), + + // 提款金额 + _buildAmountInput(), + const SizedBox(height: 16), + + // 手续费信息 + _buildFeeInfo(), + const SizedBox(height: 32), + + // 提交按钮 + _buildSubmitButton(), + const SizedBox(height: 16), + + // 注意事项 + _buildNotice(), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + /// 构建顶部导航栏 + Widget _buildAppBar() { + return Container( + height: 64, + padding: const EdgeInsets.only(top: 16, left: 16, right: 16), + child: Row( + children: [ + // 返回按钮 + GestureDetector( + onTap: _goBack, + child: Container( + width: 48, + height: 48, + alignment: Alignment.center, + child: const Icon( + Icons.arrow_back, + size: 24, + color: Color(0xFF5D4037), + ), + ), + ), + // 标题 + const Expanded( + child: Text( + '提款 USDT', + style: TextStyle( + fontSize: 18, + fontFamily: 'Inter', + fontWeight: FontWeight.w500, + height: 1.25, + letterSpacing: -0.27, + color: Color(0xFF5D4037), + ), + textAlign: TextAlign.center, + ), + ), + // 占位 + const SizedBox(width: 48), + ], + ), + ); + } + + /// 构建余额卡片 + Widget _buildBalanceCard() { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0x80FFFFFF), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0x33D4AF37), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '可用余额', + style: TextStyle( + fontSize: 14, + fontFamily: 'Inter', + color: Color(0xFF745D43), + ), + ), + const SizedBox(height: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + _formatNumber(_usdtBalance), + style: const TextStyle( + fontSize: 32, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + height: 1.2, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(width: 8), + const Padding( + padding: EdgeInsets.only(bottom: 4), + child: Text( + 'USDT', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w500, + color: Color(0xFF8B5A2B), + ), + ), + ), + ], + ), + ], + ), + ); + } + + /// 构建网络选择器 + Widget _buildNetworkSelector() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '选择网络', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(height: 12), + _buildNetworkOption(WithdrawNetwork.kava), + const SizedBox(height: 12), + _buildNetworkOption(WithdrawNetwork.bsc), + ], + ); + } + + /// 构建网络选项 + Widget _buildNetworkOption(WithdrawNetwork network) { + final isSelected = _selectedNetwork == network; + return GestureDetector( + onTap: () => _selectNetwork(network), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isSelected ? const Color(0x1AD4AF37) : const Color(0x80FFFFFF), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected ? const Color(0xFFD4AF37) : const Color(0x33D4AF37), + width: isSelected ? 2 : 1, + ), + ), + child: Row( + children: [ + // 选中图标 + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? const Color(0xFFD4AF37) : const Color(0xFF8B5A2B), + width: 2, + ), + color: isSelected ? const Color(0xFFD4AF37) : Colors.transparent, + ), + child: isSelected + ? const Icon( + Icons.check, + size: 16, + color: Colors.white, + ) + : null, + ), + const SizedBox(width: 12), + // 网络信息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _getNetworkName(network), + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + color: isSelected ? const Color(0xFF5D4037) : const Color(0xFF745D43), + ), + ), + const SizedBox(height: 4), + Text( + _getNetworkDescription(network), + style: const TextStyle( + fontSize: 12, + fontFamily: 'Inter', + color: Color(0xFF8B5A2B), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + /// 构建地址输入 + Widget _buildAddressInput() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '提款地址', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(height: 12), + Container( + width: double.infinity, + decoration: BoxDecoration( + color: const Color(0x80FFFFFF), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0x80FFFFFF), + width: 1, + ), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 2, + offset: Offset(0, 1), + ), + ], + ), + child: TextField( + controller: _addressController, + style: const TextStyle( + fontSize: 14, + fontFamily: 'Inter', + height: 1.4, + color: Color(0xFF5D4037), + ), + decoration: InputDecoration( + contentPadding: const EdgeInsets.all(16), + border: InputBorder.none, + hintText: _selectedNetwork == WithdrawNetwork.kava + ? '请输入 KAVA 或 EVM 地址' + : '请输入 BSC 地址 (0x...)', + hintStyle: const TextStyle( + fontSize: 14, + fontFamily: 'Inter', + height: 1.4, + color: Color(0x995D4037), + ), + suffixIcon: IconButton( + icon: const Icon( + Icons.content_paste, + color: Color(0xFF8B5A2B), + size: 20, + ), + onPressed: () async { + final data = await Clipboard.getData('text/plain'); + if (data?.text != null) { + _addressController.text = data!.text!; + } + }, + ), + ), + ), + ), + ], + ); + } + + /// 构建金额输入 + Widget _buildAmountInput() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '提款金额', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + ), + GestureDetector( + onTap: _setMaxAmount, + child: const Text( + '全部', + style: TextStyle( + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + color: Color(0xFFD4AF37), + ), + ), + ), + ], + ), + const SizedBox(height: 12), + Container( + width: double.infinity, + decoration: BoxDecoration( + color: const Color(0x80FFFFFF), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0x80FFFFFF), + width: 1, + ), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 2, + offset: Offset(0, 1), + ), + ], + ), + child: TextField( + controller: _amountController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}')), + ], + onChanged: (value) => setState(() {}), + style: const TextStyle( + fontSize: 18, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + height: 1.4, + color: Color(0xFF5D4037), + ), + decoration: const InputDecoration( + contentPadding: EdgeInsets.all(16), + border: InputBorder.none, + hintText: '请输入提款金额', + hintStyle: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + height: 1.4, + color: Color(0x995D4037), + ), + suffixText: 'USDT', + suffixStyle: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w500, + color: Color(0xFF8B5A2B), + ), + ), + ), + ), + const SizedBox(height: 8), + Text( + '最小提款金额: $_minAmount USDT', + style: const TextStyle( + fontSize: 12, + fontFamily: 'Inter', + color: Color(0xFF8B5A2B), + ), + ), + ], + ); + } + + /// 构建手续费信息 + Widget _buildFeeInfo() { + final fee = _calculateFee(); + final actual = _calculateActualAmount(); + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFFFF5E6), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0x33D4AF37), + width: 1, + ), + ), + child: Column( + children: [ + _buildFeeRow('手续费率', '${(_feeRate * 100).toStringAsFixed(1)}%'), + const SizedBox(height: 8), + _buildFeeRow('手续费', '${fee.toStringAsFixed(2)} USDT'), + const Divider(color: Color(0x33D4AF37), height: 24), + _buildFeeRow( + '预计到账', + '${actual > 0 ? actual.toStringAsFixed(2) : '0.00'} USDT', + isHighlight: true, + ), + ], + ), + ); + } + + /// 构建手续费行 + Widget _buildFeeRow(String label, String value, {bool isHighlight = false}) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: TextStyle( + fontSize: isHighlight ? 16 : 14, + fontFamily: 'Inter', + fontWeight: isHighlight ? FontWeight.w600 : FontWeight.normal, + color: const Color(0xFF745D43), + ), + ), + Text( + value, + style: TextStyle( + fontSize: isHighlight ? 18 : 14, + fontFamily: 'Inter', + fontWeight: isHighlight ? FontWeight.w700 : FontWeight.w500, + color: isHighlight ? const Color(0xFFD4AF37) : const Color(0xFF5D4037), + ), + ), + ], + ); + } + + /// 构建提交按钮 + Widget _buildSubmitButton() { + final amount = double.tryParse(_amountController.text) ?? 0; + final isValid = _addressController.text.isNotEmpty && + amount >= _minAmount && + amount <= _usdtBalance; + + return GestureDetector( + onTap: isValid ? _onSubmit : null, + child: Container( + width: double.infinity, + height: 56, + decoration: BoxDecoration( + color: isValid ? const Color(0xFFD4AF37) : const Color(0x80D4AF37), + borderRadius: BorderRadius.circular(12), + boxShadow: isValid + ? const [ + BoxShadow( + color: Color(0x4DD4AF37), + blurRadius: 14, + offset: Offset(0, 4), + ), + ] + : null, + ), + child: const Center( + child: Text( + '下一步', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + height: 1.5, + letterSpacing: 0.24, + color: Colors.white, + ), + ), + ), + ), + ); + } + + /// 构建注意事项 + Widget _buildNotice() { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0x1AFF9800), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0x33FF9800), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: const [ + Icon( + Icons.warning_amber_rounded, + size: 20, + color: Color(0xFFFF9800), + ), + SizedBox(width: 8), + Text( + '注意事项', + style: TextStyle( + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + color: Color(0xFFE65100), + ), + ), + ], + ), + const SizedBox(height: 12), + _buildNoticeItem('请确保提款地址正确,错误地址将导致资产丢失'), + _buildNoticeItem('请选择正确的网络,不同网络之间不可互转'), + _buildNoticeItem('提款需要进行谷歌验证器验证'), + _buildNoticeItem('提款通常在 1-30 分钟内到账'), + ], + ), + ); + } + + /// 构建注意事项项 + Widget _buildNoticeItem(String text) { + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '• ', + style: TextStyle( + fontSize: 12, + fontFamily: 'Inter', + color: Color(0xFF8B5A2B), + ), + ), + Expanded( + child: Text( + text, + style: const TextStyle( + fontSize: 12, + fontFamily: 'Inter', + color: Color(0xFF8B5A2B), + height: 1.5, + ), + ), + ), + ], + ), + ); + } +} diff --git a/frontend/mobile-app/lib/routes/app_router.dart b/frontend/mobile-app/lib/routes/app_router.dart index 4b0e1429..1c2e1f7c 100644 --- a/frontend/mobile-app/lib/routes/app_router.dart +++ b/frontend/mobile-app/lib/routes/app_router.dart @@ -22,6 +22,8 @@ import '../features/planting/presentation/pages/planting_location_page.dart'; import '../features/security/presentation/pages/google_auth_page.dart'; import '../features/security/presentation/pages/change_password_page.dart'; import '../features/security/presentation/pages/bind_email_page.dart'; +import '../features/withdraw/presentation/pages/withdraw_usdt_page.dart'; +import '../features/withdraw/presentation/pages/withdraw_confirm_page.dart'; import 'route_paths.dart'; import 'route_names.dart'; @@ -235,6 +237,23 @@ final appRouterProvider = Provider((ref) { builder: (context, state) => const BindEmailPage(), ), + // Withdraw USDT Page (USDT 提款) + GoRoute( + path: RoutePaths.withdrawUsdt, + name: RouteNames.withdrawUsdt, + builder: (context, state) => const WithdrawUsdtPage(), + ), + + // Withdraw Confirm Page (提款确认) + GoRoute( + path: RoutePaths.withdrawConfirm, + name: RouteNames.withdrawConfirm, + builder: (context, state) { + final params = state.extra as WithdrawUsdtParams; + return WithdrawConfirmPage(params: params); + }, + ), + // Main Shell with Bottom Navigation ShellRoute( navigatorKey: _shellNavigatorKey, diff --git a/frontend/mobile-app/lib/routes/route_names.dart b/frontend/mobile-app/lib/routes/route_names.dart index f62614a8..fde86abb 100644 --- a/frontend/mobile-app/lib/routes/route_names.dart +++ b/frontend/mobile-app/lib/routes/route_names.dart @@ -30,6 +30,8 @@ class RouteNames { static const changePassword = 'change-password'; static const bindEmail = 'bind-email'; static const transactionHistory = 'transaction-history'; + static const withdrawUsdt = 'withdraw-usdt'; + static const withdrawConfirm = 'withdraw-confirm'; // Share static const share = 'share'; diff --git a/frontend/mobile-app/lib/routes/route_paths.dart b/frontend/mobile-app/lib/routes/route_paths.dart index d88c1228..a81b827c 100644 --- a/frontend/mobile-app/lib/routes/route_paths.dart +++ b/frontend/mobile-app/lib/routes/route_paths.dart @@ -30,6 +30,8 @@ class RoutePaths { static const changePassword = '/security/password'; static const bindEmail = '/security/email'; static const transactionHistory = '/trading/history'; + static const withdrawUsdt = '/withdraw/usdt'; + static const withdrawConfirm = '/withdraw/confirm'; // Share static const share = '/share';