import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/l10n/app_localizations.dart'; import '../../../core/network/dio_client.dart'; import '../../../core/theme/app_colors.dart'; import '../../../data/services/futures_service.dart'; import '../../widgets/common/app_refresh_indicator.dart'; import '../../widgets/common/app_shimmer.dart'; import '../../widgets/common/app_tab_bar.dart'; // ── 分页状态 ──────────────────────────────────────────────── class _PageState { final List> items; final bool isLoading; final bool hasMore; final int page; final String? error; const _PageState({ this.items = const [], this.isLoading = false, this.hasMore = true, this.page = 0, this.error, }); _PageState copyWith({ List>? items, bool? isLoading, bool? hasMore, int? page, String? error, }) => _PageState( items: items ?? this.items, isLoading: isLoading ?? this.isLoading, hasMore: hasMore ?? this.hasMore, page: page ?? this.page, error: error, ); } // ── 历史持仓分页 Notifier ─────────────────────────────────── class _PositionHistoryNotifier extends AutoDisposeNotifier<_PageState> { static const _pageSize = 10; @override _PageState build() { Future.microtask(loadMore); return const _PageState(); } Future loadMore() async { final s = state; if (s.isLoading || !s.hasMore) return; state = s.copyWith(isLoading: true); try { final svc = FuturesService(ref.read(dioClientProvider)); final result = await svc.getPositionHistory( pageNo: s.page + 1, pageSize: _pageSize, ); state = state.copyWith( items: [...s.items, ...result.items], hasMore: result.hasMore, page: s.page + 1, isLoading: false, ); } catch (e) { state = state.copyWith(isLoading: false, error: e.toString()); } } Future refresh() async { state = const _PageState(); await loadMore(); } } final _positionHistoryProvider = NotifierProvider.autoDispose<_PositionHistoryNotifier, _PageState>( _PositionHistoryNotifier.new, ); // ── 历史委托分页 Notifier ─────────────────────────────────── // 使用 history-all 接口,一次返回所有历史委托(ContractOrderEntrust) // 包含:开仓委托(含撤销/失败)+ 平仓委托(成功) class _OrderHistoryNotifier extends AutoDisposeNotifier<_PageState> { static const _pageSize = 10; @override _PageState build() { Future.microtask(loadMore); return const _PageState(); } Future loadMore() async { final s = state; if (s.isLoading || !s.hasMore) return; state = s.copyWith(isLoading: true); try { final svc = FuturesService(ref.read(dioClientProvider)); final nextPage = s.page + 1; final result = await svc.getOrderHistoryAll( pageNo: nextPage, pageSize: _pageSize, ); state = state.copyWith( items: [...s.items, ...result.items], hasMore: result.hasMore, page: nextPage, isLoading: false, ); } catch (e) { state = state.copyWith(isLoading: false, error: e.toString()); } } Future refresh() async { state = const _PageState(); await loadMore(); } } final _orderHistoryProvider = NotifierProvider.autoDispose<_OrderHistoryNotifier, _PageState>( _OrderHistoryNotifier.new, ); // ── Screen ───────────────────────────────────────────────── class FuturesHistoryScreen extends ConsumerStatefulWidget { const FuturesHistoryScreen({super.key}); @override ConsumerState createState() => _FuturesHistoryScreenState(); } class _FuturesHistoryScreenState extends ConsumerState with SingleTickerProviderStateMixin { late final TabController _tabCtrl; late final PageController _pageCtrl; @override void initState() { super.initState(); _tabCtrl = TabController(length: 2, vsync: this); _pageCtrl = PageController(); // Tab 点击 → 驱动 PageView 动画 _tabCtrl.addListener(() { if (!_tabCtrl.indexIsChanging) return; _pageCtrl.animateToPage( _tabCtrl.index, duration: const Duration(milliseconds: 280), curve: Curves.easeOut, ); }); // 拖动 PageView → 实时更新指示器 offset(平滑插值) _pageCtrl.addListener(() { 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); } }); } @override void dispose() { _tabCtrl.dispose(); _pageCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; return Scaffold( backgroundColor: isDark ? AppColors.darkBg : AppColors.lightBgSecondary, appBar: AppBar( backgroundColor: isDark ? AppColors.darkBgSecondary : Colors.white, elevation: 0, leading: IconButton( icon: Icon(Icons.chevron_left, color: cs.onSurface, size: 28), onPressed: () => Navigator.pop(context), ), title: TabBar( controller: _tabCtrl, tabs: [ Tab(text: AppLocalizations.of(context)!.historicalPositions), Tab(text: AppLocalizations.of(context)!.historicalOrders), ], indicator: StretchTabIndicator( controller: _tabCtrl, color: AppColors.brand, ), indicatorSize: TabBarIndicatorSize.label, dividerColor: Colors.transparent, ), titleSpacing: 0, bottom: PreferredSize( preferredSize: const Size.fromHeight(1), child: Divider( height: 1, color: isDark ? AppColors.darkDivider : AppColors.lightDivider, ), ), ), body: PageView( controller: _pageCtrl, physics: const BouncingScrollPhysics( parent: AlwaysScrollableScrollPhysics(), ), onPageChanged: (index) { if (_tabCtrl.indexIsChanging) return; _tabCtrl.index = index; }, children: const [ _PositionHistoryTab(), _OrderHistoryTab(), ], ), ); } } // ── 历史持仓 Tab ────────────────────────────────────────── class _PositionHistoryTab extends ConsumerStatefulWidget { const _PositionHistoryTab(); @override ConsumerState<_PositionHistoryTab> createState() => _PositionHistoryTabState(); } class _PositionHistoryTabState extends ConsumerState<_PositionHistoryTab> { final _scroll = ScrollController(); @override void initState() { super.initState(); _scroll.addListener(_onScroll); } @override void dispose() { _scroll.removeListener(_onScroll); _scroll.dispose(); super.dispose(); } void _onScroll() { if (_scroll.position.pixels >= _scroll.position.maxScrollExtent - 200) { ref.read(_positionHistoryProvider.notifier).loadMore(); } } @override Widget build(BuildContext context) { final s = ref.watch(_positionHistoryProvider); if (s.items.isEmpty && s.isLoading) { return const _HistoryListShimmer(isPosition: true); } if (s.items.isEmpty && !s.isLoading) { return _EmptyHint(message: s.error != null ? AppLocalizations.of(context)!.loadFailedRetry : null); } return AppRefreshIndicator( onRefresh: () => ref.read(_positionHistoryProvider.notifier).refresh(), child: ListView.separated( controller: _scroll, padding: const EdgeInsets.fromLTRB(12, 12, 12, 24), itemCount: s.items.length + 1, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (ctx, i) { if (i == s.items.length) { return _LoadMoreFooter(isLoading: s.isLoading, hasMore: s.hasMore); } return _PositionHistoryCard(data: s.items[i]); }, ), ); } } // ── 历史委托 Tab ────────────────────────────────────────── class _OrderHistoryTab extends ConsumerStatefulWidget { const _OrderHistoryTab(); @override ConsumerState<_OrderHistoryTab> createState() => _OrderHistoryTabState(); } class _OrderHistoryTabState extends ConsumerState<_OrderHistoryTab> { final _scroll = ScrollController(); @override void initState() { super.initState(); _scroll.addListener(_onScroll); } @override void dispose() { _scroll.removeListener(_onScroll); _scroll.dispose(); super.dispose(); } void _onScroll() { if (_scroll.position.pixels >= _scroll.position.maxScrollExtent - 200) { ref.read(_orderHistoryProvider.notifier).loadMore(); } } @override Widget build(BuildContext context) { final s = ref.watch(_orderHistoryProvider); if (s.items.isEmpty && s.isLoading) { return const _HistoryListShimmer(isPosition: false); } if (s.items.isEmpty && !s.isLoading) { return _EmptyHint(message: s.error != null ? AppLocalizations.of(context)!.loadFailedRetry : null); } return AppRefreshIndicator( onRefresh: () => ref.read(_orderHistoryProvider.notifier).refresh(), child: ListView.separated( controller: _scroll, padding: const EdgeInsets.fromLTRB(12, 12, 12, 24), itemCount: s.items.length + 1, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (ctx, i) { if (i == s.items.length) { return _LoadMoreFooter(isLoading: s.isLoading, hasMore: s.hasMore); } return _OrderHistoryCard(data: s.items[i]); }, ), ); } } // ── 持仓历史卡片 ────────────────────────────────────────── class _PositionHistoryCard extends StatelessWidget { const _PositionHistoryCard({required this.data}); final Map data; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final l10n = AppLocalizations.of(context)!; // symbol 优先读嵌套 coin 对象(持仓历史结构),兼容委托历史平铺字段 final coinObj = data['coin'] as Map?; final symbol = _str(coinObj?['symbol'] ?? data['symbol'] ?? data['coinSymbol'] ?? ''); final dirInfo = _directionInfo(data['direction'], null, l10n); final dirColor = dirInfo.isGreen ? AppColors.rise : AppColors.fall; final openTypeLabel = _mapOrderType(data['openType'], l10n); final closeTypeLabel = _mapCloseType(data['closeType'], l10n); final leverage = (_toDouble(data['leverage'] ?? 0)).toInt(); final positionTypeLabel = _mapPositionType( data['type'] ?? data['positionType'], data['patterns'], l10n); final positionTypeTagColor = _positionTypeColor( data['type'] ?? data['positionType'], data['patterns']); final status = _str(data['status'] ?? ''); final statusLabel = _mapStatus(status, l10n, isPosition: true); final statusColor = _statusColor(status); final coinLabel = symbol.contains('/') ? symbol.split('/').first : symbol.replaceAll(RegExp(r'USDT$|BUSD$|BTC$|ETH$'), '').isNotEmpty ? symbol.replaceAll(RegExp(r'USDT$|BUSD$'), '') : coinObj?['coinSymbol']?.toString() ?? 'BTC'; final totalVolume = _toDouble(data['totalPosition'] ?? data['volume'] ?? 0); final dealVolume = _toDouble(data['closedPosition'] ?? data['tradedVolume'] ?? data['dealVolume'] ?? 0); final openAvgPrice = _toDouble(data['openPrice'] ?? data['usdtOpenPrice'] ?? 0); final closeAvgPrice = _toDouble(data['closePrice'] ?? data['tradedPrice'] ?? 0); final profitLoss = _toDouble(data['usdtProfit'] ?? data['profitAndLoss'] ?? 0); final totalPrincipal = _toDouble(data['totalPrincipalAmount'] ?? data['principalAmount'] ?? 0); final profitRate = totalPrincipal > 0 ? (profitLoss / totalPrincipal * 100) : 0.0; final openTime = _formatTime(data['openTime'] ?? data['usdtOpenTime'] ?? data['createTime']); final closeTime = _formatTime(data['closeTime'] ?? data['dealTime']); final cardBg = isDark ? AppColors.darkBgSecondary : Colors.white; final dividerColor = isDark ? AppColors.darkDivider : AppColors.lightDivider; return GestureDetector( onTap: () { final uri = GoRouterState.of(context).uri.toString(); context.push('$uri/position-detail', extra: data); }, child: Container( decoration: BoxDecoration( color: cardBg, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withAlpha(isDark ? 40 : 15), blurRadius: 6, offset: const Offset(0, 1), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── 头部:币对名 + 状态 ───────────────────────── Padding( padding: const EdgeInsets.fromLTRB(14, 14, 14, 10), child: Row( children: [ Expanded( child: Row( children: [ Text( symbol.isNotEmpty ? '$symbol ${AppLocalizations.of(context)!.perpetual}' : '--', style: TextStyle( color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w700, ), ), const SizedBox(width: 2), Icon(Icons.chevron_right, color: cs.onSurface.withAlpha(100), size: 16), ], ), ), Text( statusLabel, style: TextStyle( color: statusColor, fontSize: 13, fontWeight: FontWeight.w600, ), ), ], ), ), // ── 标签行 ───────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(14, 0, 14, 12), child: Wrap( spacing: 6, runSpacing: 6, children: [ _Tag(label: dirInfo.label, tagStyle: _TagStyle.direction, color: dirColor), if (openTypeLabel.isNotEmpty) _Tag(label: openTypeLabel, tagStyle: _TagStyle.orderType), if (closeTypeLabel.isNotEmpty) _Tag(label: closeTypeLabel, tagStyle: _TagStyle.closeType), _Tag(label: positionTypeLabel, tagStyle: _TagStyle.positionType, color: positionTypeTagColor), if (leverage > 0) _Tag(label: '${leverage}x', tagStyle: _TagStyle.gray), ], ), ), Divider(height: 1, color: dividerColor), // ── 数据行 1:委托总量 | 开仓均价 | 收益 ────────── Padding( padding: const EdgeInsets.fromLTRB(14, 10, 14, 6), child: Row( children: [ _DataField( label: '${l10n.entrustTotal}($coinLabel)', value: _rawNum(totalVolume), align: CrossAxisAlignment.start, ), _DataField( label: '${l10n.openAvgPrice}(USDT)', value: openAvgPrice > 0 ? _rawNum(openAvgPrice) : '--', align: CrossAxisAlignment.center, ), _DataField( label: '${l10n.profitLabel}(USDT)', value: profitLoss != 0 ? '${profitLoss >= 0 ? '+' : ''}${_rawNum(profitLoss)}' : '--', valueColor: profitLoss != 0 ? AppColors.changeColor(profitLoss) : null, align: CrossAxisAlignment.end, ), ], ), ), // ── 数据行 2:已成交量 | 平仓均价 | 收益率 ───────── Padding( padding: const EdgeInsets.fromLTRB(14, 0, 14, 12), child: Row( children: [ _DataField( label: '${l10n.filledVolume}($coinLabel)', value: _rawNum(dealVolume), align: CrossAxisAlignment.start, ), _DataField( label: '${l10n.closeAvgPrice}(USDT)', value: closeAvgPrice > 0 ? _rawNum(closeAvgPrice) : '--', align: CrossAxisAlignment.center, ), _DataField( label: l10n.profitRateLabel, value: profitRate != 0 ? '${profitRate >= 0 ? '+' : ''}${profitRate.toStringAsFixed(2)}%' : '--', valueColor: profitRate != 0 ? AppColors.changeColor(profitRate) : null, align: CrossAxisAlignment.end, ), ], ), ), Divider(height: 1, color: dividerColor), // ── 时间行 ───────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(14, 10, 14, 12), child: Row( children: [ Expanded( child: _TimeBlock( label: l10n.openTime, time: openTime, align: CrossAxisAlignment.start, ), ), _TimeBlock( label: l10n.closeTime, time: closeTime, align: CrossAxisAlignment.end, ), ], ), ), ], ), ), ); } } // ── 委托历史卡片 ────────────────────────────────────────── class _OrderHistoryCard extends StatelessWidget { const _OrderHistoryCard({required this.data}); final Map data; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final l10n = AppLocalizations.of(context)!; // history-all 返回 ContractOrderEntrust,symbol 直接在顶层 final symbol = _str(data['symbol'] ?? data['coinSymbol'] ?? ''); final dirInfo = _directionInfo(data['direction'], data['entrustType'], l10n); final dirColor = dirInfo.isGreen ? AppColors.rise : AppColors.fall; final openTypeLabel = _mapOrderType(data['type'], l10n); final leverage = (_toDouble(data['leverage'] ?? 0)).toInt(); final positionTypeLabel = _mapPositionType( data['positionType'], data['patterns'], l10n); final positionTypeTagColor = _positionTypeColor( data['positionType'], data['patterns']); final status = _str(data['status'] ?? ''); final statusLabel = _mapStatus(status, l10n); final statusColor = _statusColor(status); final coinLabel = symbol.contains('/') ? symbol.split('/').first : symbol.replaceAll(RegExp(r'USDT$|BUSD$'), '').isNotEmpty ? symbol.replaceAll(RegExp(r'USDT$|BUSD$'), '') : _str(data['coinSymbol'] ?? 'BTC'); final volume = _toDouble(data['volume'] ?? 0); final dealVolume = _toDouble(data['tradedVolume'] ?? 0); final entrustPrice = _toDouble(data['entrustPrice'] ?? 0); // 开仓均价:开仓单用 tradedPrice(成交均价);平仓单用 usdtOpenPrice(原仓位开仓均价) final entrustTypeRaw = (data['entrustType']?.toString() ?? '').toUpperCase(); final isCloseOrd = entrustTypeRaw == '1' || entrustTypeRaw == 'CLOSE'; final openAvgPrice = isCloseOrd ? _toDouble(data['usdtOpenPrice'] ?? data['openPrice'] ?? 0) : _toDouble(data['tradedPrice'] ?? data['usdtOpenPrice'] ?? 0); final createTime = _formatTime(data['createTime']); final dealTime = _formatTime(data['dealTime']); // 委托价格:市价/计划市价→"市价",限价/计划限价→显示原始委托价 final typeRaw = data['type']; final entrustPriceStr = _showMarketPrice(typeRaw, entrustPrice) ? l10n.marketOrderType : (entrustPrice > 0 ? _rawNum(entrustPrice) : '--'); final cardBg = isDark ? AppColors.darkBgSecondary : Colors.white; final dividerColor = isDark ? AppColors.darkDivider : AppColors.lightDivider; return GestureDetector( onTap: () { final uri = GoRouterState.of(context).uri.toString(); context.push('$uri/order-detail', extra: data); }, child: Container( decoration: BoxDecoration( color: cardBg, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withAlpha(isDark ? 40 : 15), blurRadius: 6, offset: const Offset(0, 1), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── 头部:币对名 + 状态 ───────────────────────── Padding( padding: const EdgeInsets.fromLTRB(14, 14, 14, 10), child: Row( children: [ Expanded( child: Row( children: [ Text( symbol.isNotEmpty ? '$symbol ${AppLocalizations.of(context)!.perpetual}' : '--', style: TextStyle( color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w700, ), ), const SizedBox(width: 2), Icon(Icons.chevron_right, color: cs.onSurface.withAlpha(100), size: 16), ], ), ), Text( statusLabel, style: TextStyle( color: statusColor, fontSize: 13, fontWeight: FontWeight.w600, ), ), ], ), ), // ── 标签行 ───────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(14, 0, 14, 12), child: Wrap( spacing: 6, runSpacing: 6, children: [ _Tag(label: dirInfo.label, tagStyle: _TagStyle.direction, color: dirColor), if (openTypeLabel.isNotEmpty) _Tag(label: openTypeLabel, tagStyle: _TagStyle.orderType), _Tag(label: positionTypeLabel, tagStyle: _TagStyle.positionType, color: positionTypeTagColor), if (leverage > 0) _Tag(label: '${leverage}x', tagStyle: _TagStyle.gray), ], ), ), Divider(height: 1, color: dividerColor), // ── 数据行 1:委托总量 | 开仓均价 ────────────────── Padding( padding: const EdgeInsets.fromLTRB(14, 10, 14, 6), child: Row( children: [ _DataField( label: '${l10n.entrustTotal}($coinLabel)', value: _rawNum(volume), align: CrossAxisAlignment.start, ), _DataField( label: '${l10n.openAvgPrice}(USDT)', value: openAvgPrice > 0 ? _rawNum(openAvgPrice) : '--', align: CrossAxisAlignment.end, ), ], ), ), // ── 数据行 2:已成交量 | 委托价格 ────────────────── Padding( padding: const EdgeInsets.fromLTRB(14, 0, 14, 12), child: Row( children: [ _DataField( label: '${l10n.filledVolume}($coinLabel)', value: _rawNum(dealVolume), align: CrossAxisAlignment.start, ), _DataField( label: '${l10n.orderPriceLabel}(USDT)', value: entrustPriceStr, align: CrossAxisAlignment.end, ), ], ), ), Divider(height: 1, color: dividerColor), // ── 时间行 ───────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(14, 10, 14, 12), child: Row( children: [ Expanded( child: _TimeBlock( label: l10n.orderTime, time: createTime, align: CrossAxisAlignment.start, ), ), _TimeBlock( label: l10n.closeTime, time: dealTime, align: CrossAxisAlignment.end, ), ], ), ), ], ), ), ); } } // ── 加载更多底部组件 ─────────────────────────────────────── class _LoadMoreFooter extends StatelessWidget { const _LoadMoreFooter({required this.isLoading, required this.hasMore}); final bool isLoading; final bool hasMore; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; if (isLoading) { return const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Center(child: CircularProgressIndicator(strokeWidth: 2)), ); } if (!hasMore) { return Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Center( child: Text(AppLocalizations.of(context)!.allLoaded, style: TextStyle( color: cs.onSurface.withAlpha(100), fontSize: 12)), ), ); } return const SizedBox(height: 16); } } // ── Tag 样式枚举 ─────────────────────────────────────────── enum _TagStyle { direction, orderType, closeType, gray, positionType } // ── 通用辅助 Widgets ────────────────────────────────────── class _Tag extends StatelessWidget { const _Tag({ required this.label, required this.tagStyle, this.color, }); final String label; final _TagStyle tagStyle; final Color? color; // 仅 direction 使用 static const _blue = AppColors.tagBlue; @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; Color bgColor; Color textColor; switch (tagStyle) { case _TagStyle.direction: final c = color ?? AppColors.rise; bgColor = c.withAlpha(30); textColor = c; case _TagStyle.orderType: bgColor = _blue.withAlpha(isDark ? 50 : 30); textColor = _blue; case _TagStyle.closeType: bgColor = isDark ? AppColors.darkBgTertiary : AppColors.darkBgMid; textColor = isDark ? AppColors.darkTextSecondary : Colors.white; case _TagStyle.gray: bgColor = isDark ? AppColors.darkBgTertiary : AppColors.lightBgTertiary; textColor = isDark ? AppColors.darkTextSecondary : AppColors.lightTextSecondary; case _TagStyle.positionType: final c = color ?? AppColors.tagBlue; bgColor = c.withAlpha(isDark ? 45 : 25); textColor = c; } return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(4), ), child: Text( label, style: TextStyle( color: textColor, fontSize: 11, fontWeight: FontWeight.w500, ), ), ); } } /// 数据字段(标签 + 值),支持左/中/右对齐 class _DataField extends StatelessWidget { const _DataField({ required this.label, required this.value, this.valueColor, this.align = CrossAxisAlignment.start, }); final String label; final String value; final Color? valueColor; final CrossAxisAlignment align; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final secondaryText = isDark ? AppColors.darkTextSecondary : AppColors.lightTextSecondary; TextAlign textAlign; if (align == CrossAxisAlignment.center) { textAlign = TextAlign.center; } else if (align == CrossAxisAlignment.end) { textAlign = TextAlign.right; } else { textAlign = TextAlign.left; } return Expanded( child: Column( crossAxisAlignment: align, children: [ Text( label, style: TextStyle(color: secondaryText, fontSize: 11), textAlign: textAlign, ), const SizedBox(height: 3), Text( value, style: TextStyle( color: valueColor ?? cs.onSurface, fontSize: 13, fontWeight: FontWeight.w600, ), textAlign: textAlign, ), ], ), ); } } /// 时间块(标签 + 时间值加粗) class _TimeBlock extends StatelessWidget { const _TimeBlock({ required this.label, required this.time, this.align = CrossAxisAlignment.start, }); final String label; final String time; final CrossAxisAlignment align; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final secondaryText = isDark ? AppColors.darkTextSecondary : AppColors.lightTextSecondary; final textAlign = align == CrossAxisAlignment.end ? TextAlign.right : TextAlign.left; return Column( crossAxisAlignment: align, children: [ Text( label, style: TextStyle(color: secondaryText, fontSize: 11), textAlign: textAlign, ), const SizedBox(height: 3), Text( time, style: TextStyle( color: cs.onSurface, fontSize: 12, fontWeight: FontWeight.w700, ), textAlign: textAlign, ), ], ); } } class _EmptyHint extends StatelessWidget { const _EmptyHint({this.message}); final String? message; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.history_outlined, color: cs.onSurface.withAlpha(80), size: 48), const SizedBox(height: 8), Text(message ?? AppLocalizations.of(context)!.noRecord, style: TextStyle( color: cs.onSurface.withAlpha(120), fontSize: 13)), ], ), ); } } // ── 工具函数 ─────────────────────────────────────────────── String _str(dynamic v) => v?.toString() ?? ''; double _toDouble(dynamic v) { if (v == null) return 0.0; if (v is num) return v.toDouble(); return double.tryParse(v.toString()) ?? 0.0; } /// 去除多余的尾零,整数不带小数点 String _rawNum(double v) { if (v == v.truncateToDouble()) return v.toInt().toString(); return v.toString(); } ({String label, bool isGreen}) _directionInfo( dynamic direction, dynamic entrustType, AppLocalizations l10n) { // direction: 0/"BUY" = 买;1/"SELL" = 卖 final dirRaw = direction?.toString() ?? ''; final isBuy = dirRaw == '0' || dirRaw.toUpperCase() == 'BUY'; // entrustType: null/0/"OPEN" = 开仓;1/"CLOSE" = 平仓 final etRaw = entrustType?.toString() ?? ''; final isClose = etRaw == '1' || etRaw.toUpperCase() == 'CLOSE'; if (!isClose) { return isBuy ? (label: l10n.longBull, isGreen: true) : (label: l10n.shortBear, isGreen: false); } else { // 平仓委托方向与仓位方向相反:BUY+CLOSE=平空(绿),SELL+CLOSE=平多(红) return isBuy ? (label: l10n.closeBull, isGreen: true) : (label: l10n.closeBear, isGreen: false); } } String _mapOrderType(dynamic raw, AppLocalizations l10n) { if (raw == null) return ''; final s = raw.toString(); switch (s) { case '0': case 'MARKET_PRICE': case 'MARKET': return l10n.marketOrderLabel; case '1': case 'LIMIT_PRICE': case 'LIMIT': return l10n.limitOrderLabel; case '2': case 'STOP': case 'PLAN': case 'SPOT_LIMIT': return l10n.planOrderLabel; case '3': return l10n.mergeOrderLabel; default: return s.isNotEmpty ? s : ''; } } String _mapCloseType(dynamic raw, AppLocalizations l10n) { if (raw == null) return ''; final s = raw.toString(); switch (s) { case '0': case 'CLOSE': return l10n.closePosition; case '1': case 'MARKET_CLOSE': return l10n.closePositionMarket; case '2': case 'ONE_KEY_CLOSE': return l10n.closeAll; case '3': case 'REVERSE_OPEN': return l10n.reverseOpen; case '4': case 'CUT': return l10n.stopProfitLoss; case '5': case 'BLAST': return l10n.liquidationLabel; case '6': case 'ADMIN_CLOSE': return l10n.adminForceClose; default: return s.isNotEmpty ? s : ''; } } String _mapPositionType( dynamic positionType, dynamic patterns, AppLocalizations l10n) { final pt = (positionType?.toString() ?? '').toUpperCase(); // 0/INTEGRAL=全仓,1/SEPARATE=分仓 if (pt == '0' || pt == 'INTEGRAL') return l10n.crossMargin; if (pt == '1' || pt == 'SEPARATE') return l10n.splitMargin; final p = (patterns?.toString() ?? '').toUpperCase(); if (p == 'CROSSED') return l10n.crossMargin; if (p == 'ISOLATED' || p == 'SEPARATE') return l10n.splitMargin; return l10n.crossMargin; } /// 根据原始 positionType / patterns 返回标签颜色 Color _positionTypeColor(dynamic positionType, dynamic patterns) { final pt = (positionType?.toString() ?? '').toUpperCase(); if (pt == '1' || pt == 'SEPARATE') return AppColors.rankPurple; // 分仓 final p = (patterns?.toString() ?? '').toUpperCase(); if (p == 'ISOLATED' || p == 'SEPARATE') return AppColors.rankPurple; // 分仓 return AppColors.tagBlue; // 全仓 } String _mapStatus(String status, AppLocalizations l10n, {bool isPosition = false}) { switch (status.toUpperCase()) { case 'ENTRUST_ING': case 'OPEN': case 'STARTED': return l10n.orderPending; case 'ENTRUST_SUCCESS': case 'FILLED': case 'DONE': // MemberContractPosition: 全部平仓 case 'PARTLYDONE': // MemberContractPosition: 部分平仓 return isPosition ? l10n.tradingSuccess : l10n.orderFilled; case 'ENTRUST_CANCEL': case 'CANCELLED': return l10n.orderCancelledLabel; case 'ENTRUST_FAILURE': case 'FAILED': return l10n.orderFailedLabel; default: return status.isNotEmpty ? status : (isPosition ? l10n.tradingSuccess : l10n.orderFilled); } } Color _statusColor(String status) { switch (status.toUpperCase()) { // 成功/进行中 → 绿色 case 'ENTRUST_SUCCESS': case 'FILLED': case 'DONE': case 'PARTLYDONE': case 'ENTRUST_ING': case 'OPEN': case 'STARTED': return AppColors.rise; // 撤销/失败 → 红色 case 'ENTRUST_CANCEL': case 'CANCELLED': case 'ENTRUST_FAILURE': case 'FAILED': return AppColors.fall; default: return AppColors.fall; } } /// 判断是否为市价单(type=0/MARKET) bool _isMarketType(dynamic raw) { if (raw == null) return false; final s = raw.toString(); return s == '0' || s == 'MARKET_PRICE' || s == 'MARKET'; } /// 是否展示"市价":普通市价,或计划委托 entrustPrice=0(计划市价) bool _showMarketPrice(dynamic type, double entrustPrice) { if (_isMarketType(type)) return true; final s = (type?.toString() ?? '').toUpperCase(); final isPlan = s == '2' || s == 'SPOT_LIMIT' || s == 'PLAN'; return isPlan && entrustPrice == 0; } String _formatTime(dynamic raw) { if (raw == null) return '--'; final ts = raw is num ? raw.toInt() : int.tryParse(raw.toString()); if (ts == null || ts == 0) return '--'; final dt = DateTime.fromMillisecondsSinceEpoch(ts); 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 se = dt.second.toString().padLeft(2, '0'); return '${dt.year}-$mo-$d $h:$mi:$se'; } // ═══════════════════════════════════════════════════════════ // 历史持仓详情页 // ═══════════════════════════════════════════════════════════ class PositionHistoryDetailScreen extends StatelessWidget { const PositionHistoryDetailScreen({super.key, required this.data}); final Map data; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; final coinObj = data['coin'] as Map?; final rawSym = _str(coinObj?['symbol'] ?? data['symbol'] ?? data['coinSymbol'] ?? ''); final coinLabel = rawSym.contains('/') ? rawSym.split('/').first : rawSym.replaceAll(RegExp(r'USDT$|BUSD$'), '').isNotEmpty ? rawSym.replaceAll(RegExp(r'USDT$|BUSD$'), '') : coinObj?['coinSymbol']?.toString() ?? 'BTC'; final posType = _mapPositionType( data['type'] ?? data['positionType'], data['patterns'], l10n); // 持仓详情无 entrustType,按仓位方向显示 final dirInfo = _directionInfo(data['direction'], null, l10n); final dirColor = dirInfo.isGreen ? AppColors.rise : AppColors.fall; final orderType = _mapOrderType(data['openType'] ?? data['type'], l10n); final leverage = (_toDouble(data['leverage'])).toInt(); final openTime = _formatTime(data['openTime'] ?? data['usdtOpenTime'] ?? data['createTime']); final closeTime = _formatTime(data['closeTime'] ?? data['dealTime']); final triggerPrice = _toDouble(data['triggerPrice']); final entrustPrice = _toDouble(data['entrustPrice']); final openAvgPrice = _toDouble(data['openPrice'] ?? data['usdtOpenPrice']); final closeAvgPrice = _toDouble(data['closePrice'] ?? data['tradedPrice']); final profitPrice = _toDouble(data['profitPrice']); final lossPrice = _toDouble(data['lossPrice']); final margin = _toDouble(data['totalPrincipalAmount'] ?? data['principalAmount']); final volume = _toDouble(data['totalPosition'] ?? data['volume'] ?? data['tradedVolume']); final profitLoss = _toDouble(data['usdtProfit'] ?? data['profitAndLoss'] ?? data['profit'] ?? 0); // 手续费 = 开仓手续费 + 平仓手续费(后端字段 openFee / closeFee) final fee = _toDouble(data['openFee']) + _toDouble(data['closeFee']) + _toDouble(data['commissionFee']); final profitRate = margin > 0 ? profitLoss / margin * 100 : 0.0; final profitColor = AppColors.changeColor(profitLoss); // 委托价格:市价/计划市价显示"市价",限价/计划限价显示原始价格 final entrustPriceStr = _showMarketPrice(data['type'], entrustPrice) ? l10n.marketOrderType : (entrustPrice > 0 ? _rawNum(entrustPrice) : '--'); final status = _str(data['status'] ?? ''); final statusLabel = _mapStatus(status, l10n, isPosition: true); return Scaffold( backgroundColor: cs.surface, appBar: AppBar( elevation: 0, centerTitle: true, backgroundColor: cs.surface, title: Text( l10n.positionDetail, style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w700), ), ), body: SingleChildScrollView( child: Column( children: [ _HistoryDetailStatus(label: statusLabel), _HistorySection(rows: [ _HistoryRow(l10n.crossIsolatedLabel, posType), _HistoryRow(l10n.direction, dirInfo.label, valueColor: dirColor), _HistoryRow(l10n.orderType, orderType), _HistoryRow(l10n.leverage, '${leverage}x'), _HistoryRow(l10n.openTime, openTime), _HistoryRow(l10n.closeTime, closeTime, isLast: true), ]), const _HistorySectionGap(), _HistorySection(rows: [ _HistoryRow('${l10n.triggerPrice}(USDT)', triggerPrice > 0 ? _rawNum(triggerPrice) : '--'), _HistoryRow('${l10n.orderPriceLabel}(USDT)', entrustPriceStr), _HistoryRow('${l10n.openAvgPrice}(USDT)', openAvgPrice > 0 ? _rawNum(openAvgPrice) : '--'), _HistoryRow('${l10n.closeAvgPrice}(USDT)', closeAvgPrice > 0 ? _rawNum(closeAvgPrice) : '--'), _HistoryRow('${l10n.takeProfitTriggerPrice}(USDT)', profitPrice > 0 ? _rawNum(profitPrice) : '--'), _HistoryRow('${l10n.stopLossTriggerPrice}(USDT)', lossPrice > 0 ? _rawNum(lossPrice) : '--'), _HistoryRow('${l10n.marginLabel}(USDT)', margin > 0 ? margin.toStringAsFixed(2) : '--', isLast: true), ]), const _HistorySectionGap(), _HistorySection(rows: [ _HistoryRow('${l10n.entrustAmount}($coinLabel)', _rawNum(volume)), _HistoryRow('${l10n.profitLabel}(USDT)', profitLoss != 0 ? profitLoss.toStringAsFixed(4) : '--', valueColor: profitLoss != 0 ? profitColor : null), _HistoryRow(l10n.profitRateLabel, profitRate != 0 ? '${profitRate.toStringAsFixed(2)}%' : '--', valueColor: profitRate != 0 ? profitColor : null), _HistoryRow('${l10n.fee}(USDT)', fee > 0 ? fee.toStringAsFixed(4) : '--', isLast: true), ]), const SizedBox(height: 24), ], ), ), ); } } // ═══════════════════════════════════════════════════════════ // 历史委托详情页 // ═══════════════════════════════════════════════════════════ class OrderHistoryDetailScreen extends StatelessWidget { const OrderHistoryDetailScreen({super.key, required this.data}); final Map data; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; final rawSym = _str(data['symbol'] ?? data['coinSymbol'] ?? ''); final coinLabel = rawSym.contains('/') ? rawSym.split('/').first : rawSym.replaceAll(RegExp(r'USDT$|BUSD$'), '').isNotEmpty ? rawSym.replaceAll(RegExp(r'USDT$|BUSD$'), '') : 'BTC'; final posType = _mapPositionType( data['positionType'], data['patterns'], l10n); final dirInfo = _directionInfo(data['direction'], data['entrustType'], l10n); final dirColor = dirInfo.isGreen ? AppColors.rise : AppColors.fall; final orderType = _mapOrderType(data['type'], l10n); final leverage = (_toDouble(data['leverage'])).toInt(); final openTime = _formatTime(data['createTime'] ?? data['usdtOpenTime']); final closeTime = _formatTime(data['dealTime']); final triggerPrice = _toDouble(data['triggerPrice']); final entrustPrice = _toDouble(data['entrustPrice']); // 判断是否为平仓单(entrustType=1/CLOSE) final entrustTypeRaw = (data['entrustType']?.toString() ?? '').toUpperCase(); final isCloseOrder = entrustTypeRaw == '1' || entrustTypeRaw == 'CLOSE'; // 开仓单:tradedPrice = 开仓成交均价,无平仓均价 // 平仓单:usdtOpenPrice/openPrice = 原仓位开仓均价,tradedPrice = 平仓成交均价 final openAvgPrice = isCloseOrder ? _toDouble(data['usdtOpenPrice'] ?? data['openPrice'] ?? 0) : _toDouble(data['tradedPrice']); final closeAvgPrice = isCloseOrder ? _toDouble(data['tradedPrice']) : 0.0; final profitPrice = _toDouble(data['profitPrice'] ?? data['stopProfitPrice']); final lossPrice = _toDouble(data['lossPrice'] ?? data['stopLossPrice']); final margin = _toDouble(data['principalAmount']); final volume = _toDouble(data['volume'] ?? data['size']); // 手续费 = 开仓手续费 + 平仓手续费(后端字段 openFee / closeFee) final fee = _toDouble(data['openFee']) + _toDouble(data['closeFee']); // 委托价格:市价/计划市价显示"市价",限价/计划限价显示原始价格 final entrustPriceStr = _showMarketPrice(data['type'], entrustPrice) ? l10n.marketOrderType : (entrustPrice > 0 ? _rawNum(entrustPrice) : '--'); final status = _str(data['status'] ?? ''); final statusLabel = _mapStatus(status, l10n); return Scaffold( backgroundColor: cs.surface, appBar: AppBar( elevation: 0, centerTitle: true, backgroundColor: cs.surface, title: Text( l10n.orderDetail, style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w700), ), ), body: SingleChildScrollView( child: Column( children: [ _HistoryDetailStatus(label: statusLabel), _HistorySection(rows: [ _HistoryRow(l10n.crossIsolatedLabel, posType), _HistoryRow(l10n.direction, dirInfo.label, valueColor: dirColor), _HistoryRow(l10n.orderType, orderType), _HistoryRow(l10n.leverage, '${leverage}x'), _HistoryRow(l10n.openTime, openTime), _HistoryRow(l10n.closeTime, closeTime, isLast: true), ]), const _HistorySectionGap(), _HistorySection(rows: [ _HistoryRow('${l10n.triggerPrice}(USDT)', triggerPrice > 0 ? _rawNum(triggerPrice) : '--'), _HistoryRow('${l10n.orderPriceLabel}(USDT)', entrustPriceStr), _HistoryRow('${l10n.openAvgPrice}(USDT)', openAvgPrice > 0 ? _rawNum(openAvgPrice) : '--'), _HistoryRow('${l10n.closeAvgPrice}(USDT)', closeAvgPrice > 0 ? _rawNum(closeAvgPrice) : '--'), _HistoryRow('${l10n.takeProfitTriggerPrice}(USDT)', profitPrice > 0 ? _rawNum(profitPrice) : '--'), _HistoryRow('${l10n.stopLossTriggerPrice}(USDT)', lossPrice > 0 ? _rawNum(lossPrice) : '--'), _HistoryRow('${l10n.marginLabel}(USDT)', margin > 0 ? margin.toStringAsFixed(2) : '--'), _HistoryRow('${l10n.entrustAmount}($coinLabel)', _rawNum(volume)), _HistoryRow('${l10n.fee}(USDT)', fee > 0 ? fee.toStringAsFixed(4) : '--', isLast: true), ]), const SizedBox(height: 24), ], ), ), ); } } // ─── 共用辅助 widgets ────────────────────────────────────── class _HistoryDetailStatus extends StatelessWidget { const _HistoryDetailStatus({required this.label}); final String label; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.fromLTRB(0, 32, 0, 24), child: Column( children: [ Stack( clipBehavior: Clip.none, children: [ Container( width: 56, height: 56, decoration: BoxDecoration( color: const Color(0xFF3B82F6).withAlpha(25), borderRadius: BorderRadius.circular(14), ), child: const Icon(Icons.receipt_long, color: Color(0xFF3B82F6), size: 28), ), Positioned( right: -4, bottom: -4, child: Container( width: 20, height: 20, decoration: const BoxDecoration( color: AppColors.rise, shape: BoxShape.circle, ), child: const Icon(Icons.check, color: Colors.white, size: 13), ), ), ], ), const SizedBox(height: 14), Text( label, style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w600), ), ], ), ); } } class _HistoryRow { const _HistoryRow(this.label, this.value, {this.valueColor, this.isLast = false}); final String label; final String value; final Color? valueColor; final bool isLast; } class _HistorySection extends StatelessWidget { const _HistorySection({required this.rows}); final List<_HistoryRow> rows; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Column( children: rows.map((r) { return Column( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(r.label, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 13)), Text(r.value, style: TextStyle( color: r.valueColor ?? cs.onSurface, fontSize: 13, fontWeight: FontWeight.w500)), ], ), ), if (!r.isLast) Divider( height: 1, thickness: 0.5, indent: 16, endIndent: 16, color: cs.outline.withAlpha(60)), ], ); }).toList(), ); } } class _HistorySectionGap extends StatelessWidget { const _HistorySectionGap(); @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return Container(height: 8, color: isDark ? AppColors.darkBg : AppColors.lightBgSecondary); } } // ── 骨架屏 ──────────────────────────────────────────────────── /// 历史列表骨架:模拟 3 张卡片占位 class _HistoryListShimmer extends StatelessWidget { const _HistoryListShimmer({required this.isPosition}); final bool isPosition; // 持仓卡片 vs 委托卡片(行数略有差异) @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final cardBg = isDark ? AppColors.darkBgSecondary : AppColors.lightBg; Widget card() { return Container( decoration: BoxDecoration( color: cardBg, borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题行:币对名 + 状态 Row( children: [ shimmerBox(90, 15), const Spacer(), shimmerBox(52, 13), ], ), const SizedBox(height: 10), // 标签行 Row(children: [ shimmerBox(52, 20, radius: 4), const SizedBox(width: 6), shimmerBox(44, 20, radius: 4), const SizedBox(width: 6), shimmerBox(32, 20, radius: 4), const SizedBox(width: 6), shimmerBox(28, 20, radius: 4), ]), const SizedBox(height: 12), // 分割线 shimmerBox(double.infinity, 0.5), const SizedBox(height: 10), // 数据行 1 Row(children: [ _ShimmerField(), _ShimmerField(), if (isPosition) _ShimmerField(), ]), const SizedBox(height: 8), // 数据行 2 Row(children: [ _ShimmerField(), _ShimmerField(), if (isPosition) _ShimmerField(), ]), const SizedBox(height: 10), shimmerBox(double.infinity, 0.5), const SizedBox(height: 10), // 时间行 Row( children: [ Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ shimmerBox(44, 10), const SizedBox(height: 4), shimmerBox(110, 12), ]), const Spacer(), Column(crossAxisAlignment: CrossAxisAlignment.end, children: [ shimmerBox(44, 10), const SizedBox(height: 4), shimmerBox(110, 12), ]), ], ), ], ), ); } return AppShimmer( child: ListView.separated( physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB(12, 12, 12, 24), itemCount: 4, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (_, __) => card(), ), ); } } /// 数据字段占位(标签 + 值,竖向排列) class _ShimmerField extends StatelessWidget { @override Widget build(BuildContext context) { return Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ shimmerBox(40, 10), const SizedBox(height: 5), shimmerBox(60, 13), ], ), ); } }