feat(mobile): add USDT withdraw feature with Google Authenticator verification

1. Trading page:
   - Add withdraw/transfer button below DST balance section
   - Display USDT balance from wallet-service
   - Navigate to withdraw page on button tap

2. Withdraw USDT page (new):
   - Network selection (KAVA / BSC)
   - Wallet address input with paste support
   - Amount input with max button
   - Fee calculation (0.1%) and actual amount preview
   - Input validation and notice section

3. Withdraw confirm page (new):
   - Transaction details summary
   - Google Authenticator 6-digit code verification
   - Success dialog with navigation back to trading

4. Routes:
   - Add /withdraw/usdt and /withdraw/confirm routes
   - Configure route parameters for WithdrawUsdtParams

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-11 03:47:22 -08:00
parent c162ccced9
commit ca5e903724
6 changed files with 1501 additions and 0 deletions

View File

@ -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<TradingPage> {
// 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<TradingPage> {
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<TradingPage> {
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<TradingPage> {
),
);
}
/// /
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>(Color(0xFFD4AF37)),
),
)
: Text(
'USDT 余额: ${_formatNumber(_usdtBalance)}',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
height: 1.5,
color: Color(0x995D4037),
),
);
}
}

View File

@ -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<WithdrawConfirmPage> createState() => _WithdrawConfirmPageState();
}
class _WithdrawConfirmPageState extends ConsumerState<WithdrawConfirmPage> {
///
final List<TextEditingController> _codeControllers = List.generate(
6,
(index) => TextEditingController(),
);
///
final List<FocusNode> _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<void> _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<Color>(Colors.white),
),
)
: const Text(
'确认提款',
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
letterSpacing: 0.24,
color: Colors.white,
),
),
),
),
);
}
}

View File

@ -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<WithdrawUsdtPage> createState() => _WithdrawUsdtPageState();
}
class _WithdrawUsdtPageState extends ConsumerState<WithdrawUsdtPage> {
///
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<void> _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>(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,
),
),
),
],
),
);
}
}

View File

@ -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<GoRouter>((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,

View File

@ -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';

View File

@ -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';