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/theme/app_colors.dart'; import '../../../core/utils/number_format.dart'; import '../../../providers/asset_provider.dart'; import '../../../providers/futures_provider.dart' show FuturesPosition, OrderSide; import '../../../providers/market_provider.dart' show marketProvider; import '../../widgets/common/app_refresh_indicator.dart'; import '../futures/futures_screen.dart' show SharePositionSheet; /// marginMode → l10n 标签 String _marginModeLabel(String mode, AppLocalizations l10n) { switch (mode) { case '分仓': return l10n.splitMargin; default: return l10n.crossMargin; } } /// marginMode → 标签颜色 Color _marginModeColor(String mode) { switch (mode) { case '分仓': case '逐仓': return AppColors.rankPurple; default: return AppColors.tagBlue; } } /// 合约 Tab class AssetFuturesTab extends ConsumerWidget { const AssetFuturesTab({super.key, required this.state, required this.notifier}); final AssetState state; final AssetNotifier notifier; @override Widget build(BuildContext context, WidgetRef ref) { final cs = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; final obscure = state.obscureBalance; final swapBalance = state.walletBalanceExcludeEG('SWAP').toDouble(); final display = obscure ? '******' : formatPrice(swapBalance, decimalPlaces: 2); return AppRefreshIndicator( onRefresh: notifier.silentRefresh, child: ListView( children: [ // 合约账户余额 Padding( padding: const EdgeInsets.fromLTRB(16, 20, 16, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text(l10n.futuresAccount, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13)), const SizedBox(width: 6), GestureDetector( onTap: notifier.toggleObscure, child: Icon( obscure ? Icons.visibility_off_outlined : Icons.visibility_outlined, size: 16, color: cs.onSurface.withAlpha(153), ), ), ], ), const SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(display, style: TextStyle(color: cs.onSurface, fontSize: 32, fontWeight: FontWeight.w700, letterSpacing: -0.5)), const SizedBox(width: 6), Padding( padding: const EdgeInsets.only(bottom: 5), child: Text('USDT', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 14)), ), ], ), const SizedBox(height: 12), // 钱包余额 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(l10n.walletBalance, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13)), Text( obscure ? '******' : formatPrice(state.accountBalance('SWAP').toDouble(), decimalPlaces: 2), style: TextStyle(color: cs.onSurface, fontSize: 13, fontWeight: FontWeight.w500), ), ], ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(l10n.unrealizedPnl, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13)), Text( obscure ? '******' : formatPrice(state.unrealizedPnl('SWAP').toDouble(), decimalPlaces: 2), style: TextStyle( color: _getPnlColor(context, state.unrealizedPnl('SWAP').toDouble()), fontSize: 13, fontWeight: FontWeight.w500, ), ), ], ), ], ), ), // 交易 + 划转 按钮 Padding( padding: const EdgeInsets.fromLTRB(16, 20, 16, 0), child: Row( children: [ Expanded( child: GestureDetector( onTap: () { final symbols = ref.read(marketProvider).displaySymbols; final symbol = symbols.isNotEmpty ? symbols.first : 'BTCUSDT'; context.go('/futures/$symbol'); }, child: Container( height: 44, decoration: BoxDecoration( color: AppColors.brand, borderRadius: BorderRadius.circular(22), ), child: Center( child: Text(l10n.futures, style: const TextStyle(color: Colors.black, fontSize: 15, fontWeight: FontWeight.w600)), ), ), ), ), const SizedBox(width: 12), Expanded( child: GestureDetector( onTap: () => context.push('/asset/transfer?from=SWAP&to=SPOT'), child: Container( height: 44, decoration: BoxDecoration( color: AppColors.brand, borderRadius: BorderRadius.circular(22), ), child: Center( child: Text(l10n.transfer, style: const TextStyle(color: Colors.black, fontSize: 15, fontWeight: FontWeight.w600)), ), ), ), ), ], ), ), const SizedBox(height: 24), // 持仓标题 Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: Text(l10n.positions, style: TextStyle(color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w600)), ), if (state.positions.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 40), child: Center( child: Text(l10n.noPositions, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13)), ), ) else for (final pos in state.positions) _AssetPositionCard(position: pos), const SizedBox(height: 32), ], ), ); } Color _getPnlColor(BuildContext context, double pnl) { if (pnl > 0) return AppColors.rise; if (pnl < 0) return AppColors.fall; return Theme.of(context).colorScheme.onSurface; } } /// 资产合约 tab 的持仓卡片,样式与合约页面 _PositionCard 一致 class _AssetPositionCard extends StatelessWidget { const _AssetPositionCard({required this.position}); final FuturesPosition position; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; final coinName = position.symbol.replaceAll('/', '').replaceAll('USDT', ''); final isLong = position.side == OrderSide.long; final pnlColor = position.unrealizedPnl >= 0 ? AppColors.rise : AppColors.fall; final sideColor = isLong ? AppColors.rise : AppColors.fall; final symbol = '${coinName}USDT'; return GestureDetector( onTap: () => context.go('/futures/$symbol'), child: Container( decoration: BoxDecoration( border: Border(bottom: BorderSide(color: cs.outline.withAlpha(50))), ), padding: const EdgeInsets.fromLTRB(12, 10, 12, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── 标题行 ────────────────────────────────────────── Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), decoration: BoxDecoration( color: sideColor, borderRadius: BorderRadius.circular(3), ), child: Text( isLong ? l10n.openLong : l10n.openShort, style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w700), ), ), const SizedBox(width: 4), Expanded( child: Row( children: [ Flexible( child: Text( '${coinName}USDT', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: cs.onSurface, fontSize: 13, fontWeight: FontWeight.w700), ), ), const SizedBox(width: 4), _SmallTag(text: l10n.perpetual), const SizedBox(width: 3), _SmallTag( text: _marginModeLabel(position.marginMode, l10n), color: _marginModeColor(position.marginMode), ), const SizedBox(width: 3), Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), decoration: BoxDecoration( color: AppColors.leverageGoldBg, borderRadius: BorderRadius.circular(3), ), child: Text( '${position.leverage.toInt()}X', style: const TextStyle(color: AppColors.leverageGold, fontSize: 10, fontWeight: FontWeight.w700), ), ), ], ), ), const SizedBox(width: 8), GestureDetector( onTap: () => _sharePosition(context), child: Icon(Icons.share_outlined, size: 16, color: cs.onSurface.withAlpha(153)), ), ], ), const SizedBox(height: 8), // ── 未实现盈亏 + 收益率 ────────────────────────────── Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('${l10n.unrealizedPnl} (USDT)', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 9)), Text( formatAmount(position.unrealizedPnl), style: TextStyle(color: pnlColor, fontSize: 16, fontWeight: FontWeight.w700), ), ], ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(l10n.returnRate, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 9)), Text( '${formatAmount(position.roe)}%', style: TextStyle(color: pnlColor, fontSize: 13, fontWeight: FontWeight.w600), ), ], ), ], ), const SizedBox(height: 6), // ── 数据行 1 ──────────────────────────────────────── Row( children: [ _DataCol( label: '${l10n.positionSize}($coinName)', value: formatQuantity(position.size), align: CrossAxisAlignment.start, ), _DataCol( label: '${l10n.marginLabel}(USDT)', value: formatAmount(position.margin), align: CrossAxisAlignment.center, ), _DataCol( label: l10n.marginRatioLabel, value: '${formatAmount(position.marginRatio)}%', align: CrossAxisAlignment.end, ), ], ), const SizedBox(height: 4), // ── 数据行 2 ──────────────────────────────────────── Row( children: [ _DataCol( label: '${l10n.openAvgPrice}(USDT)', value: formatPrice(position.entryPrice), align: CrossAxisAlignment.start, ), _DataCol( label: '${l10n.latestLabel}(USDT)', value: formatPrice(position.markPrice), align: CrossAxisAlignment.center, ), _DataCol( label: '${l10n.liqPrice}(USDT)', value: position.liquidationPrice > 0 ? formatPrice(position.liquidationPrice) : '--', valueColor: position.liquidationPrice > 0 ? AppColors.fall : cs.onSurface.withAlpha(153), align: CrossAxisAlignment.end, ), ], ), ], ), ), ); } void _sharePosition(BuildContext context) { showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (_) => SharePositionSheet(position: position), ); } } class _SmallTag extends StatelessWidget { const _SmallTag({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: 4, vertical: 1), decoration: BoxDecoration( color: c.withAlpha(isDark ? 45 : 25), borderRadius: BorderRadius.circular(3), ), child: Text(text, style: TextStyle(color: c, fontSize: 9, fontWeight: FontWeight.w500)), ); } return Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration( borderRadius: BorderRadius.circular(3), border: Border.all(color: cs.outline.withAlpha(100)), ), child: Text(text, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 9)), ); } } class _DataCol extends StatelessWidget { const _DataCol({ 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 textAlign = align == CrossAxisAlignment.end ? TextAlign.right : align == CrossAxisAlignment.center ? TextAlign.center : TextAlign.left; return Expanded( child: Column( crossAxisAlignment: align, children: [ Text(label, maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: textAlign, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 9)), Text(value, maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: textAlign, style: TextStyle( color: valueColor ?? cs.onSurface, fontSize: 12, fontWeight: FontWeight.w500)), ], ), ); } }