Move skills system documentation from bottom to top of CLAUDE.md for better organization. Refactor Flutter asset page by extracting UI components into separate files and updating import structure for improved modularity.
518 lines
16 KiB
Dart
518 lines
16 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:lucide_icons_flutter/lucide_icons.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();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
context.read<AssetProvider>().refreshAll(force: true);
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_amountController.dispose();
|
|
_focusNode.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
// ============================================
|
|
// 数据访问
|
|
// ============================================
|
|
|
|
/// 获取资金账户余额
|
|
String get _fundBalance {
|
|
final provider = context.read<AssetProvider>();
|
|
return provider.fundAccount?.balance ?? provider.overview?.fundBalance ?? '0.00';
|
|
}
|
|
|
|
/// 获取交易账户 USDT 余额
|
|
String get _tradeUsdtBalance {
|
|
final provider = context.read<AssetProvider>();
|
|
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 usdtHolding.quantity;
|
|
}
|
|
|
|
/// 获取当前可用余额(根据方向)
|
|
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;
|
|
|
|
// ============================================
|
|
// 主题辅助
|
|
// ============================================
|
|
|
|
bool get _isDark => Theme.of(context).brightness == Brightness.dark;
|
|
|
|
/// 一次性获取所有主题感知颜色
|
|
_TransferColors get _colors => _TransferColors(_isDark);
|
|
|
|
TextStyle _inter({
|
|
required double fontSize,
|
|
required FontWeight fontWeight,
|
|
required Color color,
|
|
}) {
|
|
return GoogleFonts.inter(fontSize: fontSize, fontWeight: fontWeight, color: color);
|
|
}
|
|
|
|
// ============================================
|
|
// 业务逻辑
|
|
// ============================================
|
|
|
|
/// 执行划转
|
|
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('划转成功');
|
|
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)),
|
|
);
|
|
}
|
|
|
|
/// 设置快捷百分比金额
|
|
void _setQuickAmount(double percent) {
|
|
final available = double.tryParse(_availableBalance) ?? 0;
|
|
final amount = available * percent;
|
|
_amountController.text = amount.toStringAsFixed(8).replaceAll(RegExp(r'\.?0+$'), '');
|
|
}
|
|
|
|
/// 切换方向
|
|
void _toggleDirection() {
|
|
setState(() {
|
|
_direction = _direction == 1 ? 2 : 1;
|
|
});
|
|
}
|
|
|
|
// ============================================
|
|
// 构建 UI
|
|
// ============================================
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final c = _colors;
|
|
|
|
return Scaffold(
|
|
backgroundColor: c.bgSecondary,
|
|
appBar: AppBar(
|
|
backgroundColor: c.surfaceCard,
|
|
elevation: 0,
|
|
scrolledUnderElevation: 0,
|
|
leading: IconButton(
|
|
icon: Icon(LucideIcons.arrowLeft, color: c.textPrimary, size: 20),
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
),
|
|
title: Text(
|
|
'账户划转',
|
|
style: _inter(fontSize: 16, fontWeight: FontWeight.w600, color: c.textPrimary),
|
|
),
|
|
centerTitle: true,
|
|
),
|
|
body: Consumer<AssetProvider>(
|
|
builder: (context, provider, _) {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
|
|
child: Column(
|
|
children: [
|
|
_buildTransferDirectionCard(c),
|
|
const SizedBox(height: 24),
|
|
_buildAmountSection(c),
|
|
const SizedBox(height: 24),
|
|
_buildTipsCard(c),
|
|
const SizedBox(height: 24),
|
|
_buildConfirmButton(c),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// Transfer direction card
|
|
// ============================================
|
|
|
|
Widget _buildTransferDirectionCard(_TransferColors c) {
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: c.surfaceCard,
|
|
borderRadius: BorderRadius.circular(AppRadius.xl),
|
|
border: Border.all(color: c.borderDefault.withValues(alpha: 0.6)),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// Source account
|
|
_animatedSwitcher(
|
|
key: 'src-$_direction',
|
|
beginOffset: const Offset(0, -1),
|
|
child: _buildAccountRow(
|
|
label: '从',
|
|
accountName: _fromLabel,
|
|
balance: _fromBalance,
|
|
c: c,
|
|
),
|
|
),
|
|
|
|
// Swap button
|
|
GestureDetector(
|
|
onTap: _toggleDirection,
|
|
child: Container(
|
|
width: 36,
|
|
height: 36,
|
|
margin: const EdgeInsets.symmetric(vertical: 16),
|
|
decoration: BoxDecoration(
|
|
color: c.accentPrimary,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Center(
|
|
child: Icon(LucideIcons.arrowUpDown, size: 18, color: c.textInverse),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Destination account
|
|
_animatedSwitcher(
|
|
key: 'dst-$_direction',
|
|
beginOffset: const Offset(0, 1),
|
|
child: _buildAccountRow(
|
|
label: '到',
|
|
accountName: _toLabel,
|
|
balance: _toBalance,
|
|
c: c,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 统一的 AnimatedSwitcher 构造
|
|
Widget _animatedSwitcher({
|
|
required String key,
|
|
required Offset beginOffset,
|
|
required Widget child,
|
|
}) {
|
|
return AnimatedSwitcher(
|
|
duration: const Duration(milliseconds: 300),
|
|
switchInCurve: Curves.easeInOut,
|
|
switchOutCurve: Curves.easeInOut,
|
|
transitionBuilder: (widget, animation) {
|
|
return SlideTransition(
|
|
position: Tween<Offset>(begin: beginOffset, end: Offset.zero).animate(animation),
|
|
child: FadeTransition(opacity: animation, child: widget),
|
|
);
|
|
},
|
|
child: KeyedSubtree(key: ValueKey(key), child: child),
|
|
);
|
|
}
|
|
|
|
/// Single account row inside the direction card
|
|
Widget _buildAccountRow({
|
|
required String label,
|
|
required String accountName,
|
|
required String balance,
|
|
required _TransferColors c,
|
|
}) {
|
|
return SizedBox(
|
|
width: double.infinity,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(label, style: _inter(fontSize: 11, fontWeight: FontWeight.normal, color: c.textMuted)),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
label == '从' ? LucideIcons.wallet : LucideIcons.repeat,
|
|
size: 18,
|
|
color: c.textSecondary,
|
|
),
|
|
const SizedBox(width: 10),
|
|
Text(accountName, style: _inter(fontSize: 14, fontWeight: FontWeight.w600, color: c.textPrimary)),
|
|
],
|
|
),
|
|
Text(
|
|
'\u00A5 ${_formatBalance(balance)}',
|
|
style: _inter(fontSize: 14, fontWeight: FontWeight.w600, color: c.textPrimary),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// Amount input section
|
|
// ============================================
|
|
|
|
Widget _buildAmountSection(_TransferColors c) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Label row: "划转金额" + "全部划转"
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text('划转金额', style: _inter(fontSize: 14, fontWeight: FontWeight.w500, color: c.textSecondary)),
|
|
GestureDetector(
|
|
onTap: () => _setQuickAmount(1.0),
|
|
child: Text('全部划转', style: _inter(fontSize: 12, fontWeight: FontWeight.w600, color: c.goldAccent)),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// Amount input field
|
|
GestureDetector(
|
|
onTap: () => _focusNode.requestFocus(),
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: 56,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
decoration: BoxDecoration(
|
|
color: c.bgTertiary,
|
|
borderRadius: BorderRadius.circular(AppRadius.lg),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _amountController,
|
|
focusNode: _focusNode,
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,8}')),
|
|
],
|
|
style: _inter(fontSize: 28, fontWeight: FontWeight.w700, color: c.textPrimary),
|
|
decoration: InputDecoration(
|
|
hintText: '0.00',
|
|
hintStyle: _inter(fontSize: 28, fontWeight: FontWeight.w700, color: c.textMuted),
|
|
border: InputBorder.none,
|
|
contentPadding: EdgeInsets.zero,
|
|
isDense: true,
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 8),
|
|
child: Text('USDT', style: _inter(fontSize: 14, fontWeight: FontWeight.normal, color: c.textMuted)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// Percent buttons
|
|
Row(
|
|
children: [0.25, 0.50, 0.75, 1.0].asMap().entries.map((entry) {
|
|
final index = entry.key;
|
|
final percent = entry.value;
|
|
final label = '${(percent * 100).toInt()}%';
|
|
return Padding(
|
|
padding: EdgeInsets.only(left: index > 0 ? 8 : 0),
|
|
child: _buildPercentButton(label, percent, c),
|
|
);
|
|
}).toList(),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildPercentButton(String label, double percent, _TransferColors c) {
|
|
return Expanded(
|
|
child: GestureDetector(
|
|
onTap: () => _setQuickAmount(percent),
|
|
child: Container(
|
|
height: 36,
|
|
decoration: BoxDecoration(
|
|
color: c.bgTertiary,
|
|
borderRadius: BorderRadius.circular(AppRadius.sm),
|
|
),
|
|
child: Center(
|
|
child: Text(label, style: _inter(fontSize: 13, fontWeight: FontWeight.w500, color: c.textSecondary)),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// Tips card & Confirm button
|
|
// ============================================
|
|
|
|
Widget _buildTipsCard(_TransferColors c) {
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: c.profitGreenBg,
|
|
borderRadius: BorderRadius.circular(AppRadius.lg),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(LucideIcons.info, size: 16, color: c.profitGreen),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'划转即时到账,无需手续费',
|
|
style: _inter(fontSize: 12, fontWeight: FontWeight.normal, color: c.profitGreen),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildConfirmButton(_TransferColors c) {
|
|
return SizedBox(
|
|
width: double.infinity,
|
|
height: 52,
|
|
child: GestureDetector(
|
|
onTap: _isLoading ? null : _doTransfer,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: c.accentPrimary,
|
|
borderRadius: BorderRadius.circular(AppRadius.lg),
|
|
),
|
|
child: Center(
|
|
child: _isLoading
|
|
? SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
valueColor: AlwaysStoppedAnimation<Color>(c.textInverse),
|
|
),
|
|
)
|
|
: Text(
|
|
'确认划转',
|
|
style: _inter(fontSize: 16, fontWeight: FontWeight.w700, color: c.textInverse),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// Helpers
|
|
// ============================================
|
|
|
|
String _formatBalance(String balance) {
|
|
final val = double.tryParse(balance);
|
|
if (val == null) return '0.00';
|
|
return val.toStringAsFixed(2);
|
|
}
|
|
}
|
|
|
|
/// 主题感知颜色集合,避免在 build() 中重复定义大量局部变量
|
|
class _TransferColors {
|
|
final Color bgSecondary;
|
|
final Color surfaceCard;
|
|
final Color bgTertiary;
|
|
final Color borderDefault;
|
|
final Color textPrimary;
|
|
final Color textSecondary;
|
|
final Color textMuted;
|
|
final Color textInverse;
|
|
final Color accentPrimary;
|
|
final Color goldAccent;
|
|
final Color profitGreen;
|
|
final Color profitGreenBg;
|
|
|
|
_TransferColors(bool isDark)
|
|
: bgSecondary = isDark ? const Color(0xFF0B1120) : const Color(0xFFF8FAFC),
|
|
surfaceCard = isDark ? const Color(0xFF0F172A) : const Color(0xFFFFFFFF),
|
|
bgTertiary = isDark ? const Color(0xFF1E293B) : const Color(0xFFF1F5F9),
|
|
borderDefault = isDark ? const Color(0xFF334155) : const Color(0xFFE2E8F0),
|
|
textPrimary = isDark ? const Color(0xFFF8FAFC) : const Color(0xFF0F172A),
|
|
textSecondary = isDark ? const Color(0xFF94A3B8) : const Color(0xFF475569),
|
|
textMuted = isDark ? const Color(0xFF64748B) : const Color(0xFF94A3B8),
|
|
textInverse = isDark ? const Color(0xFF0F172A) : const Color(0xFFFFFFFF),
|
|
accentPrimary = isDark ? const Color(0xFFD4AF37) : const Color(0xFF1F2937),
|
|
goldAccent = isDark ? const Color(0xFFD4AF37) : const Color(0xFFF59E0B),
|
|
profitGreen = isDark ? const Color(0xFF4ADE80) : const Color(0xFF16A34A),
|
|
profitGreenBg = isDark ? const Color(0xFF052E16) : const Color(0xFFF0FDF4);
|
|
}
|