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.
616 lines
19 KiB
Dart
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,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|