import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/l10n/app_localizations.dart'; import '../../../core/network/dio_client.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/utils/number_format.dart'; import '../../../data/services/spot_service.dart'; import '../../widgets/common/app_tab_bar.dart'; import '../../../providers/spot_provider.dart'; import '../../../providers/spot_symbol_cache_provider.dart'; /// 现货历史记录:历史委托 / 历史成交 class SpotHistoryScreen extends ConsumerStatefulWidget { const SpotHistoryScreen({super.key, required this.symbol}); final String symbol; @override ConsumerState createState() => _SpotHistoryScreenState(); } class _SpotHistoryScreenState extends ConsumerState with SingleTickerProviderStateMixin { late TabController _tabCtrl; late PageController _pageCtrl; static const int _pageSize = 20; static const int _maxSkipEmptyPages = 12; _KindFilter _kind = _KindFilter.all; _StatusFilter _status = _StatusFilter.all; _TimeFilter _time = _TimeFilter.all; /// true:仅当前路由交易对;false:全部交易对 bool _useCurrentSymbolOnly = false; /// 是否已在交易对筛选里点选过;未点选时 pill 展示原型「交易对」 bool _pairFilterTouched = false; final _ordersScroll = ScrollController(); final List _orders = []; int _nextOrdersPage = 1; bool _ordersHasMore = true; bool _ordersLoading = false; int _ordersRawLoaded = 0; final _tradesScroll = ScrollController(); final List _trades = []; int _nextTradesPage = 1; bool _tradesHasMore = true; bool _tradesLoading = false; int _tradesRawLoaded = 0; @override void initState() { super.initState(); _tabCtrl = TabController(length: 2, vsync: this); _pageCtrl = PageController(); // Tab 点击 → 驱动 PageView 动画(与合约历史页一致) _tabCtrl.addListener(() { if (!mounted) return; if (!_tabCtrl.indexIsChanging) return; _pageCtrl.animateToPage( _tabCtrl.index, duration: const Duration(milliseconds: 280), curve: Curves.easeOut, ); }); // 横向滑动 PageView → 指示器 offset 平滑插值 _pageCtrl.addListener(() { if (!mounted) return; if (!_pageCtrl.hasClients) return; if (_tabCtrl.indexIsChanging) return; final page = _pageCtrl.page!; final offset = page - _tabCtrl.index; if (offset.abs() <= 1.0) { _tabCtrl.offset = offset.clamp(-1.0, 1.0); } }); _ordersScroll.addListener(_onOrdersScroll); _tradesScroll.addListener(_onTradesScroll); _refreshOrders(); } void _resetFiltersToDefault() { _kind = _KindFilter.all; _status = _StatusFilter.all; _time = _TimeFilter.all; _useCurrentSymbolOnly = false; _pairFilterTouched = false; } /// PageView 切换完成:重置筛选并刷新当前页数据 void _onPageCommitted(int index) { if (!mounted) return; _resetFiltersToDefault(); if (index == 0) { if (_ordersScroll.hasClients) { _ordersScroll.jumpTo(0); } _refreshOrders(); } else { if (_tradesScroll.hasClients) { _tradesScroll.jumpTo(0); } _refreshTrades(); } } void _onOrdersScroll() { if (_ordersScroll.position.pixels >= _ordersScroll.position.maxScrollExtent - 200) { _loadMoreOrders(); } } void _onTradesScroll() { if (_tradesScroll.position.pixels >= _tradesScroll.position.maxScrollExtent - 200) { _loadMoreTrades(); } } @override void dispose() { _tabCtrl.dispose(); _pageCtrl.dispose(); _ordersScroll.dispose(); _tradesScroll.dispose(); super.dispose(); } String get _apiSymbol => widget.symbol .replaceAll('/', '') .replaceAll('-', '') .toUpperCase(); /// 交易对 pill:未选过展示「交易对」;选过后展示「全部交易对」或具体币对 String _pairChipDisplay(AppLocalizations l10n) { if (!_pairFilterTouched) return l10n.spotHistoryFilterSymbol; return _useCurrentSymbolOnly ? _apiSymbol : l10n.spotFilterSymbolAllPairs; } /// 委托类型:全部时展示「全部委托」,否则展示市价/限价(与原型一致) String _kindChipDisplay(AppLocalizations l10n) { switch (_kind) { case _KindFilter.all: return l10n.spotFilterEntrustAll; case _KindFilter.market: return l10n.spotFilterKindMarket; case _KindFilter.limit: return l10n.spotFilterKindLimit; } } String _statusChipDisplay(AppLocalizations l10n) { switch (_status) { case _StatusFilter.all: return l10n.spotHistoryFilterStatus; case _StatusFilter.completed: return l10n.spotOrderStatusCompleted; case _StatusFilter.partial: return l10n.spotOrderStatusPartialFilled; case _StatusFilter.cancelled: return l10n.spotOrderStatusCancelled; } } String _timeChipDisplay(AppLocalizations l10n) { switch (_time) { case _TimeFilter.all: return l10n.spotHistoryFilterTime; case _TimeFilter.d7: return l10n.spotFilterTime7d; case _TimeFilter.d30: return l10n.spotFilterTime30d; } } int? get _ctimeBeginMs { final now = DateTime.now(); switch (_time) { case _TimeFilter.all: return null; case _TimeFilter.d7: return now.subtract(const Duration(days: 7)).millisecondsSinceEpoch; case _TimeFilter.d30: return now.subtract(const Duration(days: 30)).millisecondsSinceEpoch; } } /// 历史委托 Tab:传 type 给服务端(并做客户端二次过滤) int? _apiTypeOrders() { switch (_kind) { case _KindFilter.all: return null; case _KindFilter.limit: return 1; case _KindFilter.market: return 2; } } /// 历史成交 Tab:不按委托类型筛(与原型一致),仅客户端可按状态/时间筛 int? _apiTypeTrades() => null; int? _apiStatus() { switch (_status) { case _StatusFilter.all: case _StatusFilter.partial: return null; case _StatusFilter.completed: return 2; case _StatusFilter.cancelled: return 4; } } bool _matchesFilters(SpotOrder o, {required bool tradesTab}) { if (_useCurrentSymbolOnly) { final os = o.symbol.replaceAll('/', '').toUpperCase(); if (os != _apiSymbol) return false; } if (!tradesTab) { switch (_kind) { case _KindFilter.all: break; case _KindFilter.limit: if (o.typeCode != 1) return false; case _KindFilter.market: if (o.typeCode != 2) return false; } } switch (_status) { case _StatusFilter.all: break; case _StatusFilter.completed: if (o.statusCode != 2) return false; case _StatusFilter.partial: final isPartial = o.statusCode == 3 || ((o.statusCode == 4 || o.statusCode == 5 || o.statusCode == 6) && o.tradedAmount > 0 && o.tradedAmount < o.amount); if (!isPartial) return false; case _StatusFilter.cancelled: final partial = o.tradedAmount > 0 && o.tradedAmount < o.amount; final isCancelState = o.statusCode == 4 || o.statusCode == 5 || o.statusCode == 6; if (!isCancelState || partial) return false; } final begin = _ctimeBeginMs; if (begin != null) { final t = o.createTime; if (t != null && t.millisecondsSinceEpoch < begin) return false; } return true; } Future _refreshOrders() async { setState(() { _ordersLoading = true; _nextOrdersPage = 1; _ordersHasMore = true; _orders.clear(); _ordersRawLoaded = 0; }); await _fetchOrdersPage(); } Future _loadMoreOrders() async { if (_ordersLoading || !_ordersHasMore) return; setState(() => _ordersLoading = true); await _fetchOrdersPage(); } Future _fetchOrdersPage() async { try { final svc = SpotService(ref.read(dioClientProvider)); int skipped = 0; while (skipped <= _maxSkipEmptyPages && _ordersHasMore) { final data = await svc.getHistoryOrders( page: _nextOrdersPage, size: _pageSize, symbol: _useCurrentSymbolOnly ? _apiSymbol : null, type: _apiTypeOrders(), status: _apiStatus(), ctimeBegin: _ctimeBeginMs, ); final list = _extractRecordMaps(data); final total = _parsePageTotal(data); if (list.isEmpty) { _ordersHasMore = false; break; } _ordersRawLoaded += list.length; final filtered = list .map(SpotOrder.fromJson) .where((o) => _matchesFilters(o, tradesTab: false)) .toList(); _orders.addAll(filtered); _nextOrdersPage++; _ordersHasMore = total > _ordersRawLoaded || (total == 0 && list.length >= _pageSize); if (filtered.isNotEmpty || !_ordersHasMore) break; skipped++; } } catch (_) { _ordersHasMore = false; } finally { if (mounted) setState(() => _ordersLoading = false); } } Future _refreshTrades() async { setState(() { _tradesLoading = true; _nextTradesPage = 1; _tradesHasMore = true; _trades.clear(); _tradesRawLoaded = 0; }); await _fetchTradesPage(); } Future _loadMoreTrades() async { if (_tradesLoading || !_tradesHasMore) return; setState(() => _tradesLoading = true); await _fetchTradesPage(); } Future _fetchTradesPage() async { try { final svc = SpotService(ref.read(dioClientProvider)); int skipped = 0; while (skipped <= _maxSkipEmptyPages && _tradesHasMore) { final data = await svc.getTrades( page: _nextTradesPage, size: _pageSize, symbol: _useCurrentSymbolOnly ? _apiSymbol : null, type: _apiTypeTrades(), status: _apiStatus(), ctimeBegin: _ctimeBeginMs, ); final list = _extractRecordMaps(data); final total = _parsePageTotal(data); if (list.isEmpty) { _tradesHasMore = false; break; } _tradesRawLoaded += list.length; final filtered = list .map(SpotOrder.fromJson) .where((o) => _matchesFilters(o, tradesTab: true)) .toList(); _trades.addAll(filtered); _nextTradesPage++; _tradesHasMore = total > _tradesRawLoaded || (total == 0 && list.length >= _pageSize); if (filtered.isNotEmpty || !_tradesHasMore) break; skipped++; } } catch (_) { _tradesHasMore = false; } finally { if (mounted) setState(() => _tradesLoading = false); } } Future _applyFilters() async { await Future.wait([_refreshOrders(), _refreshTrades()]); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Scaffold( appBar: AppBar( title: Text(l10n.spotHistoryRecordsTitle), bottom: AppTabBar( controller: _tabCtrl, tabs: [ Tab(text: l10n.spotHistoryTitle), Tab(text: l10n.spotHistoryTradesTab), ], ), ), body: PageView( controller: _pageCtrl, physics: const BouncingScrollPhysics( parent: AlwaysScrollableScrollPhysics(), ), onPageChanged: (index) { if (!mounted) return; if (!_tabCtrl.indexIsChanging) { _tabCtrl.index = index; } _onPageCommitted(index); }, children: [ _OrdersTabBody( scrollController: _ordersScroll, orders: _orders, loading: _ordersLoading, hasMore: _ordersHasMore, onRefresh: _refreshOrders, tradesMode: false, pairText: _pairChipDisplay(l10n), kindText: _kindChipDisplay(l10n), statusText: _statusChipDisplay(l10n), timeText: _timeChipDisplay(l10n), onPickSymbol: () => _pickSymbol(l10n), onPickKind: () => _pickKind(l10n), onPickStatus: () => _pickStatus(l10n), onPickTime: () => _pickTime(l10n), ), _OrdersTabBody( scrollController: _tradesScroll, orders: _trades, loading: _tradesLoading, hasMore: _tradesHasMore, onRefresh: _refreshTrades, tradesMode: true, pairText: _pairChipDisplay(l10n), kindText: null, statusText: _statusChipDisplay(l10n), timeText: _timeChipDisplay(l10n), onPickSymbol: () => _pickSymbol(l10n), onPickKind: null, onPickStatus: () => _pickStatus(l10n), onPickTime: () => _pickTime(l10n), ), ], ), ); } Future _pickSymbol(AppLocalizations l10n) async { final cs = Theme.of(context).colorScheme; final v = await showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: cs.surface, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (ctx) => _SimpleSheet( selected: _useCurrentSymbolOnly, options: [ (false, l10n.spotFilterSymbolAllPairs), (true, _apiSymbol), ], ), ); if (v != null && mounted) { setState(() { _pairFilterTouched = true; _useCurrentSymbolOnly = v; }); await _applyFilters(); } } Future _pickKind(AppLocalizations l10n) async { final cs = Theme.of(context).colorScheme; final v = await showModalBottomSheet<_KindFilter>( context: context, useRootNavigator: true, backgroundColor: cs.surface, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (ctx) => _SimpleSheet<_KindFilter>( selected: _kind, options: [ (_KindFilter.all, l10n.spotFilterKindAll), (_KindFilter.market, l10n.spotFilterKindMarket), (_KindFilter.limit, l10n.spotFilterKindLimit), ], ), ); if (v != null && mounted) { setState(() => _kind = v); await _applyFilters(); } } Future _pickStatus(AppLocalizations l10n) async { final cs = Theme.of(context).colorScheme; final v = await showModalBottomSheet<_StatusFilter>( context: context, useRootNavigator: true, backgroundColor: cs.surface, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (ctx) => _SimpleSheet<_StatusFilter>( selected: _status, options: [ (_StatusFilter.all, l10n.spotFilterStatusAll), (_StatusFilter.completed, l10n.spotOrderStatusCompleted), (_StatusFilter.partial, l10n.spotOrderStatusPartialFilled), (_StatusFilter.cancelled, l10n.spotOrderStatusCancelled), ], ), ); if (v != null && mounted) { setState(() => _status = v); await _applyFilters(); } } Future _pickTime(AppLocalizations l10n) async { final cs = Theme.of(context).colorScheme; final v = await showModalBottomSheet<_TimeFilter>( context: context, useRootNavigator: true, backgroundColor: cs.surface, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (ctx) => _SimpleSheet<_TimeFilter>( selected: _time, options: [ (_TimeFilter.all, l10n.spotFilterTimeAll), (_TimeFilter.d7, l10n.spotFilterTime7d), (_TimeFilter.d30, l10n.spotFilterTime30d), ], ), ); if (v != null && mounted) { setState(() => _time = v); await _applyFilters(); } } } List> _extractRecordMaps(Map data) { final raw = data['records'] ?? data['list']; if (raw is! List) return []; return raw.whereType>().toList(); } int _parsePageTotal(Map data) { final t = data['total'] ?? data['totalRow'] ?? data['totalCount']; if (t is int) return t; if (t is num) return t.toInt(); return 0; } enum _KindFilter { all, market, limit } enum _StatusFilter { all, completed, partial, cancelled } enum _TimeFilter { all, d7, d30 } /// 选项样式与合约页「仓位模式」底部弹窗一致:圆角描边、选中加粗 + 对勾 class _SimpleSheet extends StatelessWidget { const _SimpleSheet({ super.key, required this.selected, required this.options, }); final T selected; final List<(T, String)> options; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return SafeArea( child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (var i = 0; i < options.length; i++) ...[ if (i > 0) const SizedBox(height: 10), GestureDetector( onTap: () => Navigator.pop(context, options[i].$1), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( border: Border.all( color: options[i].$1 == selected ? cs.onSurface : cs.outline, width: options[i].$1 == selected ? 1.5 : 1, ), borderRadius: BorderRadius.circular(10), color: Colors.transparent, ), child: Row( children: [ Expanded( child: Text( options[i].$2, style: TextStyle( color: cs.onSurface, fontWeight: FontWeight.w600, fontSize: 14, ), ), ), if (options[i].$1 == selected) Icon(Icons.check_circle, size: 16, color: cs.onSurface), ], ), ), ), ], ], ), ), ), ); } } class _OrdersTabBody extends StatelessWidget { const _OrdersTabBody({ required this.scrollController, required this.orders, required this.loading, required this.hasMore, required this.onRefresh, required this.tradesMode, required this.pairText, this.kindText, required this.statusText, required this.timeText, required this.onPickSymbol, this.onPickKind, required this.onPickStatus, required this.onPickTime, }); final ScrollController scrollController; final List orders; final bool loading; final bool hasMore; final Future Function() onRefresh; final bool tradesMode; final String pairText; final String? kindText; final String statusText; final String timeText; final VoidCallback onPickSymbol; final VoidCallback? onPickKind; final VoidCallback onPickStatus; final VoidCallback onPickTime; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; final showKind = !tradesMode && kindText != null && onPickKind != null; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), child: Row( children: [ Expanded( child: _DropdownFilterPill( text: pairText, onTap: onPickSymbol, ), ), const SizedBox(width: 8), if (showKind) ...[ Expanded( child: _DropdownFilterPill( text: kindText!, onTap: onPickKind!, ), ), const SizedBox(width: 8), ], Expanded( child: _DropdownFilterPill( text: statusText, onTap: onPickStatus, ), ), const SizedBox(width: 8), Expanded( child: _DropdownFilterPill( text: timeText, onTap: onPickTime, ), ), ], ), ), Expanded( child: RefreshIndicator( onRefresh: onRefresh, child: orders.isEmpty && !loading ? ListView( physics: const AlwaysScrollableScrollPhysics(), children: [ const SizedBox(height: 120), Center( child: Text( tradesMode ? l10n.noTradeData : l10n.noHistoryOrders, style: TextStyle( color: cs.onSurface.withAlpha(140), fontSize: 14, ), ), ), ], ) : ListView.builder( controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), itemCount: orders.length + 1, itemBuilder: (_, i) { if (i == orders.length) { if (loading) { return const Padding( padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()), ); } if (!hasMore) { return Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Center( child: Text( l10n.noMoreData, style: TextStyle( color: cs.onSurface.withAlpha(120), fontSize: 12, ), ), ), ); } return const SizedBox.shrink(); } return _HistoryCard( order: orders[i], tradesMode: tradesMode, ); }, ), ), ), ], ); } } /// 原型:浅灰圆角条,单行文案 + 右侧下拉箭头;默认展示维度名,选中后展示选项文案 class _DropdownFilterPill extends StatelessWidget { const _DropdownFilterPill({ required this.text, required this.onTap, }); final String text; final VoidCallback onTap; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final bg = isDark ? cs.surfaceContainerHighest.withAlpha(120) : const Color(0xFFF2F2F4); return Material( color: bg, borderRadius: BorderRadius.circular(8), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.fromLTRB(8, 10, 4, 10), child: Row( children: [ Expanded( child: Text( text, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13, color: cs.onSurface, fontWeight: FontWeight.w500, ), ), ), Icon( Icons.keyboard_arrow_down, size: 20, color: cs.onSurface.withAlpha(130), ), ], ), ), ), ); } } class _HistoryCard extends ConsumerWidget { const _HistoryCard({ required this.order, required this.tradesMode, }); final SpotOrder order; final bool tradesMode; @override Widget build(BuildContext context, WidgetRef ref) { final cs = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; final base = _spotBaseAsset(order.symbol); final quote = 'USDT'; final sym = order.symbol.replaceAll('/', '').toUpperCase(); final precision = ref.watch( spotSymbolCacheProvider.select((map) => map[sym])); final pricePre = precision?.pricePre ?? 2; final volPre = precision?.volumePre ?? 4; final statusLabel = _historyStatusLabel(order, l10n); final statusColor = _historyStatusColor(cs, order); final sideColor = order.side == SpotSide.buy ? AppColors.rise : AppColors.fall; final typeSide = _orderTypeSideLine(order, l10n); return Container( padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), decoration: BoxDecoration( border: Border( bottom: BorderSide(color: cs.outline.withAlpha(40)), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( order.symbol.replaceAll('/', ''), style: TextStyle( color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w700, ), ), ), Text( statusLabel, style: TextStyle( color: statusColor, fontSize: 13, fontWeight: FontWeight.w600, ), ), ], ), const SizedBox(height: 6), Text( typeSide, style: TextStyle( color: sideColor, fontSize: 13, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 10), if (tradesMode) ...[ _kvRow( context, '${l10n.tradedDealAmount}($base)', '${formatAmount(order.tradedAmount, decimals: volPre)} / ${formatAmount(order.amount, decimals: volPre)}', ), const SizedBox(height: 6), _kvRow( context, '${l10n.spotAvgPrice}($quote)', order.avgPrice > 0 ? formatAmount(order.avgPrice, decimals: pricePre) : '--', ), const SizedBox(height: 6), _kvRow( context, l10n.spotDealTime, order.createTime != null ? _fmtTime(order.createTime!) : '--', ), ] else ...[ _kvRow( context, '${l10n.spotEntrustQuantity}($base)', formatAmount(order.amount, decimals: volPre), ), const SizedBox(height: 6), _kvRow( context, '${l10n.orderPriceLabel}($quote)', order.type == SpotOrderType.market && order.price <= 0 ? l10n.marketPrice : formatAmount(order.price, decimals: pricePre), ), const SizedBox(height: 6), _kvRow( context, l10n.orderTime, order.createTime != null ? _fmtTime(order.createTime!) : '--', ), ], ], ), ); } } Widget _kvRow(BuildContext context, String k, String v) { final cs = Theme.of(context).colorScheme; return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 12, child: Text( k, style: TextStyle( color: cs.onSurface.withAlpha(140), fontSize: 12, ), ), ), Expanded( flex: 13, child: Text( v, textAlign: TextAlign.right, style: TextStyle( color: cs.onSurface, fontSize: 13, fontWeight: FontWeight.w500, fontFeatures: const [FontFeature.tabularFigures()], ), ), ), ], ); } String _fmtTime(DateTime t) { final x = t.toLocal(); String two(int n) => n.toString().padLeft(2, '0'); return '${x.year}-${two(x.month)}-${two(x.day)} ${two(x.hour)}:${two(x.minute)}:${two(x.second)}'; } String _spotBaseAsset(String symbolUpper) { final s = symbolUpper.replaceAll('/', '').toUpperCase(); const quotes = ['USDT', 'USDC', 'BUSD', 'TUSD']; for (final q in quotes) { if (s.endsWith(q) && s.length > q.length) { return s.substring(0, s.length - q.length); } } return s; } Color _historyStatusColor(ColorScheme cs, SpotOrder o) { if (o.statusCode == 2) return AppColors.rise; return cs.onSurface; } String _historyStatusLabel(SpotOrder o, AppLocalizations l10n) { if (o.statusCode == 2) return l10n.spotOrderStatusCompleted; if (o.statusCode == 3) return l10n.spotOrderStatusPartialFilled; if (o.statusCode == 4 || o.statusCode == 5 || o.statusCode == 6) { if (o.tradedAmount > 0 && o.tradedAmount < o.amount) { return l10n.spotOrderStatusPartialFilled; } return l10n.spotOrderStatusCancelled; } return o.status; } String _orderTypeSideLine(SpotOrder o, AppLocalizations l10n) { final type = switch (o.type) { SpotOrderType.market => l10n.marketOrder, SpotOrderType.conditionalMarket => l10n.spotOrderTypeMarketConditional, _ => l10n.limitOrder, }; final side = o.side == SpotSide.buy ? l10n.buyAction : l10n.sellAction; return '$type / $side'; }