import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/l10n/app_localizations.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/utils/number_format.dart'; import '../../../core/utils/top_toast.dart'; import '../../../providers/futures_provider.dart'; /// 委托详情页 class OrderDetailScreen extends ConsumerStatefulWidget { const OrderDetailScreen({super.key, required this.order}); final FuturesOrder order; @override ConsumerState createState() => _OrderDetailScreenState(); } class _OrderDetailScreenState extends ConsumerState { bool _cancelling = false; FuturesOrder get order => widget.order; String _baseCoin(String sym) { if (sym.contains('/')) return sym.split('/').first; return sym.toUpperCase().replaceFirst(RegExp(r'USDT$'), ''); } String _formatDateTime(DateTime? dt) { if (dt == null) return '--'; final y = dt.year; final mo = dt.month.toString().padLeft(2, '0'); final d = dt.day.toString().padLeft(2, '0'); final h = dt.hour.toString().padLeft(2, '0'); final mi = dt.minute.toString().padLeft(2, '0'); final s = dt.second.toString().padLeft(2, '0'); return '$y-$mo-$d $h:$mi:$s'; } Future _cancelOrder(BuildContext context) async { setState(() => _cancelling = true); final activeSymbol = ref.read(futuresActiveSymbolProvider); final notifier = ref.read(futuresProvider(activeSymbol).notifier); final err = await notifier.cancelOrder(order); if (!context.mounted) return; setState(() => _cancelling = false); final l10n = AppLocalizations.of(context)!; if (err == null) { showTopToast(context, message: l10n.cancelOrderSuccess, backgroundColor: AppColors.rise); context.pop(); } else { showTopToast(context, message: resolveProviderError(err, l10n) ?? err, backgroundColor: AppColors.fall); } } /// 去除多余的尾零,整数不带小数点;为空/未传才显示 '--',由调用方处理 String _rawNum(double v) { if (v == v.truncateToDouble()) return v.toInt().toString(); return v.toString(); } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; final isOpen = order.isOpenOrder; final isLong = order.side == OrderSide.long; final actionColor = isOpen ? (isLong ? AppColors.rise : AppColors.fall) : (isLong ? AppColors.fall : AppColors.rise); final coinSymbol = _baseCoin(order.symbol); final hasTrigger = order.triggerPrice > 0; final hasProfit = order.profitPrice != null && order.profitPrice! > 0; final hasLoss = order.lossPrice != null && order.lossPrice! > 0; final hasTpsl = hasProfit || hasLoss; final canCancel = order.isPending; final isDark = Theme.of(context).brightness == Brightness.dark; final scaffoldBg = isDark ? AppColors.darkBg : AppColors.lightBgSecondary; final cardBg = isDark ? AppColors.darkBgSecondary : AppColors.lightBg; return Scaffold( backgroundColor: scaffoldBg, appBar: AppBar( elevation: 0, centerTitle: true, backgroundColor: cardBg, title: Text( l10n.orderDetail, style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w700), ), ), body: Stack( children: [ SingleChildScrollView( padding: EdgeInsets.only(bottom: canCancel ? 90 : 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── 标的行 ────────────────────────────────────── Container( color: cardBg, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Row( children: [ // 开多/开空/平多/平空 chip Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), decoration: BoxDecoration( color: actionColor, borderRadius: BorderRadius.circular(4), ), child: Text( isOpen ? (isLong ? l10n.openLong : l10n.openShort) : (isLong ? l10n.closeLong : l10n.closeShort), style: const TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.w700), ), ), const SizedBox(width: 6), Expanded( child: Row( children: [ Flexible( child: Text( order.symbol, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w700), ), ), const SizedBox(width: 6), _SmTag(text: order.type == OrderType.market ? l10n.marketHint : order.type == OrderType.limit ? l10n.limitLabel : l10n.planOrderLabel), const SizedBox(width: 4), _SmTag( text: switch (order.marginMode) { '分仓' || '逐仓' => l10n.splitMargin, _ => l10n.crossMargin, }, color: switch (order.marginMode) { '分仓' || '逐仓' => AppColors.rankPurple, _ => AppColors.tagBlue, }, ), const SizedBox(width: 4), _LevTag(leverage: order.leverage.toInt()), ], ), ), ], ), ), const SizedBox(height: 8), // ── 委托基本信息 ───────────────────────────────── _InfoCard(rows: [ _InfoRowData('${l10n.orderPriceLabel}(USDT)', order.priceDisplay), _InfoRowData('${l10n.entrustAmount}($coinSymbol)', _rawNum(order.size)), _InfoRowData('${l10n.filledVolume}($coinSymbol)', _rawNum(order.filledSize)), _InfoRowData( l10n.avgTradePrice, order.tradedPrice > 0 ? _rawNum(order.tradedPrice) : '--', ), if (hasTrigger) _InfoRowData( l10n.triggerCondition, '${l10n.markLabel}≥${_rawNum(order.triggerPrice)}', valueColor: cs.onSurface.withAlpha(100), isLast: true, ), ]), const SizedBox(height: 8), // ── 止盈止损 ───────────────────────────────────── if (hasTpsl) ...[ _InfoCard( sectionTitle: l10n.stopProfitLoss, rows: [ if (hasProfit) _InfoRowData( '${l10n.takeProfitPrice}(USDT)', _rawNum(order.profitPrice!), valueColor: AppColors.rise, isLast: !hasLoss, ), if (hasLoss) _InfoRowData( '${l10n.stopLossPrice}(USDT)', _rawNum(order.lossPrice!), valueColor: AppColors.fall, isLast: true, ), ], ), const SizedBox(height: 8), ], // ── 时间 + 订单 ID ─────────────────────────────── _InfoCard(rows: [ _InfoRowData(l10n.createTime, _formatDateTime(order.createTime)), _InfoRowData( 'ID', order.id, isLast: true, copyable: true, ), ]), const SizedBox(height: 8), // ── 交易明细(已成交时显示)────────────────────────── if (order.filledSize > 0) ...[ _InfoCard( sectionTitle: l10n.tradeDetailLabel, rows: [ _InfoRowData(l10n.tradePrice, order.tradedPrice > 0 ? _rawNum(order.tradedPrice) : '--'), _InfoRowData('${l10n.filledVolume}($coinSymbol)', _rawNum(order.filledSize)), if (order.fee > 0) _InfoRowData('${l10n.fee}(USDT)', formatAmount(order.fee)), _InfoRowData(l10n.tradeTime, _formatDateTime(order.dealTime ?? order.createTime), isLast: true), ], ), const SizedBox(height: 8), ], ], ), ), // ── 撤单按钮(仅委托中状态)───────────────────────────── if (canCancel) Positioned( left: 0, right: 0, bottom: 0, child: Container( color: Theme.of(context).brightness == Brightness.dark ? AppColors.darkBgSecondary : AppColors.lightBg, padding: const EdgeInsets.fromLTRB(16, 12, 16, 28), child: SizedBox( height: 50, child: ElevatedButton( onPressed: _cancelling ? null : () => _cancelOrder(context), style: ElevatedButton.styleFrom( backgroundColor: AppColors.brand, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(25)), ), child: _cancelling ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white), ) : Text( l10n.cancelOrder, style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w700, letterSpacing: 2), ), ), ), ), ), ], ), ); } } // ── 共用 widgets ─────────────────────────────────────────────── class _SmTag extends StatelessWidget { const _SmTag({required this.text, this.color}); final String text; final Color? color; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final c = color; if (c != null) { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: c.withAlpha(isDark ? 45 : 25), borderRadius: BorderRadius.circular(4), ), child: Text(text, style: TextStyle( color: c, fontSize: 11, fontWeight: FontWeight.w600)), ); } return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: cs.onSurface.withAlpha(12), border: Border.all(color: cs.outline.withAlpha(80)), borderRadius: BorderRadius.circular(4), ), child: Text(text, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 11, fontWeight: FontWeight.w600)), ); } } class _LevTag extends StatelessWidget { const _LevTag({required this.leverage}); final int leverage; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: AppColors.leverageGoldBg, borderRadius: BorderRadius.circular(4), ), child: Text('${leverage}X', style: const TextStyle( color: AppColors.leverageGold, fontSize: 11, fontWeight: FontWeight.w700)), ); } } class _InfoRowData { const _InfoRowData(this.label, this.value, {this.valueColor, this.isLast = false, this.copyable = false}); final String label; final String value; final Color? valueColor; final bool isLast; final bool copyable; } class _InfoCard extends StatelessWidget { const _InfoCard({required this.rows, this.sectionTitle}); final List<_InfoRowData> rows; final String? sectionTitle; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; return Container( color: isDark ? AppColors.darkBgSecondary : AppColors.lightBg, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (sectionTitle != null) Padding( padding: const EdgeInsets.fromLTRB(16, 14, 16, 0), child: Text(sectionTitle!, style: TextStyle( color: cs.onSurface, fontSize: 14, fontWeight: FontWeight.w700)), ), ...rows.map((r) { return Column( children: [ Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 13), child: Row( children: [ Expanded( child: Text(r.label, overflow: TextOverflow.ellipsis, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 13)), ), const SizedBox(width: 8), Row( mainAxisSize: MainAxisSize.min, children: [ Text(r.value, overflow: TextOverflow.ellipsis, textAlign: TextAlign.end, style: TextStyle( color: r.valueColor ?? cs.onSurface, fontSize: 13, fontWeight: FontWeight.w500)), if (r.copyable) ...[ const SizedBox(width: 6), GestureDetector( onTap: () { Clipboard.setData( ClipboardData(text: r.value)); showTopToast(context, message: AppLocalizations.of(context)!.copied, backgroundColor: AppColors.rise); }, child: Icon(Icons.copy_outlined, size: 15, color: cs.onSurface.withAlpha(100)), ), ], ], ), ], ), ), if (!r.isLast) Divider( height: 1, thickness: 0.5, indent: 16, endIndent: 16, color: cs.outline.withAlpha(60)), ], ); }).toList(), ], ), ); } }