111
This commit is contained in:
@@ -117,8 +117,7 @@ class PriceCard extends StatelessWidget {
|
||||
String _fmt(double? v) {
|
||||
if (v == null) return '--';
|
||||
if (v >= 1000) return v.toStringAsFixed(2);
|
||||
if (v >= 1) return v.toStringAsFixed(4);
|
||||
return v.toStringAsFixed(6);
|
||||
return v.toStringAsFixed(4);
|
||||
}
|
||||
|
||||
String _fmtVol(double? v) {
|
||||
|
||||
@@ -55,6 +55,15 @@ class SplitTradeForm extends StatefulWidget {
|
||||
class _SplitTradeFormState extends State<SplitTradeForm>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
double _currentPercent = 0.0;
|
||||
|
||||
String _fmtAvailable(String raw) {
|
||||
final v = double.tryParse(raw);
|
||||
if (v == null || v == 0) return '0';
|
||||
if (v >= 1) return v.toStringAsFixed(2);
|
||||
if (v >= 0.01) return v.toStringAsFixed(4);
|
||||
return v.toStringAsFixed(6);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -256,11 +265,21 @@ class _SplitTradeFormState extends State<SplitTradeForm>
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
|
||||
// 可用余额
|
||||
Text(
|
||||
'可用: $available ${_isBuy ? 'USDT' : (widget.coin?.code ?? '')}',
|
||||
style: AppTextStyles.bodySmall(context)
|
||||
.copyWith(color: context.colors.onSurfaceVariant),
|
||||
// 可用余额 + 手续费
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'可用: ${_fmtAvailable(available)} ${_isBuy ? 'USDT' : (widget.coin?.code ?? '')}',
|
||||
style: AppTextStyles.bodySmall(context)
|
||||
.copyWith(color: context.colors.onSurfaceVariant),
|
||||
),
|
||||
Text(
|
||||
'手续费: 0%',
|
||||
style: AppTextStyles.bodySmall(context)
|
||||
.copyWith(color: context.colors.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
|
||||
@@ -276,7 +295,29 @@ class _SplitTradeFormState extends State<SplitTradeForm>
|
||||
_pctBtn(context, '100%', 1.0, onFillPercent),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
const SizedBox(height: 2),
|
||||
|
||||
// 滑块选择仓位
|
||||
SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackHeight: 3,
|
||||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6),
|
||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 12),
|
||||
activeTrackColor: accentColor,
|
||||
inactiveTrackColor: context.colors.surfaceContainerHighest,
|
||||
thumbColor: accentColor,
|
||||
),
|
||||
child: Slider(
|
||||
value: _currentPercent.clamp(0.0, 1.0),
|
||||
onChanged: (v) {
|
||||
setState(() => _currentPercent = v);
|
||||
onFillPercent(v);
|
||||
},
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
|
||||
// 提交按钮
|
||||
SizedBox(
|
||||
@@ -296,7 +337,7 @@ class _SplitTradeFormState extends State<SplitTradeForm>
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
'${_isBuy ? '买入' : '卖出'} ${widget.coin?.code ?? ''}',
|
||||
'${_isBuy ? '買入' : '賣出'} ${widget.coin?.code ?? ''}',
|
||||
style: AppTextStyles.labelLarge(context)
|
||||
.copyWith(color: Colors.white, fontWeight: FontWeight.w700),
|
||||
),
|
||||
@@ -308,20 +349,26 @@ class _SplitTradeFormState extends State<SplitTradeForm>
|
||||
}
|
||||
|
||||
Widget _pctBtn(BuildContext context, String label, double pct, ValueChanged<double> onTap) {
|
||||
final isActive = (_currentPercent - pct).abs() < 0.01;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => onTap(pct),
|
||||
onTap: () {
|
||||
setState(() => _currentPercent = pct);
|
||||
onTap(pct);
|
||||
},
|
||||
child: Container(
|
||||
height: 28,
|
||||
height: 26,
|
||||
decoration: BoxDecoration(
|
||||
color: context.colors.surfaceContainerHighest.withValues(alpha: 0.4),
|
||||
color: isActive
|
||||
? context.colors.primary.withValues(alpha: 0.15)
|
||||
: context.colors.surfaceContainerHighest.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(label,
|
||||
style: AppTextStyles.bodySmall(context).copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500)),
|
||||
color: isActive ? context.colors.primary : context.colors.onSurfaceVariant,
|
||||
fontWeight: isActive ? FontWeight.w600 : FontWeight.w500)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -37,6 +37,7 @@ class _TradePageState extends State<TradePage>
|
||||
bool _isSellSubmitting = false;
|
||||
int _orderType = 1;
|
||||
double _realtimePrice = 0;
|
||||
double _currentBuyPercent = 0.0;
|
||||
|
||||
final _buyPriceController = TextEditingController();
|
||||
final _buyQuantityController = TextEditingController();
|
||||
@@ -695,10 +696,9 @@ class _TradePageState extends State<TradePage>
|
||||
if (_selectedCoin == null) return false;
|
||||
final qty = double.tryParse(_buyQuantityController.text) ?? 0;
|
||||
if (qty <= 0) return false;
|
||||
final price = double.tryParse(_buyPriceController.text) ?? 0;
|
||||
final amount = price * qty;
|
||||
// 只要有余额就可以买,后端会微调价格确保成交
|
||||
final available = double.tryParse(_availableUsdt) ?? 0;
|
||||
return amount <= available;
|
||||
return available > 0;
|
||||
}
|
||||
|
||||
bool _canSell() {
|
||||
@@ -710,13 +710,12 @@ class _TradePageState extends State<TradePage>
|
||||
}
|
||||
|
||||
void _buyFillPercent(double pct) {
|
||||
_currentBuyPercent = pct;
|
||||
final price = _orderType == 1 ? _realtimePrice : (double.tryParse(_buyPriceController.text) ?? 0);
|
||||
final available = double.tryParse(_availableUsdt) ?? 0;
|
||||
if (price <= 0) return;
|
||||
// 100%时留极小余量防精度误差
|
||||
final safePct = pct >= 1.0 ? 0.9999 : pct;
|
||||
final qty = (available / price) * safePct;
|
||||
_buyQuantityController.text = qty < 0.0001 ? '' : ((qty * 10000).truncateToDouble() / 10000).toStringAsFixed(4);
|
||||
final qty = (available / price * pct * 10000).truncateToDouble() / 10000;
|
||||
_buyQuantityController.text = qty < 0.0001 ? '' : qty.toStringAsFixed(4);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@@ -732,11 +731,24 @@ class _TradePageState extends State<TradePage>
|
||||
final priceController = isBuy ? _buyPriceController : _sellPriceController;
|
||||
final qtyController = isBuy ? _buyQuantityController : _sellQuantityController;
|
||||
|
||||
final price = priceController.text;
|
||||
String price = priceController.text;
|
||||
final quantity = qtyController.text;
|
||||
final coinCode = _selectedCoin!.code;
|
||||
final typeLabel = _orderType == 1 ? '市价单' : '限价单';
|
||||
|
||||
// 买入时:按所选仓位百分比重算价格,确保 价格×数量=仓位金额
|
||||
if (isBuy && _currentBuyPercent > 0) {
|
||||
final available = double.tryParse(_availableUsdt) ?? 0;
|
||||
final qty = double.tryParse(quantity) ?? 0;
|
||||
if (qty > 0 && available > 0) {
|
||||
final positionAmount = available * _currentBuyPercent;
|
||||
final adjustedPrice = positionAmount / qty;
|
||||
price = adjustedPrice.toStringAsFixed(8);
|
||||
}
|
||||
}
|
||||
|
||||
final displayAmount = ((double.tryParse(price) ?? 0) * (double.tryParse(quantity) ?? 0)).toStringAsFixed(2);
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => ConfirmDialog(
|
||||
@@ -744,7 +756,7 @@ class _TradePageState extends State<TradePage>
|
||||
coinCode: coinCode,
|
||||
price: price,
|
||||
quantity: quantity,
|
||||
amount: ((double.tryParse(price) ?? 0) * (double.tryParse(quantity) ?? 0)).toStringAsFixed(2),
|
||||
amount: displayAmount,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -757,20 +769,39 @@ class _TradePageState extends State<TradePage>
|
||||
|
||||
try {
|
||||
final tradeService = context.read<TradeService>();
|
||||
final response = isBuy
|
||||
var response = isBuy
|
||||
? await tradeService.buy(coinCode: coinCode, price: price, quantity: quantity, orderType: _orderType)
|
||||
: await tradeService.sell(coinCode: coinCode, price: price, quantity: quantity, orderType: _orderType);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// 买入次数用完 → 弹出交易码输入框重试
|
||||
if (isBuy && !response.success &&
|
||||
(response.message?.contains('次數') == true || response.message?.contains('交易碼') == true)) {
|
||||
final code = await _showTradeCodeDialog(response.message ?? '請輸入交易碼');
|
||||
if (code != null && code.isNotEmpty && mounted) {
|
||||
response = await tradeService.buy(
|
||||
coinCode: coinCode, price: price, quantity: quantity,
|
||||
orderType: _orderType, tradeCode: code,
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (response.success) {
|
||||
qtyController.clear();
|
||||
context.read<AssetProvider>().refreshAll(force: true);
|
||||
context.read<AppEventBus>().fire(AppEventType.assetChanged);
|
||||
_loadPendingOrders();
|
||||
final msg = _orderType == 2
|
||||
? '$typeLabel委托成功: $quantity $coinCode @ $price USDT'
|
||||
: '$typeLabel: $quantity $coinCode @ $price USDT';
|
||||
final resultAmount = response.data?['amount'];
|
||||
final resultQty = response.data?['quantity'];
|
||||
final resultPrice = response.data?['price'];
|
||||
final msg = isBuy
|
||||
? '買入 ${resultQty ?? quantity} $coinCode\n成交金額: ${resultAmount != null ? (double.tryParse(resultAmount.toString())?.toStringAsFixed(2) ?? displayAmount) : displayAmount} USDT'
|
||||
: '賣出 ${resultQty ?? quantity} $coinCode\n成交金額: ${resultAmount != null ? (double.tryParse(resultAmount.toString())?.toStringAsFixed(2) ?? displayAmount) : displayAmount} USDT';
|
||||
_showResultDialog(true, isBuy ? '買入成功' : '賣出成功', msg);
|
||||
} else {
|
||||
_showResultDialog(false, '交易失敗', response.message ?? '請稍後重試');
|
||||
@@ -787,6 +818,51 @@ class _TradePageState extends State<TradePage>
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _showTradeCodeDialog(String hint) {
|
||||
final controller = TextEditingController();
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('需要交易碼'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(hint, style: AppTextStyles.bodySmall(ctx)),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
textCapitalization: TextCapitalization.characters,
|
||||
style: AppTextStyles.headlineMedium(ctx),
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: InputDecoration(
|
||||
hintText: '請輸入交易碼',
|
||||
filled: true,
|
||||
fillColor: ctx.colors.surfaceContainerHighest.withValues(alpha: 0.3),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(null),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(controller.text.trim()),
|
||||
child: const Text('確認'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showResultDialog(bool success, String title, String message) {
|
||||
showDialog(
|
||||
context: context,
|
||||
|
||||
Reference in New Issue
Block a user