Files
monisuo/flutter_monisuo/lib/ui/pages/asset/transfer_page.dart
2026-04-08 02:13:29 +08:00

567 lines
17 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:provider/provider.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../providers/asset_provider.dart';
import '../../../data/models/account_models.dart';
/// 划转页面
class TransferPage extends StatefulWidget {
const TransferPage({super.key});
@override
State<TransferPage> createState() => _TransferPageState();
}
class _TransferPageState extends State<TransferPage> {
final _amountController = TextEditingController();
final _focusNode = FocusNode();
int _direction = 1; // 1: 资金→交易, 2: 交易→资金
bool _isLoading = false;
@override
void initState() {
super.initState();
_amountController.addListener(() => setState(() {}));
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<AssetProvider>().refreshAll(force: true);
});
}
@override
void dispose() {
_amountController.dispose();
_focusNode.dispose();
super.dispose();
}
// ============================================
// 数据访问
// ============================================
String get _fundBalance {
try {
final provider = context.read<AssetProvider>();
final balance = provider.fundAccount?.balance ??
provider.overview?.fundBalance ??
'0.00';
return _formatBalance(balance);
} catch (e) {
return '0.00';
}
}
String get _tradeUsdtBalance {
try {
final provider = context.read<AssetProvider>();
if (provider.tradeAccounts.isEmpty) return '0.00';
final usdtHolding = provider.tradeAccounts.firstWhere(
(t) => t.coinCode.toUpperCase() == 'USDT',
orElse: () => AccountTrade(
id: 0,
userId: 0,
coinCode: 'USDT',
quantity: '0',
avgPrice: '1',
totalCost: '0',
currentValue: '0',
profit: '0',
profitRate: 0,
),
);
return _formatBalance(usdtHolding.quantity);
} catch (e) {
return '0.00';
}
}
String get _availableBalance =>
_direction == 1 ? _fundBalance : _tradeUsdtBalance;
String get _fromLabel => _direction == 1 ? '資金賬戶' : '交易賬戶';
String get _toLabel => _direction == 1 ? '交易賬戶' : '資金賬戶';
String get _fromBalance => _direction == 1 ? _fundBalance : _tradeUsdtBalance;
String get _toBalance => _direction == 1 ? _tradeUsdtBalance : _fundBalance;
// ============================================
// 业务逻辑
// ============================================
Future<void> _doTransfer() async {
final amount = _amountController.text;
final available = double.tryParse(_availableBalance) ?? 0;
final transferAmount = double.tryParse(amount) ?? 0;
if (transferAmount <= 0) {
_showSnackBar('請輸入有效的劃轉金額');
return;
}
if (transferAmount > available) {
_showSnackBar('餘額不足');
return;
}
setState(() => _isLoading = true);
try {
final response = await context.read<AssetProvider>().transfer(
direction: _direction,
amount: amount,
);
if (mounted) {
if (response.success) {
_amountController.clear();
_showSnackBar('劃轉成功');
await Future.delayed(const Duration(milliseconds: 500));
if (mounted) Navigator.of(context).pop(true);
} else {
_showSnackBar(response.message ?? '劃轉失敗');
}
}
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
void _showSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
);
}
void _setQuickAmount(double percent) {
final available = double.tryParse(_availableBalance) ?? 0;
final amount = available * percent;
// 向下截斷到2位小數避免四捨五入超出餘額
_amountController.text =
((amount * 100).truncateToDouble() / 100).toStringAsFixed(2);
// Trigger haptic feedback
HapticFeedback.selectionClick();
}
void _toggleDirection() {
HapticFeedback.mediumImpact();
setState(() => _direction = _direction == 1 ? 2 : 1);
}
// ============================================
// 构建 UI
// ============================================
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colorScheme.surface,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
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,
),
),
centerTitle: true,
),
body: Consumer<AssetProvider>(
builder: (context, provider, _) {
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(
AppSpacing.md, AppSpacing.xs, AppSpacing.md, AppSpacing.xl),
child: Column(
children: [
_buildTransferCard(colorScheme),
const SizedBox(height: AppSpacing.md),
_buildAmountSection(colorScheme),
const SizedBox(height: AppSpacing.md),
_buildTips(colorScheme),
const SizedBox(height: AppSpacing.xl),
_buildConfirmButton(colorScheme),
const SizedBox(height: AppSpacing.lg),
],
),
);
},
),
);
}
// ============================================
// 划转方向卡片
// ============================================
Widget _buildTransferCard(ColorScheme colorScheme) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
width: 1,
),
),
child: Stack(
clipBehavior: Clip.none,
children: [
Column(
children: [
// From
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
'',
style: AppTextStyles.bodySmall(context).copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(width: 8),
Text(
_fromLabel,
style:
AppTextStyles.bodyLarge(context).copyWith(
fontWeight: FontWeight.w500,
),
),
],
),
Text(
_fromBalance,
style: AppTextStyles.bodyLarge(context).copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
),
Divider(
height: 1,
thickness: 1,
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
),
// To
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
'',
style: AppTextStyles.bodySmall(context).copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(width: 8),
Text(
_toLabel,
style:
AppTextStyles.bodyLarge(context).copyWith(
fontWeight: FontWeight.w500,
),
),
],
),
Text(
_toBalance,
style: AppTextStyles.bodyLarge(context).copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
// 交换按钮 - 右侧贴分割线
Positioned(
right: 12,
top: 20,
child: GestureDetector(
onTap: _toggleDirection,
child: Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: colorScheme.surface,
shape: BoxShape.circle,
border: Border.all(
color: colorScheme.outlineVariant,
width: 1,
),
),
child: Icon(
LucideIcons.arrowUpDown,
size: 14,
color: colorScheme.onSurfaceVariant,
),
),
),
),
],
),
);
}
// ============================================
// 金额输入区域
// ============================================
Widget _buildAmountSection(ColorScheme colorScheme) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题行
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'劃轉金額',
style: AppTextStyles.bodyMedium(context).copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
GestureDetector(
onTap: () => _setQuickAmount(1.0),
child: Text(
'全部',
style: AppTextStyles.bodyMedium(context).copyWith(
color: colorScheme.secondary,
fontWeight: FontWeight.w500,
),
),
),
],
),
const SizedBox(height: 12),
// 金额输入框
Container(
width: double.infinity,
height: 52,
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: _focusNode.hasFocus
? colorScheme.secondary
: colorScheme.outlineVariant.withValues(alpha: 0.5),
width: 1,
),
),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
child: TextField(
controller: _amountController,
focusNode: _focusNode,
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'^\d*\.?\d{0,8}')),
],
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
decoration: InputDecoration(
hintText: '0.00',
hintStyle: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.3),
),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true,
),
onChanged: (_) => setState(() {}),
),
),
Text(
'USDT',
style: AppTextStyles.bodyMedium(context).copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
],
),
),
const SizedBox(height: 10),
// 可用余额
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'可用餘額',
style: AppTextStyles.bodySmall(context).copyWith(
color: colorScheme.onSurfaceVariant,
),
),
Text(
'$_availableBalance USDT',
style: AppTextStyles.bodySmall(context).copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 12),
// 百分比按钮
Row(
children: [
_buildPercentButton('25%', 0.25, colorScheme),
const SizedBox(width: 8),
_buildPercentButton('50%', 0.50, colorScheme),
const SizedBox(width: 8),
_buildPercentButton('75%', 0.75, colorScheme),
const SizedBox(width: 8),
_buildPercentButton('100%', 1.0, colorScheme),
],
),
],
);
}
Widget _buildPercentButton(
String label, double percent, ColorScheme colorScheme) {
return Expanded(
child: GestureDetector(
onTap: () => _setQuickAmount(percent),
child: Container(
height: 34,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
label,
style: AppTextStyles.bodySmall(context).copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
),
),
),
);
}
// ============================================
// 提示文字
// ============================================
Widget _buildTips(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),
Text(
'劃轉即時到賬,無需手續費',
style: AppTextStyles.bodySmall(context).copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
);
}
// ============================================
// 确认按钮
// ============================================
Widget _buildConfirmButton(ColorScheme colorScheme) {
final hasAmount = _amountController.text.isNotEmpty &&
double.tryParse(_amountController.text) != null &&
double.parse(_amountController.text) > 0;
return SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: _isLoading ? null : _doTransfer,
style: ElevatedButton.styleFrom(
backgroundColor: hasAmount
? colorScheme.onSurface
: colorScheme.surfaceContainerHighest,
foregroundColor: hasAmount
? colorScheme.surface
: colorScheme.onSurfaceVariant,
disabledBackgroundColor: colorScheme.surfaceContainerHighest,
disabledForegroundColor: colorScheme.onSurfaceVariant,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: _isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
colorScheme.onSurfaceVariant,
),
),
)
: Text(
'確認劃轉',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
),
);
}
// ============================================
// Helpers
// ============================================
String _formatBalance(String balance) {
final val = double.tryParse(balance);
if (val == null) return '0.00';
return val.toStringAsFixed(2);
}
}