Files
monisuo/flutter_monisuo/lib/ui/pages/trade/trade_page.dart

290 lines
9.2 KiB
Dart
Raw Normal View History

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';
2026-03-28 23:57:51 +08:00
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';
2026-03-28 23:57:51 +08:00
/// 交易页面
///
/// 设计稿 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<TradePage> createState() => _TradePageState();
}
2026-03-28 23:57:51 +08:00
class _TradePageState extends State<TradePage>
with AutomaticKeepAliveClientMixin {
int _tradeType = 0; // 0=买入, 1=卖出
Coin? _selectedCoin;
2026-03-28 23:57:51 +08:00
final _amountController = TextEditingController();
bool _isSubmitting = false;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
2026-03-23 00:43:19 +08:00
WidgetsBinding.instance.addPostFrameCallback((_) => _loadData());
}
void _loadData() {
final marketProvider = context.read<MarketProvider>();
marketProvider.loadCoins().then((_) {
if (widget.initialCoinCode != null && _selectedCoin == null) {
final coins = marketProvider.allCoins;
final coin = coins.firstWhere(
2026-03-28 23:57:51 +08:00
(c) =>
c.code.toUpperCase() == widget.initialCoinCode!.toUpperCase(),
orElse: () =>
coins.isNotEmpty ? coins.first : throw Exception('No coins'),
);
2026-03-28 23:57:51 +08:00
if (mounted) setState(() => _selectedCoin = coin);
}
});
2026-03-28 23:57:51 +08:00
context.read<AssetProvider>().refreshAll(force: true);
}
@override
void dispose() {
2026-03-28 23:57:51 +08:00
_amountController.dispose();
super.dispose();
}
2026-03-28 23:57:51 +08:00
/// 获取交易账户中 USDT 可用余额
String get _availableUsdt {
final holdings = context.read<AssetProvider>().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<AssetProvider>().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<MarketProvider, AssetProvider>(
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: '请先选择交易币种',
2026-03-28 23:57:51 +08:00
),
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,
2026-03-28 23:57:51 +08:00
),
),
],
),
),
);
},
),
);
}
2026-03-28 23:57:51 +08:00
bool _canTrade() {
if (_selectedCoin == null) return false;
final amount = double.tryParse(_amountController.text) ?? 0;
2026-03-30 00:30:42 +08:00
if (amount <= 0) return false;
// 买入时校验不超过可用USDT
if (_tradeType == 0) {
final available = double.tryParse(_availableUsdt) ?? 0;
if (amount > available) return false;
}
return true;
2026-03-28 23:57:51 +08:00
}
void _fillPercent(double pct) {
final max = double.tryParse(_maxAmount) ?? 0;
_amountController.text = (max * pct).toStringAsFixed(2);
setState(() {});
}
void _executeTrade() async {
2026-03-23 00:43:19 +08:00
final isBuy = _tradeType == 0;
2026-03-28 23:57:51 +08:00
final amount = _amountController.text;
final quantity = _calculatedQuantity;
final price = _selectedCoin!.price.toStringAsFixed(2);
final coinCode = _selectedCoin!.code;
2026-03-23 00:43:19 +08:00
2026-03-28 23:57:51 +08:00
final confirmed = await showDialog<bool>(
2026-03-23 00:43:19 +08:00
context: context,
builder: (ctx) => ConfirmDialog(
2026-03-28 23:57:51 +08:00
isBuy: isBuy,
coinCode: coinCode,
price: price,
quantity: quantity,
amount: amount,
2026-03-23 00:43:19 +08:00
),
);
2026-03-28 23:57:51 +08:00
if (confirmed != true) return;
setState(() => _isSubmitting = true);
try {
final tradeService = context.read<TradeService>();
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<AssetProvider>().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);
}
2026-03-23 00:43:19 +08:00
}
2026-03-28 23:57:51 +08:00
void _showResultDialog(bool success, String title, String message) {
2026-03-23 00:43:19 +08:00
showShadDialog(
context: context,
builder: (ctx) => ShadDialog.alert(
title: Row(
children: [
NeonIcon(
2026-03-28 23:57:51 +08:00
icon: success ? Icons.check_circle : Icons.error,
color: success
? ctx.appColors.up
: ctx.colors.error,
size: 24,
),
SizedBox(width: AppSpacing.sm),
2026-03-28 23:57:51 +08:00
Text(title),
2026-03-23 00:43:19 +08:00
],
),
2026-03-28 23:57:51 +08:00
description: Text(message),
2026-03-23 00:43:19 +08:00
actions: [
ShadButton(
child: const Text('确定'),
2026-03-28 23:57:51 +08:00
onPressed: () => Navigator.of(ctx).pop(),
2026-03-23 00:43:19 +08:00
),
],
),
);
}
}