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'; // 向下截斷到4位小數,避免回算超出金額 final qty = amount / price; return ((qty * 10000).truncateToDouble() / 10000).toStringAsFixed(4); } @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, AppSpacing.md, 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; final value = max * pct; // 向下截斷到2位小數,避免四捨五入超出可用餘額 // 向下截斷到2位小數,再減0.01作為安全緩衝,避免精度問題導致餘額不足 final truncated = ((value * 100).truncateToDouble() / 100); final safe = truncated > 0.01 ? truncated - 0.01 : truncated; _amountController.text = safe.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(), ), ], ), ); } }