Files
monisuo/flutter_monisuo/lib/ui/pages/asset/transfer_page.dart
sion123 d8cd38c4de feat(theme): update color scheme with new Slate theme and improved surface hierarchy
Updated the app's color scheme to implement a new "Slate" theme with refined dark and light variants. Changed background colors from #0A0E14 to #0B1120 for dark mode and updated surface layer colors to follow Material Design 3 specifications. Modified text colors and outline variants for better contrast and accessibility. Updated font sizes in transaction details screen from 11px to 12px for improved readability.
2026-04-05 22:24:04 +08:00

616 lines
19 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_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 {
return _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('划转成功');
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;
});
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
// Theme-aware colors matching .pen design tokens
final bgSecondary = isDark ? const Color(0xFF0B1120) : const Color(0xFFF8FAFC);
final surfaceCard = isDark ? const Color(0xFF0F172A) : const Color(0xFFFFFFFF);
final bgTertiary = isDark ? const Color(0xFF1E293B) : const Color(0xFFF1F5F9);
final borderDefault = isDark ? const Color(0xFF334155) : const Color(0xFFE2E8F0);
final textPrimary = isDark ? const Color(0xFFF8FAFC) : const Color(0xFF0F172A);
final textSecondary = isDark ? const Color(0xFF94A3B8) : const Color(0xFF475569);
final textMuted = isDark ? const Color(0xFF64748B) : const Color(0xFF94A3B8);
final textInverse = isDark ? const Color(0xFF0F172A) : const Color(0xFFFFFFFF);
final accentPrimary = isDark ? const Color(0xFFD4AF37) : const Color(0xFF1F2937);
final goldAccent = isDark ? const Color(0xFFD4AF37) : const Color(0xFFF59E0B);
final profitGreen = isDark ? const Color(0xFF4ADE80) : const Color(0xFF16A34A);
final profitGreenBg = isDark ? const Color(0xFF052E16) : const Color(0xFFF0FDF4);
return Scaffold(
backgroundColor: bgSecondary,
appBar: AppBar(
backgroundColor: isDark ? const Color(0xFF0F172A) : const Color(0xFFFFFFFF),
elevation: 0,
scrolledUnderElevation: 0,
leading: IconButton(
icon: Icon(LucideIcons.arrowLeft, color: textPrimary, size: 20),
onPressed: () => Navigator.of(context).pop(),
),
title: Text(
'账户划转',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: textPrimary,
),
),
centerTitle: true,
),
body: Consumer<AssetProvider>(
builder: (context, provider, _) {
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
child: Column(
children: [
// --- Transfer Direction Card ---
_buildTransferDirectionCard(
colorScheme: colorScheme,
isDark: isDark,
surfaceCard: surfaceCard,
borderDefault: borderDefault,
textPrimary: textPrimary,
textSecondary: textSecondary,
textMuted: textMuted,
textInverse: textInverse,
accentPrimary: accentPrimary,
),
const SizedBox(height: 24),
// --- Amount Section ---
_buildAmountSection(
isDark: isDark,
bgTertiary: bgTertiary,
textPrimary: textPrimary,
textSecondary: textSecondary,
textMuted: textMuted,
goldAccent: goldAccent,
),
const SizedBox(height: 24),
// --- Tips Card ---
_buildTipsCard(
profitGreen: profitGreen,
profitGreenBg: profitGreenBg,
),
const SizedBox(height: 24),
// --- Confirm Button ---
_buildConfirmButton(
accentPrimary: accentPrimary,
textInverse: textInverse,
),
],
),
);
},
),
);
}
/// Transfer direction card with source, swap, destination
Widget _buildTransferDirectionCard({
required ColorScheme colorScheme,
required bool isDark,
required Color surfaceCard,
required Color borderDefault,
required Color textPrimary,
required Color textSecondary,
required Color textMuted,
required Color textInverse,
required Color accentPrimary,
}) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: surfaceCard,
borderRadius: BorderRadius.circular(AppRadius.xl),
border: Border.all(color: borderDefault.withOpacity(0.6)),
),
child: Column(
children: [
// Source account
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
transitionBuilder: (widget, animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, -1),
end: Offset.zero,
).animate(animation),
child: FadeTransition(opacity: animation, child: widget),
);
},
child: _buildAccountRow(
key: ValueKey('src-$_direction'),
label: '',
accountName: _fromLabel,
balance: _fromBalance,
isDark: isDark,
textMuted: textMuted,
textPrimary: textPrimary,
textSecondary: textSecondary,
),
),
// Swap button
GestureDetector(
onTap: _toggleDirection,
child: Container(
width: 36,
height: 36,
margin: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: accentPrimary,
shape: BoxShape.circle,
),
child: Center(
child: Icon(
LucideIcons.arrowUpDown,
size: 18,
color: textInverse,
),
),
),
),
// Destination account
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
transitionBuilder: (widget, animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(animation),
child: FadeTransition(opacity: animation, child: widget),
);
},
child: _buildAccountRow(
key: ValueKey('dst-$_direction'),
label: '',
accountName: _toLabel,
balance: _toBalance,
isDark: isDark,
textMuted: textMuted,
textPrimary: textPrimary,
textSecondary: textSecondary,
),
),
],
),
);
}
/// Single account row inside the direction card
Widget _buildAccountRow({
Key? key,
required String label,
required String accountName,
required String balance,
required bool isDark,
required Color textMuted,
required Color textPrimary,
required Color textSecondary,
}) {
return Container(
key: key,
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Label row
Text(
label,
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.normal,
color: textMuted,
),
),
const SizedBox(height: 8),
// Account name + balance row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Account name with icon
Row(
children: [
Icon(
label == '' ? LucideIcons.wallet : LucideIcons.repeat,
size: 18,
color: textSecondary,
),
const SizedBox(width: 10),
Text(
accountName,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: textPrimary,
),
),
],
),
// Balance
Text(
'\u00A5 ${_formatBalance(balance)}',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: textPrimary,
),
),
],
),
],
),
);
}
/// Format balance for display
String _formatBalance(String balance) {
final val = double.tryParse(balance);
if (val == null) return '0.00';
return val.toStringAsFixed(2);
}
/// Amount input section
Widget _buildAmountSection({
required bool isDark,
required Color bgTertiary,
required Color textPrimary,
required Color textSecondary,
required Color textMuted,
required Color goldAccent,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Label row: "划转金额" + "全部划转"
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'划转金额',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
color: textSecondary,
),
),
GestureDetector(
onTap: () => _setQuickAmount(1.0),
child: Text(
'全部划转',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
color: 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: bgTertiary,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Input
Expanded(
child: TextField(
controller: _amountController,
focusNode: _focusNode,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,8}')),
],
style: GoogleFonts.inter(
fontSize: 28,
fontWeight: FontWeight.w700,
color: textPrimary,
),
decoration: InputDecoration(
hintText: '0.00',
hintStyle: GoogleFonts.inter(
fontSize: 28,
fontWeight: FontWeight.w700,
color: textMuted,
),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true,
),
),
),
// Suffix
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
'USDT',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.normal,
color: textMuted,
),
),
),
],
),
),
),
const SizedBox(height: 12),
// Percent buttons
Row(
children: [
_buildPercentButton('25%', 0.25, isDark, bgTertiary, textSecondary),
const SizedBox(width: 8),
_buildPercentButton('50%', 0.50, isDark, bgTertiary, textSecondary),
const SizedBox(width: 8),
_buildPercentButton('75%', 0.75, isDark, bgTertiary, textSecondary),
const SizedBox(width: 8),
_buildPercentButton('100%', 1.0, isDark, bgTertiary, textSecondary),
],
),
],
);
}
/// Percent quick button
Widget _buildPercentButton(String label, double percent, bool isDark, Color bgTertiary, Color textSecondary) {
return Expanded(
child: GestureDetector(
onTap: () => _setQuickAmount(percent),
child: Container(
height: 36,
decoration: BoxDecoration(
color: bgTertiary,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Center(
child: Text(
label,
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w500,
color: textSecondary,
),
),
),
),
),
);
}
/// Tips card with green background
Widget _buildTipsCard({
required Color profitGreen,
required Color profitGreenBg,
}) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: profitGreenBg,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Row(
children: [
Icon(
LucideIcons.info,
size: 16,
color: profitGreen,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'划转即时到账,无需手续费',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: profitGreen,
),
),
),
],
),
);
}
/// Confirm button
Widget _buildConfirmButton({
required Color accentPrimary,
required Color textInverse,
}) {
return SizedBox(
width: double.infinity,
height: 52,
child: GestureDetector(
onTap: _isLoading ? null : _doTransfer,
child: Container(
decoration: BoxDecoration(
color: accentPrimary,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Center(
child: _isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(textInverse),
),
)
: Text(
'确认划转',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w700,
color: textInverse,
),
),
),
),
),
);
}
}