import 'package:flutter/material.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:provider/provider.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../../core/theme/app_color_scheme.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../data/models/coin.dart'; import '../../../providers/market_provider.dart'; import '../../../providers/asset_provider.dart'; import '../../../data/services/trade_service.dart'; import '../../components/glass_panel.dart'; import '../../components/neon_glow.dart'; /// 交易页面 class TradePage extends StatefulWidget { final String? initialCoinCode; const TradePage({super.key, this.initialCoinCode}); @override State createState() => _TradePageState(); } class _TradePageState extends State with AutomaticKeepAliveClientMixin { int _tradeType = 0; // 0=买入, 1=卖出 Coin? _selectedCoin; final _amountController = TextEditingController(); bool _isSubmitting = false; @override bool get wantKeepAlive => true; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) => _loadData()); } void _loadData() { final marketProvider = context.read(); marketProvider.loadCoins().then((_) { if (widget.initialCoinCode != null && _selectedCoin == null) { final coins = marketProvider.allCoins; final coin = coins.firstWhere( (c) => c.code.toUpperCase() == widget.initialCoinCode!.toUpperCase(), orElse: () => coins.isNotEmpty ? coins.first : throw Exception('No coins'), ); if (mounted) setState(() => _selectedCoin = coin); } }); context.read().refreshAll(force: true); } @override void dispose() { _amountController.dispose(); super.dispose(); } /// 获取交易账户中 USDT 可用余额 String get _availableUsdt { final holdings = context.read().holdings; final usdt = holdings.where((h) => h.coinCode == 'USDT').firstOrNull; return usdt?.quantity ?? '0'; } /// 获取交易账户中当前币种的持仓数量 String get _availableCoinQty { if (_selectedCoin == null) return '0'; final holdings = context.read().holdings; final pos = holdings .where((h) => h.coinCode == _selectedCoin!.code) .firstOrNull; return pos?.quantity ?? '0'; } /// 计算可买入/卖出的最大 USDT 金额 String get _maxAmount { if (_selectedCoin == null) return '0'; final price = _selectedCoin!.price; if (price <= 0) return '0'; if (_tradeType == 0) { return _availableUsdt; } else { final qty = double.tryParse(_availableCoinQty) ?? 0; return (qty * price).toStringAsFixed(2); } } /// 计算数量 String get _calculatedQuantity { final amount = double.tryParse(_amountController.text) ?? 0; final price = _selectedCoin?.price ?? 0; if (price <= 0 || amount <= 0) return '0'; return (amount / price).toStringAsFixed(6); } @override Widget build(BuildContext context) { super.build(context); final colorScheme = Theme.of(context).colorScheme; return Scaffold( backgroundColor: colorScheme.background, body: Consumer2( builder: (context, market, asset, _) { return SingleChildScrollView( padding: AppSpacing.pagePadding, child: Column( children: [ // 币种选择器 _CoinSelector( selectedCoin: _selectedCoin, coins: market.allCoins .where((c) => c.code != 'USDT' && c.code != 'BTC' && c.code != 'ETH') .toList(), onCoinSelected: (coin) { setState(() { _selectedCoin = coin; _amountController.clear(); }); }, ), SizedBox(height: AppSpacing.md), // 价格卡片 if (_selectedCoin != null) _PriceCard(coin: _selectedCoin!) else const _PlaceholderCard(message: '请先选择交易币种'), SizedBox(height: AppSpacing.md), // 交易表单 _TradeFormCard( tradeType: _tradeType, selectedCoin: _selectedCoin, amountController: _amountController, availableUsdt: _availableUsdt, availableCoinQty: _availableCoinQty, calculatedQuantity: _calculatedQuantity, maxAmount: _maxAmount, onTradeTypeChanged: (type) => setState(() { _tradeType = type; _amountController.clear(); }), onAmountChanged: () => setState(() {}), onFillPercent: (pct) => _fillPercent(pct), ), SizedBox(height: AppSpacing.lg), // 买入 + 卖出双按钮 Row( children: [ Expanded( child: _TradeButton( isBuy: true, coinCode: _selectedCoin?.code, enabled: _canTrade() && !_isSubmitting, isLoading: _isSubmitting && _tradeType == 0, onPressed: () { _tradeType = 0; _executeTrade(); }, ), ), SizedBox(width: AppSpacing.md), Expanded( child: _TradeButton( isBuy: false, coinCode: _selectedCoin?.code, enabled: _canTrade() && !_isSubmitting, isLoading: _isSubmitting && _tradeType == 1, onPressed: () { _tradeType = 1; _executeTrade(); }, ), ), ], ), ], ), ); }, ), ); } bool _canTrade() { if (_selectedCoin == null) return false; final amount = double.tryParse(_amountController.text) ?? 0; if (amount <= 0) return false; // 买入时校验不超过可用USDT if (_tradeType == 0) { final available = double.tryParse(_availableUsdt) ?? 0; if (amount > available) return false; } return true; } void _fillPercent(double pct) { final max = double.tryParse(_maxAmount) ?? 0; _amountController.text = (max * pct).toStringAsFixed(2); setState(() {}); } void _executeTrade() async { final isBuy = _tradeType == 0; final amount = _amountController.text; final quantity = _calculatedQuantity; final price = _selectedCoin!.price.toStringAsFixed(2); final coinCode = _selectedCoin!.code; final confirmed = await showDialog( context: context, builder: (ctx) => _ConfirmDialog( isBuy: isBuy, coinCode: coinCode, price: price, quantity: quantity, amount: amount, ), ); if (confirmed != true) return; setState(() => _isSubmitting = true); try { final tradeService = context.read(); final response = isBuy ? await tradeService.buy( coinCode: coinCode, price: price, quantity: quantity) : await tradeService.sell( coinCode: coinCode, price: price, quantity: quantity); if (!mounted) return; if (response.success) { _amountController.clear(); // 刷新资产数据 context.read().refreshAll(force: true); _showResultDialog(true, '${isBuy ? '买入' : '卖出'}成功', '$quantity $coinCode @ $price USDT'); } else { _showResultDialog(false, '交易失败', response.message ?? '请稍后重试'); } } catch (e) { if (mounted) { _showResultDialog(false, '交易失败', e.toString()); } } finally { if (mounted) setState(() => _isSubmitting = false); } } void _showResultDialog(bool success, String title, String message) { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; showShadDialog( context: context, builder: (ctx) => ShadDialog.alert( title: Row( children: [ NeonIcon( icon: success ? Icons.check_circle : Icons.error, color: success ? AppColorScheme.getUpColor(isDark) : colorScheme.error, size: 24, ), SizedBox(width: AppSpacing.sm), Text(title), ], ), description: Text(message), actions: [ ShadButton( child: const Text('确定'), onPressed: () => Navigator.of(ctx).pop(), ), ], ), ); } } /// 确认对话框 class _ConfirmDialog extends StatelessWidget { final bool isBuy; final String coinCode; final String price; final String quantity; final String amount; const _ConfirmDialog({ required this.isBuy, required this.coinCode, required this.price, required this.quantity, required this.amount, }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final actionColor = isBuy ? AppColorScheme.getUpColor(isDark) : AppColorScheme.getDownColor(isDark); return Dialog( backgroundColor: Colors.transparent, child: GlassPanel( borderRadius: BorderRadius.circular(AppRadius.lg), padding: EdgeInsets.all(AppSpacing.lg), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Text( '确认${isBuy ? '买入' : '卖出'}', style: GoogleFonts.spaceGrotesk( fontSize: 20, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), ), SizedBox(height: AppSpacing.lg), _dialogRow('交易对', '$coinCode/USDT', colorScheme), SizedBox(height: AppSpacing.sm), _dialogRow('委托价格', '$price USDT', colorScheme), SizedBox(height: AppSpacing.sm), _dialogRow('交易金额', '$amount USDT', colorScheme, valueColor: actionColor), SizedBox(height: AppSpacing.sm), _dialogRow('交易数量', '$quantity $coinCode', colorScheme), SizedBox(height: AppSpacing.lg), Row( children: [ Expanded( child: NeonButton( text: '取消', type: NeonButtonType.outline, onPressed: () => Navigator.of(context).pop(false), height: 44, showGlow: false, ), ), SizedBox(width: AppSpacing.sm), Expanded( child: NeonButton( text: '确认${isBuy ? '买入' : '卖出'}', type: isBuy ? NeonButtonType.tertiary : NeonButtonType.error, onPressed: () => Navigator.of(context).pop(true), height: 44, showGlow: true, ), ), ], ), ], ), ), ); } Widget _dialogRow(String label, String value, ColorScheme colorScheme, {Color? valueColor}) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: TextStyle( fontSize: 14, color: colorScheme.onSurfaceVariant)), Text(value, style: GoogleFonts.spaceGrotesk( fontSize: 14, fontWeight: FontWeight.w600, color: valueColor ?? colorScheme.onSurface, )), ], ); } } /// 币种选择器 class _CoinSelector extends StatelessWidget { final Coin? selectedCoin; final List coins; final ValueChanged onCoinSelected; const _CoinSelector({ required this.selectedCoin, required this.coins, required this.onCoinSelected, }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return GestureDetector( onTap: () => _showCoinPicker(context), child: GlassPanel( padding: EdgeInsets.all(AppSpacing.md), child: Row( children: [ _CoinAvatar(icon: selectedCoin?.displayIcon), SizedBox(width: AppSpacing.sm + AppSpacing.xs), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( selectedCoin != null ? '${selectedCoin!.code}/USDT' : '选择币种', style: GoogleFonts.spaceGrotesk( fontSize: 18, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), SizedBox(height: AppSpacing.xs), Text( selectedCoin?.name ?? '点击选择交易对', style: TextStyle( fontSize: 12, color: colorScheme.onSurfaceVariant, ), ), ], ), ), Icon(LucideIcons.chevronDown, color: colorScheme.onSurfaceVariant), ], ), ), ); } void _showCoinPicker(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (ctx) => Container( height: MediaQuery.of(ctx).size.height * 0.65, decoration: BoxDecoration( color: isDark ? colorScheme.surface : colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xxl)), ), child: Column( children: [ Container( margin: EdgeInsets.only(top: AppSpacing.sm), width: 40, height: 4, decoration: BoxDecoration( color: colorScheme.onSurfaceVariant.withOpacity(0.3), borderRadius: BorderRadius.circular(2), ), ), Padding( padding: EdgeInsets.all(AppSpacing.lg), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('选择币种', style: GoogleFonts.spaceGrotesk( fontSize: 20, fontWeight: FontWeight.bold, color: colorScheme.onSurface, )), GestureDetector( onTap: () => Navigator.of(ctx).pop(), child: Icon(LucideIcons.x, color: colorScheme.onSurfaceVariant), ), ], ), ), Divider(height: 1, color: colorScheme.outlineVariant.withOpacity(0.2)), Expanded( child: ListView.builder( padding: EdgeInsets.symmetric(vertical: AppSpacing.sm), itemCount: coins.length, itemBuilder: (listCtx, index) => _buildCoinItem(coins[index], context, listCtx), ), ), ], ), ), ); } Widget _buildCoinItem( Coin coin, BuildContext context, BuildContext sheetContext) { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final isSelected = selectedCoin?.code == coin.code; final changeColor = coin.isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.getDownColor(isDark); return GestureDetector( onTap: () { Navigator.of(sheetContext).pop(); onCoinSelected(coin); }, child: Container( padding: EdgeInsets.symmetric(horizontal: AppSpacing.lg, vertical: AppSpacing.md), color: isSelected ? colorScheme.primary.withOpacity(0.1) : Colors.transparent, child: Row( children: [ _CoinAvatar(icon: coin.displayIcon), SizedBox(width: AppSpacing.md), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 第一行:币种代码 + 价格 + 涨跌幅 Row( children: [ Text(coin.code, style: GoogleFonts.spaceGrotesk( fontSize: 15, fontWeight: FontWeight.bold, color: colorScheme.onSurface, )), SizedBox(width: AppSpacing.xs), Text('/USDT', style: TextStyle( fontSize: 11, color: colorScheme.onSurfaceVariant, )), const Spacer(), Text('\$${coin.formattedPrice}', style: GoogleFonts.spaceGrotesk( fontSize: 13, fontWeight: FontWeight.w600, color: colorScheme.onSurface, )), SizedBox(width: AppSpacing.sm), Container( padding: EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: changeColor.withOpacity(0.1), borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Text(coin.formattedChange, style: TextStyle( fontSize: 11, color: changeColor, fontWeight: FontWeight.w600, )), ), if (isSelected) ...[ SizedBox(width: AppSpacing.sm), Icon(LucideIcons.check, size: 16, color: colorScheme.primary), ], ], ), SizedBox(height: 3), // 第二行:币种名称 Text(coin.name, style: TextStyle( fontSize: 12, color: colorScheme.onSurfaceVariant, )), ], ), ), ], ), ), ); } } /// 币种头像 class _CoinAvatar extends StatelessWidget { final String? icon; const _CoinAvatar({this.icon}); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Container( width: 44, height: 44, decoration: BoxDecoration( color: colorScheme.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: colorScheme.primary.withOpacity(0.2)), ), child: Center( child: Text(icon ?? '?', style: TextStyle( fontSize: 20, color: colorScheme.primary, fontWeight: FontWeight.bold, )), ), ); } } /// 价格卡片 - 重新设计 class _PriceCard extends StatelessWidget { final Coin coin; const _PriceCard({required this.coin}); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final isUp = coin.isUp; final changeColor = isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.down; final changeBgColor = isUp ? AppColorScheme.getUpBackgroundColor(isDark) : colorScheme.error.withOpacity(0.1); return GlassPanel( padding: EdgeInsets.symmetric( horizontal: AppSpacing.lg, vertical: AppSpacing.md + AppSpacing.sm), child: Row( children: [ // 左侧:币种标签 + 价格 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: EdgeInsets.symmetric( horizontal: AppSpacing.sm, vertical: 2), decoration: BoxDecoration( color: colorScheme.primary.withOpacity(0.08), borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Text( '${coin.code}/USDT', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: colorScheme.primary, ), ), ), ], ), SizedBox(height: AppSpacing.sm), Text( '\$${coin.formattedPrice}', style: GoogleFonts.spaceGrotesk( fontSize: 30, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), ], ), ), // 右侧:涨跌幅 Container( padding: EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.sm + 2), decoration: BoxDecoration( color: changeBgColor, borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all(color: changeColor.withOpacity(0.15)), ), child: Column( children: [ Text( '24h', style: TextStyle( fontSize: 10, color: changeColor.withOpacity(0.7), fontWeight: FontWeight.w500, ), ), SizedBox(height: 2), Text( coin.formattedChange, style: TextStyle( fontSize: 16, color: changeColor, fontWeight: FontWeight.w700), ), ], ), ), ], ), ); } } /// 占位卡片 class _PlaceholderCard extends StatelessWidget { final String message; const _PlaceholderCard({required this.message}); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return GlassPanel( padding: EdgeInsets.all(AppSpacing.xl), child: Center( child: Text(message, style: TextStyle( color: colorScheme.onSurfaceVariant, fontSize: 14, )), ), ); } } /// 交易表单卡片 - 重新设计 class _TradeFormCard extends StatelessWidget { final int tradeType; final Coin? selectedCoin; final TextEditingController amountController; final String availableUsdt; final String availableCoinQty; final String calculatedQuantity; final String maxAmount; final ValueChanged onTradeTypeChanged; final VoidCallback onAmountChanged; final ValueChanged onFillPercent; const _TradeFormCard({ required this.tradeType, required this.selectedCoin, required this.amountController, required this.availableUsdt, required this.availableCoinQty, required this.calculatedQuantity, required this.maxAmount, required this.onTradeTypeChanged, required this.onAmountChanged, required this.onFillPercent, }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final isBuy = tradeType == 0; final actionColor = isBuy ? AppColorScheme.getUpColor(isDark) : AppColorScheme.getDownColor(isDark); return GlassPanel( padding: EdgeInsets.all(AppSpacing.lg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 买入/卖出切换 - 重新设计 Container( padding: EdgeInsets.all(4), decoration: BoxDecoration( color: colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(AppRadius.xl), ), child: Row( children: [ Expanded( child: _buildTypeButton( context: context, label: '买入', isActive: isBuy, color: AppColorScheme.buyButtonFill, icon: LucideIcons.trendingUp, onTap: () => onTradeTypeChanged(0), ), ), SizedBox(width: 4), Expanded( child: _buildTypeButton( context: context, label: '卖出', isActive: !isBuy, color: AppColorScheme.sellButtonFill, icon: LucideIcons.trendingDown, onTap: () => onTradeTypeChanged(1), ), ), ], ), ), SizedBox(height: AppSpacing.lg), // 交易金额输入 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('交易金额', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: colorScheme.onSurface, )), Text('USDT', style: GoogleFonts.spaceGrotesk( fontSize: 11, fontWeight: FontWeight.w700, color: actionColor.withOpacity(0.7), letterSpacing: 0.5, )), ], ), SizedBox(height: AppSpacing.sm), _AmountInput( amountController: amountController, maxAmount: maxAmount, isBuy: isBuy, actionColor: actionColor, onChanged: onAmountChanged, ), SizedBox(height: AppSpacing.sm), // 快捷比例按钮 - 药丸样式 Row( children: [ _buildPctButton('25%', 0.25, colorScheme, actionColor), SizedBox(width: 6), _buildPctButton('50%', 0.5, colorScheme, actionColor), SizedBox(width: 6), _buildPctButton('75%', 0.75, colorScheme, actionColor), SizedBox(width: 6), _buildPctButton('全部', 1.0, colorScheme, actionColor), ], ), SizedBox(height: AppSpacing.lg), // 预计数量 + 可用余额 - 卡片样式 Container( padding: EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all( color: colorScheme.outlineVariant.withOpacity(0.1)), ), child: Column( children: [ // 预计数量 Row( children: [ Icon(LucideIcons.calculator, size: 14, color: colorScheme.onSurfaceVariant), SizedBox(width: AppSpacing.sm), Text('预计数量', style: TextStyle( fontSize: 12, color: colorScheme.onSurfaceVariant, )), const Spacer(), Text( '$calculatedQuantity ${selectedCoin?.code ?? ''}', style: GoogleFonts.spaceGrotesk( fontSize: 14, fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), ], ), Padding( padding: EdgeInsets.symmetric(vertical: AppSpacing.sm), child: Divider( height: 1, color: colorScheme.outlineVariant.withOpacity(0.08)), ), // 可用余额 Row( children: [ Icon(LucideIcons.wallet, size: 14, color: colorScheme.onSurfaceVariant), SizedBox(width: AppSpacing.sm), Text(isBuy ? '可用 USDT' : '可用 ${selectedCoin?.code ?? ""}', style: TextStyle( fontSize: 12, color: colorScheme.onSurfaceVariant, )), const Spacer(), Text( isBuy ? '$availableUsdt USDT' : '$availableCoinQty ${selectedCoin?.code ?? ""}', style: GoogleFonts.spaceGrotesk( fontSize: 14, fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), ], ), ], ), ), ], ), ); } /// 买入/卖出切换按钮 - 实心填充 + 图标 Widget _buildTypeButton({ required BuildContext context, required String label, required bool isActive, required Color color, required IconData icon, required VoidCallback onTap, }) { return GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, padding: EdgeInsets.symmetric(vertical: AppSpacing.sm + 4), decoration: BoxDecoration( color: isActive ? color : Colors.transparent, borderRadius: BorderRadius.circular(AppRadius.lg), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 16, color: isActive ? Colors.white : color.withOpacity(0.5)), SizedBox(width: AppSpacing.xs), Text( label, style: TextStyle( color: isActive ? Colors.white : color.withOpacity(0.5), fontWeight: FontWeight.w700, fontSize: 15, letterSpacing: 0.5, ), ), ], ), ), ); } /// 百分比按钮 - 药丸样式 Widget _buildPctButton( String label, double pct, ColorScheme colorScheme, Color actionColor) { return Expanded( child: GestureDetector( onTap: () => onFillPercent(pct), child: Container( padding: EdgeInsets.symmetric(vertical: AppSpacing.sm - 2), decoration: BoxDecoration( color: actionColor.withOpacity(0.06), borderRadius: BorderRadius.circular(AppRadius.full), border: Border.all(color: actionColor.withOpacity(0.12)), ), child: Center( child: Text(label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: actionColor.withOpacity(0.8), )), ), ), ), ); } } /// 交易按钮 - 使用 NeonButton 风格 class _TradeButton extends StatelessWidget { final bool isBuy; final String? coinCode; final bool enabled; final bool isLoading; final VoidCallback onPressed; const _TradeButton({ required this.isBuy, required this.coinCode, required this.enabled, required this.isLoading, required this.onPressed, }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final fillColor = isBuy ? AppColorScheme.buyButtonFill : AppColorScheme.sellButtonFill; return GestureDetector( onTap: enabled ? onPressed : null, child: AnimatedContainer( duration: const Duration(milliseconds: 200), height: 52, decoration: BoxDecoration( color: enabled ? fillColor : colorScheme.onSurface.withOpacity(0.08), borderRadius: BorderRadius.circular(AppRadius.lg), ), child: Center( child: isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( isBuy ? LucideIcons.trendingUp : LucideIcons.trendingDown, size: 16, color: enabled ? Colors.white : colorScheme.onSurface.withOpacity(0.3), ), SizedBox(width: AppSpacing.xs), Text( '${isBuy ? '买入' : '卖出'} ${coinCode ?? ""}', style: GoogleFonts.spaceGrotesk( fontSize: 16, fontWeight: FontWeight.w700, color: enabled ? Colors.white : colorScheme.onSurface.withOpacity(0.3), letterSpacing: 0.5, ), ), ], ), ), ), ); } } /// 金额输入框(含超额提示) class _AmountInput extends StatefulWidget { final TextEditingController amountController; final String maxAmount; final bool isBuy; final Color actionColor; final VoidCallback onChanged; const _AmountInput({ required this.amountController, required this.maxAmount, required this.isBuy, required this.actionColor, required this.onChanged, }); @override State<_AmountInput> createState() => _AmountInputState(); } class _AmountInputState extends State<_AmountInput> { bool _isExceeded = false; void _checkLimit() { final input = double.tryParse(widget.amountController.text) ?? 0; final max = double.tryParse(widget.maxAmount) ?? 0; final exceeded = widget.isBuy && input > max && max > 0 && input > 0; if (exceeded != _isExceeded) { setState(() => _isExceeded = exceeded); } widget.onChanged(); } @override void initState() { super.initState(); widget.amountController.addListener(_checkLimit); } @override void dispose() { widget.amountController.removeListener(_checkLimit); super.dispose(); } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final warningColor = AppColorScheme.warning; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( decoration: BoxDecoration( color: colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(AppRadius.xl), border: Border.all( color: _isExceeded ? warningColor.withOpacity(0.5) : widget.actionColor.withOpacity(0.15), ), ), child: TextField( controller: widget.amountController, keyboardType: const TextInputType.numberWithOptions(decimal: true), onChanged: (_) => _checkLimit(), style: GoogleFonts.spaceGrotesk( fontSize: 22, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), decoration: InputDecoration( hintText: '0.00', hintStyle: TextStyle( color: colorScheme.outlineVariant.withOpacity(0.4)), border: InputBorder.none, contentPadding: EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.md, ), suffixIcon: Padding( padding: EdgeInsets.only(right: AppSpacing.sm), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( padding: EdgeInsets.symmetric( horizontal: AppSpacing.sm, vertical: AppSpacing.xs), decoration: BoxDecoration( color: widget.actionColor.withOpacity(0.08), borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Text('USDT', style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: widget.actionColor.withOpacity(0.7), )), ), ], ), ), suffixIconConstraints: const BoxConstraints(minWidth: 60), ), ), ), if (_isExceeded) Padding( padding: EdgeInsets.only(top: AppSpacing.xs), child: Row( children: [ Icon(Icons.error_outline, size: 13, color: warningColor), SizedBox(width: 4), Text( '超出可用USDT余额', style: TextStyle(fontSize: 11, color: warningColor), ), ], ), ), ], ); } }