This commit is contained in:
sion
2026-04-23 00:44:39 +08:00
parent 685202dd55
commit 8047cfaa76
209 changed files with 2660 additions and 5560 deletions

View File

@@ -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) {

View File

@@ -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)),
),
),
),

View File

@@ -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,