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 'package:lucide_icons_flutter/lucide_icons.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'; /// 交易页面 /// /// 设计稿 Trade 页面,布局结构: /// - 币种选择器卡片(Coin Selector Card) /// - 价格卡片(Price Card):大号价格 + 涨跌幅徽章 + 副标题 /// - 买入/卖出切换(Buy/Sell Toggle) /// - 交易表单卡片(Trade Form Card):金额输入 + 快捷比例 + 计算数量 /// - CTA 买入/卖出按钮(Buy/Sell Button) 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 SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB( AppSpacing.md, 0, AppSpacing.md, AppSpacing.xl + AppSpacing.sm, ), 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(); }); }, ), const SizedBox(height: AppSpacing.md), // 价格卡片 if (_selectedCoin != null) _PriceCard(coin: _selectedCoin!) else _PlaceholderCard( message: '请先选择交易币种', colorScheme: colorScheme, ), const 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), ), const SizedBox(height: AppSpacing.md), // CTA 买入/卖出按钮 SizedBox( width: double.infinity, height: 48, child: _TradeButton( isBuy: _tradeType == 0, coinCode: _selectedCoin?.code, enabled: _canTrade() && !_isSubmitting, isLoading: _isSubmitting, onPressed: _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.inter( fontSize: 16, 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: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.normal, color: colorScheme.onSurfaceVariant, )), Text(value, style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.w600, color: valueColor ?? colorScheme.onSurface, )), ], ); } } // ============================================ // 币种选择器 - 设计稿 Coin Selector Card // card背景 + 圆角lg + border + padding:16 // 横向布局:coinInfo(竖向 pair+name) + chevronDown // ============================================ 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; final isDark = Theme.of(context).brightness == Brightness.dark; return GestureDetector( onTap: () => _showCoinPicker(context), child: Container( width: double.infinity, padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: isDark ? colorScheme.surfaceContainer : colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all( color: colorScheme.outlineVariant.withOpacity(0.15), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // 币种信息:交易对 + 名称 Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( selectedCoin != null ? '${selectedCoin!.code}/USDT' : '选择币种', style: GoogleFonts.inter( fontSize: 18, fontWeight: FontWeight.w700, color: colorScheme.onSurface, ), ), const SizedBox(height: 2), Text( selectedCoin?.name ?? '点击选择交易对', style: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.normal, color: colorScheme.onSurfaceVariant, ), ), ], ), // 下拉箭头 Icon(LucideIcons.chevronDown, size: 16, 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.inter( fontSize: 16, 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: [ // 第一行:币种代码 + USDT + 价格 + 涨跌幅 Row( children: [ Text(coin.code, style: GoogleFonts.inter( fontSize: 15, fontWeight: FontWeight.bold, color: colorScheme.onSurface, )), SizedBox(width: AppSpacing.xs), Text('/USDT', style: GoogleFonts.inter( fontSize: 11, color: colorScheme.onSurfaceVariant, )), const Spacer(), Text('\$${coin.formattedPrice}', style: GoogleFonts.inter( 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: GoogleFonts.inter( 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: GoogleFonts.inter( 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, )), ), ); } } // ============================================ // 价格卡片 - 设计稿 Price Card // card背景 + 圆角lg + border + padding:20 + gap:8 // 竖向布局: // priceRow: 大号价格(32px bold) + 涨跌幅徽章(圆角sm,涨绿背景) // subtitle: "24h 变化" // ============================================ 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.getDownColor(isDark); final changeBgColor = isUp ? AppColorScheme.getUpBackgroundColor(isDark) : AppColorScheme.getDownBackgroundColor(isDark); return Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: isDark ? colorScheme.surfaceContainer : colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all( color: colorScheme.outlineVariant.withOpacity(0.15), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 价格行:大号价格 + 涨跌幅徽章 Row( children: [ Text( coin.formattedPrice, style: GoogleFonts.inter( fontSize: 32, fontWeight: FontWeight.w700, color: colorScheme.onSurface, fontFeatures: [FontFeature.tabularFigures()], ), ), const SizedBox(width: AppSpacing.sm), // 涨跌幅徽章 - 圆角sm,涨绿背景 Container( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.sm, vertical: AppSpacing.xs), decoration: BoxDecoration( color: changeBgColor, borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Text( coin.formattedChange, style: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.w600, color: changeColor, fontFeatures: [FontFeature.tabularFigures()], ), ), ), ], ), const SizedBox(height: AppSpacing.sm), // 副标题 Text( '24h 变化', style: GoogleFonts.inter( fontSize: 11, fontWeight: FontWeight.normal, color: colorScheme.onSurfaceVariant, ), ), ], ), ); } } /// 占位卡片 class _PlaceholderCard extends StatelessWidget { final String message; final ColorScheme colorScheme; const _PlaceholderCard({required this.message, required this.colorScheme}); @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return Container( width: double.infinity, padding: const EdgeInsets.all(AppSpacing.xl), decoration: BoxDecoration( color: isDark ? colorScheme.surfaceContainer : colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all( color: colorScheme.outlineVariant.withOpacity(0.15), ), ), child: Center( child: Text(message, style: GoogleFonts.inter( color: colorScheme.onSurfaceVariant, fontSize: 14, )), ), ); } } // ============================================ // 交易表单卡片 - 设计稿 Trade Form Card // card背景 + 圆角lg + border + padding:20 + gap:16 // 竖向布局: // Buy/Sell Toggle(圆角md,clip,横向两等宽按钮) // 金额label行("交易金额" + "USDT") // 输入框(bg-tertiary,圆角md,高48) // 可用余额文字 // 快捷比例按钮行(25% 50% 75% 100%,gap:8) // 计算数量行 // ============================================ 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); // 设计稿中 card 背景色 final cardBgColor = isDark ? colorScheme.surfaceContainer : colorScheme.surfaceContainerLowest; return Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: cardBgColor, borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all( color: colorScheme.outlineVariant.withOpacity(0.15), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ---- 买入/卖出切换 ---- // 设计稿:ClipRRect + 圆角md,两等宽按钮 ClipRRect( borderRadius: BorderRadius.circular(AppRadius.md), child: Row( children: [ // 买入按钮 Expanded( child: GestureDetector( onTap: () => onTradeTypeChanged(0), child: AnimatedContainer( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, height: 40, decoration: BoxDecoration( color: isBuy ? AppColorScheme.buyButtonFill : cardBgColor, border: isBuy ? null : Border.all( color: colorScheme.outlineVariant.withOpacity(0.15)), ), child: Center( child: Text( '买入', style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.w600, color: isBuy ? Colors.white : colorScheme.onSurfaceVariant, ), ), ), ), ), ), // 卖出按钮 Expanded( child: GestureDetector( onTap: () => onTradeTypeChanged(1), child: AnimatedContainer( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, height: 40, decoration: BoxDecoration( color: !isBuy ? AppColorScheme.sellButtonFill : cardBgColor, border: !isBuy ? null : Border.all( color: colorScheme.outlineVariant.withOpacity(0.15)), ), child: Center( child: Text( '卖出', style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.w600, color: !isBuy ? Colors.white : colorScheme.onSurfaceVariant, ), ), ), ), ), ), ], ), ), const SizedBox(height: AppSpacing.md + AppSpacing.sm), // ---- 交易金额 label 行 ---- Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('交易金额', style: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.normal, color: colorScheme.onSurfaceVariant, )), Text('USDT', style: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.w600, color: colorScheme.onSurface, )), ], ), const SizedBox(height: AppSpacing.sm), // ---- 金额输入框 ---- _AmountInput( amountController: amountController, maxAmount: maxAmount, isBuy: isBuy, actionColor: actionColor, onChanged: onAmountChanged, ), const SizedBox(height: AppSpacing.sm), // ---- 可用余额 ---- Text( isBuy ? '可用: $availableUsdt USDT' : '可用: $availableCoinQty ${selectedCoin?.code ?? ""}', style: GoogleFonts.inter( fontSize: 11, fontWeight: FontWeight.normal, color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: AppSpacing.md), // ---- 快捷比例按钮 25% 50% 75% 100% ---- // 设计稿:gap:8,圆角sm,bg-tertiary,高32 Row( children: [ _buildPctButton('25%', 0.25, colorScheme), const SizedBox(width: AppSpacing.sm), _buildPctButton('50%', 0.5, colorScheme), const SizedBox(width: AppSpacing.sm), _buildPctButton('75%', 0.75, colorScheme), const SizedBox(width: AppSpacing.sm), _buildPctButton('100%', 1.0, colorScheme), ], ), const SizedBox(height: AppSpacing.md + AppSpacing.sm), // ---- 计算数量行 ---- Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('交易数量', style: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.normal, color: colorScheme.onSurfaceVariant, )), Text( '$calculatedQuantity ${selectedCoin?.code ?? ''}', style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.w600, color: colorScheme.onSurface, fontFeatures: [FontFeature.tabularFigures()], ), ), ], ), ], ), ); } /// 百分比按钮 - 设计稿:圆角sm,bg-tertiary,高32 Widget _buildPctButton(String label, double pct, ColorScheme colorScheme) { return Expanded( child: GestureDetector( onTap: () => onFillPercent(pct), child: Container( height: 32, decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest.withOpacity(0.5), borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Center( child: Text(label, style: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.w500, color: colorScheme.onSurfaceVariant, )), ), ), ), ); } } // ============================================ // CTA 交易按钮 - 设计稿 Buy Button // profit-green底 / sell-red底,圆角lg,高48,白字16px bold // ============================================ 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: 48, 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, ), ) : Text( '${isBuy ? '买入' : '卖出'} ${coinCode ?? ""}', style: GoogleFonts.inter( fontSize: 16, fontWeight: FontWeight.w700, color: enabled ? Colors.white : colorScheme.onSurface.withOpacity(0.3), ), ), ), ), ); } } // ============================================ // 金额输入框(含超额提示) // 设计稿:bg-tertiary,圆角md,高48 // ============================================ 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( height: 48, decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest.withOpacity(0.3), borderRadius: BorderRadius.circular(AppRadius.md), ), child: TextField( controller: widget.amountController, keyboardType: const TextInputType.numberWithOptions(decimal: true), onChanged: (_) => _checkLimit(), style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.normal, color: colorScheme.onSurface, fontFeatures: [FontFeature.tabularFigures()], ), decoration: InputDecoration( hintText: '请输入金额', hintStyle: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.normal, color: colorScheme.onSurfaceVariant.withOpacity(0.5), ), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, ), ), ), ), 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: GoogleFonts.inter( fontSize: 11, color: warningColor, ), ), ], ), ), ], ); } }