Files
monisuo/flutter_monisuo/lib/ui/pages/asset/withdraw_page.dart
2026-04-23 00:44:39 +08:00

447 lines
13 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:provider/provider.dart';
import '../../../core/network/api_response.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../core/utils/toast_utils.dart';
import '../../../providers/asset_provider.dart';
import '../../components/material_input.dart';
/// 提现页面
class WithdrawPage extends StatefulWidget {
final String? balance;
const WithdrawPage({super.key, this.balance});
@override
State<WithdrawPage> createState() => _WithdrawPageState();
}
class _WithdrawPageState extends State<WithdrawPage> {
final _amountController = TextEditingController();
final _addressController = TextEditingController();
final _contactController = TextEditingController();
final _amountFocus = FocusNode();
bool _isSubmitting = false;
List<String> _networks = [];
String? _selectedNetwork;
@override
void initState() {
super.initState();
_amountController.addListener(() => setState(() {}));
_loadNetworks();
}
@override
void dispose() {
_amountController.dispose();
_addressController.dispose();
_contactController.dispose();
_amountFocus.dispose();
super.dispose();
}
// ============================================
// 业务逻辑
// ============================================
void _loadNetworks() {
context.read<AssetProvider>().getWalletNetworks().then((list) {
if (mounted) {
setState(() {
_networks = list;
if (list.isNotEmpty) _selectedNetwork = list.first;
});
}
});
}
String get _feeText {
final amount = double.tryParse(_amountController.text) ?? 0;
if (amount > 0) {
final fee = amount * 0.1;
final receivable = amount - fee;
return '手續費(10%): -${fee.toStringAsFixed(2)} USDT | 應收款: ${receivable.toStringAsFixed(2)} USDT';
}
return '提現將扣除 10% 手續費';
}
Future<void> _submitWithdraw() async {
final amount = double.tryParse(_amountController.text);
if (amount == null || amount <= 0) {
ToastUtils.showError('請輸入有效金額');
return;
}
final address = _addressController.text.trim();
if (address.isEmpty) {
ToastUtils.showError('請輸入提現地址');
return;
}
if (_isSubmitting) return;
// 45天规则检查
try {
final checkResult = await context.read<AssetProvider>().withdrawCheck();
if (checkResult != null && checkResult['isWithin45Days'] == true) {
if (!mounted) return;
final days = checkResult['depositDays'] ?? 0;
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('本金未滿45天提示'),
content: Text('您的充值本金尚未滿45天$days天)。'
'本金部分提現可能受到限制,是否繼續?'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('確認提現')),
],
),
);
if (confirmed != true) return;
}
} catch (_) {}
setState(() => _isSubmitting = true);
try {
final response = await context.read<AssetProvider>().withdraw(
amount: _amountController.text,
withdrawAddress: address,
withdrawContact: _contactController.text.trim().isNotEmpty
? _contactController.text.trim()
: null,
network: _selectedNetwork,
);
if (!mounted) return;
if (response.success) {
ToastUtils.showSuccess('申請成功,請等待管理員審批');
Navigator.of(context).pop(true);
} else {
ToastUtils.showError(response.message ?? '申請失敗');
}
} catch (e) {
if (mounted) ToastUtils.showError('申請失敗: $e');
} finally {
if (mounted) setState(() => _isSubmitting = false);
}
}
// ============================================
// 构建 UI
// ============================================
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colorScheme.surface,
appBar: AppBar(
leading: IconButton(
icon: Icon(LucideIcons.arrowLeft,
color: colorScheme.onSurface, size: 20),
onPressed: () => Navigator.of(context).pop(),
),
title: Text(
'提現',
style: AppTextStyles.headlineLarge(context).copyWith(
fontWeight: FontWeight.w600,
),
),
backgroundColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true,
),
body: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(
AppSpacing.md, AppSpacing.xs, AppSpacing.md, AppSpacing.xl),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 可用余额
if (widget.balance != null) ...[
_buildBalanceRow(colorScheme),
const SizedBox(height: AppSpacing.lg),
],
// 提现金额
_buildAmountInput(colorScheme),
const SizedBox(height: AppSpacing.sm),
// 手续费提示
_buildFeeTip(colorScheme),
const SizedBox(height: AppSpacing.lg),
// 提现网络
if (_networks.isNotEmpty) ...[
_buildSectionLabel(colorScheme, '提現網絡'),
const SizedBox(height: AppSpacing.sm),
_buildNetworkSelector(colorScheme),
const SizedBox(height: AppSpacing.lg),
],
// 目标地址
_buildAddressInput(colorScheme),
const SizedBox(height: AppSpacing.lg),
// 联系方式
_buildContactInput(colorScheme),
const SizedBox(height: AppSpacing.xl),
// 安全提示
_buildSecurityTips(colorScheme),
const SizedBox(height: AppSpacing.xl),
// 提交按钮
_buildSubmitButton(colorScheme),
const SizedBox(height: AppSpacing.md),
_buildBottomTip(colorScheme),
],
),
),
);
}
// ============================================
// 组件
// ============================================
Widget _buildBalanceRow(ColorScheme colorScheme) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'可用餘額',
style: AppTextStyles.bodyMedium(context).copyWith(
color: colorScheme.onSurfaceVariant,
),
),
Text(
'${widget.balance} USDT',
style: AppTextStyles.bodyMedium(context).copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
);
}
Widget _buildSectionLabel(ColorScheme colorScheme, String text) {
return Text(
text,
style: AppTextStyles.bodyMedium(context).copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
);
}
Widget _buildAmountInput(ColorScheme colorScheme) {
return MaterialInput(
controller: _amountController,
focusNode: _amountFocus,
labelText: '提現金額',
hintText: '0.00',
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,8}')),
],
suffixIcon: Padding(
padding: const EdgeInsets.only(right: AppSpacing.md),
child: Text(
'USDT',
style: AppTextStyles.bodyMedium(context).copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
),
);
}
Widget _buildFeeTip(ColorScheme colorScheme) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(LucideIcons.info,
size: 13, color: colorScheme.onSurfaceVariant),
const SizedBox(width: 6),
Expanded(
child: Text(
_feeText,
style: AppTextStyles.bodySmall(context).copyWith(
color: colorScheme.onSurfaceVariant,
),
),
),
],
),
);
}
Widget _buildNetworkSelector(ColorScheme colorScheme) {
return Container(
width: double.infinity,
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
width: 1,
),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _selectedNetwork,
isExpanded: true,
hint: Text(
'選擇提現網絡',
style: AppTextStyles.bodyMedium(context).copyWith(
color: colorScheme.onSurfaceVariant,
),
),
items: _networks
.map((n) => DropdownMenuItem(
value: n,
child: Text(n),
))
.toList(),
onChanged: (value) {
if (value != null) setState(() => _selectedNetwork = value);
},
),
),
);
}
Widget _buildAddressInput(ColorScheme colorScheme) {
return MaterialInput(
controller: _addressController,
labelText: '目標地址',
hintText: '請輸入提現地址',
);
}
Widget _buildContactInput(ColorScheme colorScheme) {
return MaterialInput(
controller: _contactController,
labelText: '聯繫方式(可選)',
hintText: '方便客服與您聯繫',
);
}
Widget _buildSecurityTips(ColorScheme colorScheme) {
final tips = [
'請仔細核對提現地址,地址錯誤將導致資產無法找回',
'提現申請提交後需等待管理員審批',
'提現將扣除 10% 手續費',
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: tips
.map((t) => Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'',
style: AppTextStyles.bodySmall(context).copyWith(
color: colorScheme.onSurfaceVariant,
),
),
Expanded(
child: Text(
t,
style: AppTextStyles.bodySmall(context).copyWith(
color: colorScheme.onSurfaceVariant,
),
),
),
],
),
))
.toList(),
);
}
Widget _buildSubmitButton(ColorScheme colorScheme) {
return SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: _isSubmitting ? null : _submitWithdraw,
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.onSurface,
foregroundColor: colorScheme.surface,
disabledBackgroundColor: colorScheme.surfaceContainerHighest,
disabledForegroundColor: colorScheme.onSurfaceVariant,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: _isSubmitting
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
colorScheme.onSurfaceVariant,
),
),
)
: Text(
'提交申請',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
),
);
}
Widget _buildBottomTip(ColorScheme colorScheme) {
return Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(LucideIcons.shieldCheck,
size: 13, color: colorScheme.onSurfaceVariant),
const SizedBox(width: 4),
Text(
'資金安全由平台全程保障',
style: AppTextStyles.bodySmall(context).copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
);
}
}