import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; import '../../core/l10n/app_localizations.dart'; import '../../core/theme/app_colors.dart'; import '../../core/utils/top_toast.dart'; import '../../data/models/copy_trading/copy_position.dart'; /// Shared position card component for displaying copy trading positions class CopyPositionCard extends StatelessWidget { const CopyPositionCard({ super.key, required this.position, required this.isHistory, this.onClose, }); final CopyPosition position; final bool isHistory; final void Function(String positionId)? onClose; static const _avatarColors = [ Color(0xFFf7931a), Color(0xFF627eea), Color(0xFF9945ff), Color(0xFFf3ba2f), Color(0xFF2775ca), ]; Color get _avatarBg => _avatarColors[position.traderName.codeUnitAt(0) % _avatarColors.length]; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; final isDark = Theme.of(context).brightness == Brightness.dark; final isLong = position.isLong; final directionColor = isLong ? AppColors.rise : AppColors.fall; final directionLabel = isLong ? l10n.longBull : l10n.shortBear; final pnlColor = position.unrealizedPnl >= 0 ? AppColors.rise : AppColors.fall; final pnlSign = position.unrealizedPnl >= 0 ? '+' : ''; final roiSign = position.roi >= 0 ? '+' : ''; return Container( margin: const EdgeInsets.fromLTRB(16, 8, 16, 0), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isDark ? AppColors.darkBgSecondary : AppColors.lightBg, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow(color: Colors.black.withAlpha(18), blurRadius: 12, offset: const Offset(0, 2)), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (isHistory) // 历史跟单:品种在左,交易员头像+名称在右 Row( children: [ Expanded( child: Text( position.symbol, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w700), ), ), TraderAvatar( name: position.traderName, avatarUrl: position.traderAvatar, bgColor: _avatarBg, size: 32, ), const SizedBox(width: 8), Flexible( child: Text(position.traderName, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: cs.onSurface, fontSize: 14, fontWeight: FontWeight.w600)), ), ], ) else // 当前跟单:交易员头像+名称在左,平仓按钮在右 Row( children: [ TraderAvatar( name: position.traderName, avatarUrl: position.traderAvatar, bgColor: _avatarBg, size: 38, ), const SizedBox(width: 10), Expanded( child: Text( position.traderName, style: TextStyle(color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w600), ), ), ElevatedButton( onPressed: () => onClose?.call(position.id), style: ElevatedButton.styleFrom( backgroundColor: AppColors.brand, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, elevation: 0, ), child: Text(l10n.closePositionBtn, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)), ), ], ), const SizedBox(height: 10), // 品种行(当前跟单时显示) if (!isHistory) Wrap( spacing: 6, runSpacing: 6, crossAxisAlignment: WrapCrossAlignment.center, children: [ Text(position.symbol, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: cs.onSurface, fontSize: 14, fontWeight: FontWeight.w700)), Badge(text: directionLabel, bgColor: directionColor.withValues(alpha: 0.12), textColor: directionColor), Badge(text: l10n.perpetual, bgColor: cs.onSurface.withAlpha(20), textColor: cs.onSurface.withAlpha(153), borderColor: cs.onSurface.withAlpha(60)), Badge(text: position.positionType == '逐仓' ? l10n.isolatedMargin : l10n.crossMargin, bgColor: cs.onSurface.withAlpha(20), textColor: cs.onSurface.withAlpha(153), borderColor: cs.onSurface.withAlpha(60)), Badge(text: '${position.leverage}x', bgColor: cs.onSurface.withAlpha(20), textColor: cs.onSurface.withAlpha(180), borderColor: cs.onSurface.withAlpha(60)), ], ) else // 历史跟单:direction + 永续 + positionType + leverage Wrap( spacing: 6, runSpacing: 4, children: [ Badge(text: directionLabel, bgColor: directionColor.withValues(alpha: 0.12), textColor: directionColor), Badge(text: l10n.perpetual, bgColor: cs.onSurface.withAlpha(20), textColor: cs.onSurface.withAlpha(153), borderColor: cs.onSurface.withAlpha(60)), Badge(text: position.positionType == '逐仓' ? l10n.isolatedMargin : l10n.crossMargin, bgColor: cs.onSurface.withAlpha(20), textColor: cs.onSurface.withAlpha(153), borderColor: cs.onSurface.withAlpha(60)), Badge(text: '${position.leverage}x', bgColor: cs.onSurface.withAlpha(20), textColor: cs.onSurface.withAlpha(180), borderColor: cs.onSurface.withAlpha(60)), ], ), const SizedBox(height: 10), if (!isHistory) ...[ // 当前跟单数据行 Row( children: [ DataCell(label: '${l10n.openAvgPrice}(USDT)', value: position.openPrice.toStringAsFixed(1)), DataCell(label: '${l10n.currentPrice}(USDT)', value: position.currentPrice.toStringAsFixed(1)), DataCell(label: '${l10n.currentMarginLabel}(USDT)', value: position.margin.toStringAsFixed(2)), ], ), const SizedBox(height: 8), Row( children: [ DataCell(label: '${l10n.quantityLabel}(${_baseAsset(position.symbol)})', value: position.quantity.toStringAsFixed(4)), DataCell(label: l10n.profitRateLabel, value: '$roiSign${position.roi.toStringAsFixed(2)}%', valueColor: pnlColor), DataCell(label: l10n.profitUsdtLabel, value: '$pnlSign${position.unrealizedPnl.toStringAsFixed(3)}', valueColor: pnlColor), ], ), ] else ...[ // 历史跟单数据行:数量 / 收益 / 收益率(三列,最后列右对齐) Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('${l10n.quantityLabel}(${_baseAsset(position.symbol)})', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 11)), const SizedBox(height: 4), Text(position.quantity.toStringAsFixed(4), style: TextStyle(color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w700)), ], ), ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(l10n.profitUsdtLabel, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 11)), const SizedBox(height: 4), Text('$pnlSign${position.unrealizedPnl.toStringAsFixed(4)}', style: TextStyle(color: pnlColor, fontSize: 15, fontWeight: FontWeight.w700)), ], ), ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(l10n.profitRateLabel, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 11)), const SizedBox(height: 4), Text('$roiSign${position.roi.toStringAsFixed(2)}%', style: TextStyle(color: pnlColor, fontSize: 15, fontWeight: FontWeight.w700)), ], ), ), ], ), const SizedBox(height: 16), Divider(height: 1, thickness: 1, color: cs.outlineVariant.withAlpha(60)), const SizedBox(height: 12), // 底部:开仓均价(左) / 平仓均价(右对齐) Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('${l10n.openAvgPrice}(USDT)', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 11)), const SizedBox(height: 3), Text(position.openPrice.toStringAsFixed(1), style: TextStyle(color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w700)), const SizedBox(height: 2), Text(_formatDate(position.openTime), style: TextStyle(color: cs.onSurface.withAlpha(120), fontSize: 11)), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text('${l10n.closeAvgPrice}(USDT)', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 11)), const SizedBox(height: 3), Text( position.closePrice != null ? position.closePrice!.toStringAsFixed(1) : '--', style: TextStyle(color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w700), ), const SizedBox(height: 2), Text( position.closeTime != null ? _formatDate(position.closeTime!) : '--', style: TextStyle(color: cs.onSurface.withAlpha(120), fontSize: 11), ), ], ), ], ), ], if (!isHistory) ...[ const SizedBox(height: 10), Row( children: [ Text( '${l10n.openTime} ${_formatDate(position.openTime)}', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 11), ), const Spacer(), Text('${l10n.positionId} ${position.id}', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 11)), const SizedBox(width: 4), GestureDetector( onTap: () { Clipboard.setData(ClipboardData(text: position.id)); showTopToast(context, message: l10n.copyIdSuccess, backgroundColor: AppColors.rise); }, child: Icon(Icons.copy_outlined, size: 14, color: cs.onSurface.withAlpha(153)), ), ], ), ], ], ), ); } String _baseAsset(String symbol) { final s = symbol.replaceAll(' 永续', ''); final parts = s.split('/'); return parts.isNotEmpty ? parts[0] : s; } String _formatDate(DateTime dt) => '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} ' '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}'; } /// Trader avatar display class TraderAvatar extends StatelessWidget { const TraderAvatar({ super.key, required this.name, required this.bgColor, required this.size, this.avatarUrl, }); final String name; final String? avatarUrl; final Color bgColor; final double size; @override Widget build(BuildContext context) { final letter = name.isNotEmpty ? name[0].toUpperCase() : '?'; if (avatarUrl != null && avatarUrl!.isNotEmpty) { return ClipOval( child: Image.network( avatarUrl!, width: size, height: size, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _fallback(letter), ), ); } return _fallback(letter); } Widget _fallback(String letter) => Container( width: size, height: size, decoration: BoxDecoration(color: bgColor, shape: BoxShape.circle), child: Center( child: Text(letter, style: TextStyle( color: Colors.white, fontSize: size * 0.42, fontWeight: FontWeight.w700)), ), ); } /// Badge component for direction, leverage, position type class Badge extends StatelessWidget { const Badge({ super.key, required this.text, required this.bgColor, required this.textColor, this.borderColor, }); final String text; final Color bgColor; final Color textColor; final Color? borderColor; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(4), border: borderColor != null ? Border.all(color: borderColor!, width: 0.8) : null, ), child: Text(text, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: textColor, fontSize: 11, fontWeight: FontWeight.w500)), ); } } /// Data label-value cell class DataCell extends StatelessWidget { const DataCell({ super.key, required this.label, required this.value, this.valueColor, }); final String label; final String value; final Color? valueColor; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 11)), const SizedBox(height: 2), Text(value, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: valueColor ?? cs.onSurface, fontSize: 13, fontWeight: FontWeight.w500)), ], ), ); } }