Files
monisuo/flutter_monisuo/lib/ui/pages/asset/transfer_page.dart
sion123 f5ac578892 docs(theme): update documentation and clean up deprecated color scheme definitions
Removed outdated compatibility aliases and deprecated methods from AppColorScheme,
and updated CLAUDE.md to reflect new theme system requirements with centralized
color management and no hard-coded values in UI components.
2026-04-05 23:37:27 +08:00

498 lines
15 KiB
Dart

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_color_scheme.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;
// ============================================
// 业务逻辑
// ============================================
/// 执行划转
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 colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colorScheme.surface,
appBar: AppBar(
backgroundColor: colorScheme.surfaceContainer,
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),
),
centerTitle: true,
),
body: Consumer<AssetProvider>(
builder: (context, provider, _) {
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(AppSpacing.md, AppSpacing.md, AppSpacing.md, AppSpacing.xl),
child: Column(
children: [
_buildTransferDirectionCard(),
const SizedBox(height: AppSpacing.lg),
_buildAmountSection(),
const SizedBox(height: AppSpacing.lg),
_buildTipsCard(),
const SizedBox(height: AppSpacing.lg),
_buildConfirmButton(),
],
),
);
},
),
);
}
// ============================================
// Transfer direction card
// ============================================
Widget _buildTransferDirectionCard() {
final colorScheme = Theme.of(context).colorScheme;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
color: colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(AppRadius.xl),
border: Border.all(color: colorScheme.outlineVariant.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,
),
),
// Swap button
GestureDetector(
onTap: _toggleDirection,
child: Container(
width: 36,
height: 36,
margin: const EdgeInsets.symmetric(vertical: AppSpacing.md),
decoration: BoxDecoration(
color: colorScheme.secondary,
shape: BoxShape.circle,
),
child: Center(
child: Icon(LucideIcons.arrowUpDown, size: 18, color: colorScheme.onSecondary),
),
),
),
// Destination account
_animatedSwitcher(
key: 'dst-$_direction',
beginOffset: const Offset(0, 1),
child: _buildAccountRow(
label: '',
accountName: _toLabel,
balance: _toBalance,
),
),
],
),
);
}
/// 统一的 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,
}) {
final colorScheme = Theme.of(context).colorScheme;
return SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: AppTextStyles.bodySmall(context)),
const SizedBox(height: AppSpacing.sm),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
label == '' ? LucideIcons.wallet : LucideIcons.repeat,
size: 18,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(width: AppSpacing.sm + 2),
Text(accountName, style: AppTextStyles.headlineMedium(context)),
],
),
Text(
'\u00A5 ${_formatBalance(balance)}',
style: AppTextStyles.headlineMedium(context),
),
],
),
],
),
);
}
// ============================================
// Amount input section
// ============================================
Widget _buildAmountSection() {
final colorScheme = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Label row: "划转金额" + "全部划转"
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('划转金额', style: AppTextStyles.headlineSmall(context).copyWith(color: colorScheme.onSurfaceVariant)),
GestureDetector(
onTap: () => _setQuickAmount(1.0),
child: Text('全部划转', style: AppTextStyles.labelLarge(context).copyWith(
color: colorScheme.secondary,
fontWeight: FontWeight.w600,
)),
),
],
),
const SizedBox(height: 12),
// Amount input field
GestureDetector(
onTap: () => _focusNode.requestFocus(),
child: Container(
width: double.infinity,
height: 56,
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHigh,
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: AppTextStyles.numberLarge(context),
decoration: InputDecoration(
hintText: '0.00',
hintStyle: AppTextStyles.numberLarge(context).copyWith(
color: AppColorScheme.darkOnSurfaceMuted,
),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true,
),
),
),
Padding(
padding: const EdgeInsets.only(left: AppSpacing.sm),
child: Text('USDT', style: AppTextStyles.headlineMedium(context).copyWith(
fontWeight: FontWeight.w400,
color: AppColorScheme.darkOnSurfaceMuted,
)),
),
],
),
),
),
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 ? AppSpacing.sm : 0),
child: _buildPercentButton(label, percent),
);
}).toList(),
),
],
);
}
Widget _buildPercentButton(String label, double percent) {
final colorScheme = Theme.of(context).colorScheme;
return Expanded(
child: GestureDetector(
onTap: () => _setQuickAmount(percent),
child: Container(
height: 36,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Center(
child: Text(label, style: AppTextStyles.headlineSmall(context)),
),
),
),
);
}
// ============================================
// Tips card & Confirm button
// ============================================
Widget _buildTipsCard() {
final upColor = AppColorScheme.getUpColor(_isDark);
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: 12),
decoration: BoxDecoration(
color: AppColorScheme.getUpBackgroundColor(_isDark),
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Row(
children: [
Icon(LucideIcons.info, size: 16, color: upColor),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
'划转即时到账,无需手续费',
style: AppTextStyles.bodyMedium(context).copyWith(color: upColor),
),
),
],
),
);
}
Widget _buildConfirmButton() {
final colorScheme = Theme.of(context).colorScheme;
return SizedBox(
width: double.infinity,
height: 52,
child: GestureDetector(
onTap: _isLoading ? null : _doTransfer,
child: Container(
decoration: BoxDecoration(
color: colorScheme.secondary,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Center(
child: _isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(colorScheme.onSecondary),
),
)
: Text(
'确认划转',
style: AppTextStyles.displayMedium(context).copyWith(
color: colorScheme.onSecondary,
fontSize: 16,
),
),
),
),
),
);
}
// ============================================
// Helpers
// ============================================
String _formatBalance(String balance) {
final val = double.tryParse(balance);
if (val == null) return '0.00';
return val.toStringAsFixed(2);
}
}