import 'package:flutter/material.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:provider/provider.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_theme_extension.dart'; import '../../../data/models/coin.dart'; import '../../../providers/market_provider.dart'; import '../../../providers/asset_provider.dart'; import '../../../data/services/trade_service.dart'; import '../../components/neon_glow.dart'; import 'components/coin_selector.dart'; import 'components/price_card.dart'; import 'components/placeholder_card.dart'; import 'components/trade_form_card.dart'; import 'components/trade_button.dart'; import 'components/confirm_dialog.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); return Scaffold( backgroundColor: context.colors.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: '请先选择交易币种', ), 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) { showShadDialog( context: context, builder: (ctx) => ShadDialog.alert( title: Row( children: [ NeonIcon( icon: success ? Icons.check_circle : Icons.error, color: success ? ctx.appColors.up : ctx.colors.error, size: 24, ), SizedBox(width: AppSpacing.sm), Text(title), ], ), description: Text(message), actions: [ ShadButton( child: const Text('确定'), onPressed: () => Navigator.of(ctx).pop(), ), ], ), ); } }