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) { // 买入:最大 = USDT 余额 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') .toList(), onCoinSelected: (coin) { setState(() { _selectedCoin = coin; _amountController.clear(); }); }, ), SizedBox(height: AppSpacing.md), // 价格卡片 if (_selectedCoin != null) _PriceCard(coin: _selectedCoin!) else _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; return amount > 0; } 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; showShadDialog( context: context, builder: (ctx) => ShadDialog.alert( title: Row( children: [ NeonIcon( icon: success ? Icons.check_circle : Icons.error, color: success ? AppColorScheme.up : 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 actionColor = isBuy ? AppColorScheme.up : AppColorScheme.down; return Dialog( backgroundColor: Colors.transparent, child: GlassPanel( borderRadius: BorderRadius.circular(AppRadius.xxl), 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 isSelected = selectedCoin?.code == coin.code; final changeColor = coin.isUp ? AppColorScheme.up : AppColorScheme.down; 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: 16, fontWeight: FontWeight.bold, color: colorScheme.onSurface, )), SizedBox(width: AppSpacing.xs), Text('/USDT', style: TextStyle( fontSize: 12, color: colorScheme.onSurfaceVariant, )), ], ), Text(coin.name, style: TextStyle( fontSize: 12, color: colorScheme.onSurfaceVariant, )), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text('\$${coin.formattedPrice}', style: GoogleFonts.spaceGrotesk( fontSize: 14, fontWeight: FontWeight.w600, color: colorScheme.onSurface, )), Text(coin.formattedChange, style: TextStyle( fontSize: 12, color: changeColor, fontWeight: FontWeight.w600, )), ], ), if (isSelected) ...[ SizedBox(width: AppSpacing.sm), Icon(LucideIcons.check, size: 18, color: colorScheme.primary), ], ], ), ), ); } } /// 币种头像 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 color = coin.isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.down; final bgColor = coin.isUp ? AppColorScheme.getUpBackgroundColor(isDark) : colorScheme.error.withOpacity(0.1); return GlassPanel( padding: EdgeInsets.all(AppSpacing.lg), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('最新价', style: TextStyle( fontSize: 12, color: colorScheme.onSurfaceVariant, )), SizedBox(height: AppSpacing.xs), Text( '\$${coin.formattedPrice}', style: GoogleFonts.spaceGrotesk( fontSize: 28, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), ], ), Container( padding: EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.sm), decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: color.withOpacity(0.2)), ), child: Text( coin.formattedChange, style: TextStyle( fontSize: 16, color: color, 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 isBuy = tradeType == 0; final actionColor = isBuy ? AppColorScheme.up : AppColorScheme.down; return GlassPanel( padding: EdgeInsets.all(AppSpacing.lg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 买入/卖出切换 Container( padding: EdgeInsets.all(AppSpacing.xs), decoration: BoxDecoration( color: colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(AppRadius.xl), ), child: Row( children: [ Expanded( child: _typeButton('买入', isBuy, AppColorScheme.up, () => onTradeTypeChanged(0)), ), SizedBox(width: AppSpacing.sm), Expanded( child: _typeButton('卖出', !isBuy, AppColorScheme.down, () => onTradeTypeChanged(1)), ), ], ), ), SizedBox(height: AppSpacing.lg), // 交易金额输入 Text('交易金额 (USDT)', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w700, letterSpacing: 0.2, color: colorScheme.onSurfaceVariant, )), SizedBox(height: AppSpacing.xs), Container( decoration: BoxDecoration( color: colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(AppRadius.xl), border: Border.all(color: colorScheme.outlineVariant.withOpacity(0.3)), ), child: TextField( controller: amountController, keyboardType: const TextInputType.numberWithOptions(decimal: true), onChanged: (_) => onAmountChanged(), style: GoogleFonts.spaceGrotesk( fontSize: 20, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), decoration: InputDecoration( hintText: '输入金额', hintStyle: TextStyle( color: colorScheme.outlineVariant.withOpacity(0.5)), border: InputBorder.none, contentPadding: EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.md, ), suffixIcon: Padding( padding: EdgeInsets.only(right: AppSpacing.sm), child: Text('USDT', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: colorScheme.onSurfaceVariant, )), ), suffixIconConstraints: const BoxConstraints(minWidth: 50), ), ), ), SizedBox(height: AppSpacing.sm), // 快捷比例按钮 Row( children: [ _pctButton('25%', 0.25, colorScheme), SizedBox(width: AppSpacing.xs), _pctButton('50%', 0.5, colorScheme), SizedBox(width: AppSpacing.xs), _pctButton('75%', 0.75, colorScheme), SizedBox(width: AppSpacing.xs), _pctButton('全部', 1.0, colorScheme), ], ), SizedBox(height: AppSpacing.lg), // 预计数量 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('预计数量', style: TextStyle( fontSize: 13, color: colorScheme.onSurfaceVariant, )), Text( '$calculatedQuantity ${selectedCoin?.code ?? ''}', style: GoogleFonts.spaceGrotesk( fontSize: 14, fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), ], ), SizedBox(height: AppSpacing.md), // 可用余额 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(isBuy ? '可用 USDT' : '可用 ${selectedCoin?.code ?? ""}', style: TextStyle( fontSize: 13, color: colorScheme.onSurfaceVariant, )), Text( isBuy ? '$availableUsdt USDT' : '$availableCoinQty ${selectedCoin?.code ?? ""}', style: GoogleFonts.spaceGrotesk( fontSize: 14, fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), ], ), ], ), ); } Widget _typeButton( String label, bool isActive, Color color, VoidCallback onTap) { return GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: EdgeInsets.symmetric(vertical: AppSpacing.sm + AppSpacing.xs), decoration: BoxDecoration( color: isActive ? color.withOpacity(0.15) : Colors.transparent, borderRadius: BorderRadius.circular(AppRadius.md), border: isActive ? null : Border.all(color: color.withOpacity(0.3)), ), child: Center( child: Text( label, style: TextStyle( color: isActive ? color : color.withOpacity(0.7), fontWeight: FontWeight.w700, fontSize: 14, letterSpacing: 0.5, ), ), ), ), ); } Widget _pctButton(String label, double pct, ColorScheme colorScheme) { return Expanded( child: GestureDetector( onTap: () => onFillPercent(pct), child: Container( padding: EdgeInsets.symmetric(vertical: AppSpacing.xs + 2), decoration: BoxDecoration( color: colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Center( child: Text(label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: colorScheme.onSurfaceVariant, )), ), ), ), ); } } /// 交易按钮 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; return SizedBox( width: double.infinity, height: 52, child: ElevatedButton( onPressed: enabled ? onPressed : null, style: ElevatedButton.styleFrom( backgroundColor: isBuy ? AppColorScheme.up : AppColorScheme.down, disabledBackgroundColor: colorScheme.onSurface.withOpacity(0.12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.xl), ), elevation: isBuy && enabled ? 4 : 0, shadowColor: isBuy ? AppColorScheme.up.withOpacity(0.3) : Colors.transparent, ), child: isLoading ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: isBuy ? Colors.black87 : Colors.white, ), ) : Text( '${isBuy ? '买入' : '卖出'} ${coinCode ?? ""}', style: GoogleFonts.spaceGrotesk( fontSize: 16, fontWeight: FontWeight.w700, color: isBuy ? Colors.black87 : Colors.white, letterSpacing: 1, ), ), ), ); } }