import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gal/gal.dart'; import 'package:go_router/go_router.dart'; import 'package:path_provider/path_provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:share_plus/share_plus.dart'; import '../../../core/l10n/app_localizations.dart'; import '../../../core/network/dio_client.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/utils/dialog_utils.dart' show showConfirmDialog, showTipDialog, extractErrorMessage; import '../../../core/utils/number_format.dart'; import '../../../core/utils/top_toast.dart'; import '../../widgets/common/app_refresh_indicator.dart'; import '../../widgets/common/app_shimmer.dart'; import '../../widgets/common/app_tab_bar.dart'; import '../../../data/models/copy_trading/copy_position.dart'; import '../../../data/models/copy_trading/trader.dart'; import '../../../data/repositories/copy_trading_repository.dart'; import '../../../data/services/auth_service.dart'; import '../../../providers/my_copy_trading_provider.dart'; class MyCopyTradingScreen extends ConsumerStatefulWidget { const MyCopyTradingScreen({super.key}); @override ConsumerState createState() => _MyCopyTradingScreenState(); } class _MyCopyTradingScreenState extends ConsumerState with SingleTickerProviderStateMixin { late TabController _tabController; late PageController _pageController; final _currentScrollCtrl = ScrollController(); final _tradersScrollCtrl = ScrollController(); final _historyScrollCtrl = ScrollController(); @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); _pageController = PageController(); // 每次进入页面都重置 tab 并刷新数据 WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(myCopyTradingProvider.notifier).setTab(0); ref.read(myCopyTradingProvider.notifier).refresh(); }); _tabController.addListener(() { if (!mounted) return; if (_tabController.indexIsChanging) { _pageController.animateToPage( _tabController.index, duration: const Duration(milliseconds: 280), curve: Curves.easeOut, ); } else { ref.read(myCopyTradingProvider.notifier).setTab(_tabController.index); } }); _pageController.addListener(() { if (!mounted) return; if (!_pageController.hasClients) return; final page = _pageController.page!; final offset = page - _tabController.index; if (offset.abs() <= 1.0 && !_tabController.indexIsChanging) { _tabController.offset = offset.clamp(-1.0, 1.0); } }); _currentScrollCtrl.addListener(() { if (!mounted) return; if (_currentScrollCtrl.position.pixels >= _currentScrollCtrl.position.maxScrollExtent - 200) { ref.read(myCopyTradingProvider.notifier).loadMoreCurrent(); } }); _tradersScrollCtrl.addListener(() { if (!mounted) return; if (_tradersScrollCtrl.position.pixels >= _tradersScrollCtrl.position.maxScrollExtent - 200) { ref.read(myCopyTradingProvider.notifier).loadMoreTraders(); } }); _historyScrollCtrl.addListener(() { if (!mounted) return; if (_historyScrollCtrl.position.pixels >= _historyScrollCtrl.position.maxScrollExtent - 200) { ref.read(myCopyTradingProvider.notifier).loadMoreHistory(); } }); } @override void dispose() { _tabController.dispose(); _pageController.dispose(); _currentScrollCtrl.dispose(); _tradersScrollCtrl.dispose(); _historyScrollCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final cardBg = isDark ? AppColors.darkBgSecondary : AppColors.lightBg; final state = ref.watch(myCopyTradingProvider); final currentCount = state.currentPositions.length; final traderCount = state.myTraders.length; return Scaffold( backgroundColor: cardBg, appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back_ios, size: 18), onPressed: () => context.pop(), ), title: Text(AppLocalizations.of(context)!.myCopyTrading, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600)), ), body: state.isLoading ? _MyCopyTradingFullSkeleton(cardBg: cardBg) : Column( children: [ // 统计卡片(带眼睛隐藏)—— 铺满白色背景 if (state.account != null) _StatsCard( account: state.account!, onTransfer: () async { await context.push('/asset/transfer?from=SPOT&to=FOLLOW'); if (context.mounted) { ref.read(myCopyTradingProvider.notifier).refresh(); } }, ), // Tab Container( decoration: BoxDecoration( border: Border( bottom: BorderSide(color: cs.outlineVariant.withAlpha(60), width: 1), ), ), child: TabBar( controller: _tabController, indicator: StretchTabIndicator( controller: _tabController, color: AppColors.brand, ), indicatorSize: TabBarIndicatorSize.tab, dividerColor: Colors.transparent, labelColor: AppColors.brand, unselectedLabelColor: cs.onSurface.withAlpha(153), labelStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), unselectedLabelStyle: const TextStyle(fontSize: 14), tabs: [ Tab(text: currentCount > 0 ? '${AppLocalizations.of(context)!.currentFollowOrders}($currentCount)' : AppLocalizations.of(context)!.currentFollowOrders), Tab(text: traderCount > 0 ? '${AppLocalizations.of(context)!.myTraders}($traderCount)' : AppLocalizations.of(context)!.myTraders), Tab(text: AppLocalizations.of(context)!.historyFollowOrders), ], ), ), // 内容区 Expanded( child: PageView( controller: _pageController, physics: const BouncingScrollPhysics( parent: AlwaysScrollableScrollPhysics(), ), onPageChanged: (index) { if (_tabController.indexIsChanging) return; _tabController.index = index; }, children: [ _buildCurrentTab(state), _buildTradersTab(state), _buildHistoryTab(state), ], ), ), ], ), ); } Widget _buildCurrentTab(MyCopyTradingState state) { final onRefresh = () => ref.read(myCopyTradingProvider.notifier).silentRefresh(); if (state.isLoading && state.currentPositions.isEmpty) { return ListView.builder( padding: const EdgeInsets.fromLTRB(0, 8, 0, 16), itemCount: 3, itemBuilder: (_, __) => const _PositionCardSkeleton(), ); } return AppRefreshIndicator( onRefresh: onRefresh, child: state.currentPositions.isEmpty ? ListView( physics: const AlwaysScrollableScrollPhysics(), children: [_EmptyState(onGoMarket: () => context.pop())], ) : ListView.builder( controller: _currentScrollCtrl, physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB(0, 8, 0, 16), itemCount: state.currentPositions.length + 1, itemBuilder: (_, i) { if (i >= state.currentPositions.length) { return _loadMoreFooter(state.currentLoadingMore, state.currentHasMore); } return _PositionCard( position: state.currentPositions[i], isHistory: false, onClose: (id) async { final confirmed = await showConfirmDialog( context, content: AppLocalizations.of(context)!.closePositionConfirmMsg, ); if (!confirmed || !context.mounted) return; try { await ref.read(myCopyTradingProvider.notifier).closePosition(id); if (context.mounted) { showTopToast(context, message: AppLocalizations.of(context)!.closePositionSuccess, backgroundColor: AppColors.rise); } } catch (e) { if (context.mounted) { showTopToast(context, message: extractErrorMessage(e), backgroundColor: AppColors.fall); } } }, ); }, ), ); } Widget _buildTradersTab(MyCopyTradingState state) { final onRefresh = () => ref.read(myCopyTradingProvider.notifier).silentRefresh(); if (state.isLoading && state.myTraders.isEmpty) { return ListView.builder( padding: const EdgeInsets.fromLTRB(0, 8, 0, 16), itemCount: 3, itemBuilder: (_, __) => const _MyTraderCardSkeleton(), ); } return AppRefreshIndicator( onRefresh: onRefresh, child: state.myTraders.isEmpty ? ListView( physics: const AlwaysScrollableScrollPhysics(), children: [_EmptyState(onGoMarket: () => context.pop())], ) : ListView.builder( controller: _tradersScrollCtrl, physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB(0, 8, 0, 16), itemCount: state.myTraders.length + 1, itemBuilder: (_, i) { if (i >= state.myTraders.length) { return _loadMoreFooter(state.tradersLoadingMore, state.tradersHasMore); } return _MyTraderCard( trader: state.myTraders[i], onUnfollow: () async { final confirmed = await showConfirmDialog( context, content: AppLocalizations.of(context)!.unfollowTraderConfirm, ); if (!confirmed || !context.mounted) return; try { await ref.read(myCopyTradingProvider.notifier).unfollowTrader(state.myTraders[i].id); } catch (e) { if (context.mounted) showTipDialog(context, content: extractErrorMessage(e)); } }, ); }, ), ); } Widget _buildHistoryTab(MyCopyTradingState state) { final onRefresh = () => ref.read(myCopyTradingProvider.notifier).silentRefresh(); if (state.isLoading && state.historyPositions.isEmpty) { return ListView.builder( padding: const EdgeInsets.fromLTRB(0, 8, 0, 16), itemCount: 3, itemBuilder: (_, __) => const _PositionCardSkeleton(), ); } return AppRefreshIndicator( onRefresh: onRefresh, child: state.historyPositions.isEmpty ? ListView( physics: const AlwaysScrollableScrollPhysics(), children: [_EmptyState(onGoMarket: () => context.pop())], ) : ListView.builder( controller: _historyScrollCtrl, physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB(0, 8, 0, 16), itemCount: state.historyPositions.length + 1, itemBuilder: (_, i) { if (i >= state.historyPositions.length) { return _loadMoreFooter(state.historyLoadingMore, state.historyHasMore); } return _PositionCard( position: state.historyPositions[i], isHistory: true, ); }, ), ); } Widget _loadMoreFooter(bool loading, bool hasMore) { if (loading) { return const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Center(child: CircularProgressIndicator(color: AppColors.brand, strokeWidth: 2)), ); } if (!hasMore) { return Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Center( child: Text(AppLocalizations.of(context)!.noMore, style: TextStyle(color: Theme.of(context).colorScheme.onSurface.withAlpha(100), fontSize: 12)), ), ); } return const SizedBox(height: 8); } } // ── 统计卡片(眼睛隐藏) ─────────────────────────────────── class _StatsCard extends StatefulWidget { const _StatsCard({required this.account, this.onTransfer}); final dynamic account; final VoidCallback? onTransfer; @override State<_StatsCard> createState() => _StatsCardState(); } class _StatsCardState extends State<_StatsCard> { bool _visible = true; /// 截断到2位小数,不四舍五入(等同 Java RoundingMode.DOWN) String _fmtDown(double v) { final isNeg = v < 0; final abs = v.abs(); final str = abs.toStringAsFixed(10); final dotIdx = str.indexOf('.'); final intPart = dotIdx < 0 ? str : str.substring(0, dotIdx); final fracPart = dotIdx < 0 ? '00' : (str.substring(dotIdx + 1) + '00').substring(0, 2); return '${isNeg ? '-' : ''}$intPart.$fracPart'; } @override Widget build(BuildContext context) { final acc = widget.account; final isDark = Theme.of(context).brightness == Brightness.dark; final cs = Theme.of(context).colorScheme; final pnlValue = acc.unrealizedPnl as double; // 黄色背景上用深绿色以保证对比度 final pnlColor = pnlValue >= 0 ? const Color(0xFF1A7A4A) : AppColors.fall; final pnlSign = pnlValue >= 0 ? '+' : ''; const iconColor = Colors.black54; return Container( margin: const EdgeInsets.fromLTRB(16, 12, 16, 4), decoration: BoxDecoration( color: AppColors.brand, borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.fromLTRB(16, 10, 16, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 眼睛 + 划转 Row( mainAxisAlignment: MainAxisAlignment.end, children: [ GestureDetector( onTap: () => setState(() => _visible = !_visible), child: Icon( _visible ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 18, color: iconColor, ), ), const SizedBox(width: 12), GestureDetector( onTap: widget.onTransfer ?? () => context.push('/asset/transfer?from=SPOT&to=FOLLOW'), child: const Icon(Icons.sync_alt, size: 18, color: iconColor), ), ], ), const SizedBox(height: 8), // 三列数据 Builder(builder: (context) { final l10n = AppLocalizations.of(context)!; return Row( children: [ _StatCol( label: l10n.cumCopyProfitUsdt, value: _visible ? _fmtDown(acc.cumulativePnl as double) : '****', ), _StatCol( label: l10n.availableBalanceUsdt, value: _visible ? _fmtDown(acc.availableBalance as double) : '****', ), _StatCol( label: l10n.unrealizedPnlUsdt, value: _visible ? '$pnlSign${_fmtDown(pnlValue.abs())}' : '****', valueColor: _visible ? pnlColor : null, ), ], ); }), ], ), ), ); } } class _StatCol extends StatelessWidget { const _StatCol({required this.label, required this.value, this.valueColor}); final String label; final String value; final Color? valueColor; @override Widget build(BuildContext context) { return Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle(color: Colors.black.withAlpha(140), fontSize: 11), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 5), Text( value, style: TextStyle( color: valueColor ?? Colors.black, fontSize: 16, fontWeight: FontWeight.w700, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ); } } // ── 仓位卡片 ──────────────────────────────────────────────── class _PositionCard extends StatelessWidget { const _PositionCard({ 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 isDark = Theme.of(context).brightness == Brightness.dark; final l10n = AppLocalizations.of(context)!; final isLong = position.isLong; final directionColor = isLong ? AppColors.rise : AppColors.fall; final directionLabel = isLong ? l10n.openLongBullish : l10n.openShortBearish; 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, 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), Text(position.traderName, style: TextStyle(color: cs.onSurface, fontSize: 14, fontWeight: FontWeight.w600)), const SizedBox(width: 10), GestureDetector( onTap: () => _showShareSheet(context), child: Icon(Icons.share_outlined, size: 18, color: cs.onSurface.withAlpha(120)), ), ], ) 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.closePosition, 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: 6, crossAxisAlignment: WrapCrossAlignment.center, 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.openAvgPriceUsdt, value: position.openPrice.toStringAsFixed(1)), _DataCell(label: l10n.currentPriceUsdt, value: position.currentPrice.toStringAsFixed(1)), _DataCell(label: l10n.currentMarginUsdt, value: position.margin.toStringAsFixed(2)), ], ), const SizedBox(height: 8), Row( children: [ _DataCell(label: l10n.qtyWithCoin(_baseAsset(position.symbol)), value: position.quantity.toStringAsFixed(4)), _DataCell(label: l10n.returnRate, value: '$roiSign${position.roi.toStringAsFixed(2)}%', valueColor: pnlColor), _DataCell(label: l10n.profitUsdt, value: '$pnlSign${position.unrealizedPnl.toStringAsFixed(3)}', valueColor: pnlColor), ], ), ] else ...[ // 历史跟单数据行:数量 / 收益 / 收益率(三列,最后列右对齐) Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(l10n.qtyWithCoin(_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.profitUsdt, 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.returnRate, 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.openAvgPriceUsdt, 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.closeAvgPriceUsdt, 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.openTimeWithValue(_formatDate(position.openTime)), style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 11), ), const Spacer(), Text('${l10n.positionIdPrefix}${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.copyPositionIdSuccess, backgroundColor: AppColors.rise); }, child: Icon(Icons.copy_outlined, size: 14, color: cs.onSurface.withAlpha(153)), ), ], ), ], ], ), ); } void _showShareSheet(BuildContext context) { showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (_) => _FollowShareSheet(position: position), ); } 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')}'; } // ── 历史跟单分享 BottomSheet ─────────────────────────────── class _FollowShareSheet extends ConsumerStatefulWidget { const _FollowShareSheet({required this.position}); final CopyPosition position; @override ConsumerState<_FollowShareSheet> createState() => _FollowShareSheetState(); } class _FollowShareSheetState extends ConsumerState<_FollowShareSheet> { final _cardKey = GlobalKey(); bool _sharing = false; bool _saving = false; String? _inviteCode; String? _inviteUrl; @override void initState() { super.initState(); _loadInviteInfo(); } Future _loadInviteInfo() async { try { final dio = ref.read(dioClientProvider); final data = await AuthService(dio).getMyInfo(); final prefix = data['promotionPrefix']?.toString() ?? ''; final code = data['promotionCode']?.toString() ?? ''; final url = (prefix.isNotEmpty || code.isNotEmpty) ? '$prefix$code' : null; if (mounted) { setState(() { _inviteCode = code.isNotEmpty ? code : null; _inviteUrl = url; }); } } catch (_) {} } Future _renderCard() async { final boundary = _cardKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; if (boundary == null) return null; final image = await boundary.toImage(pixelRatio: 3.0); final byteData = await image.toByteData(format: ui.ImageByteFormat.png); return byteData?.buffer.asUint8List(); } Future _doSave(BuildContext context) async { setState(() => _saving = true); try { final bytes = await _renderCard(); if (bytes == null) return; await Gal.requestAccess(); await Gal.putImageBytes( bytes, name: 'follow_share_${DateTime.now().millisecondsSinceEpoch}', ); if (!context.mounted) return; showTopToast(context, message: AppLocalizations.of(context)!.saveSuccess, backgroundColor: AppColors.rise); } on GalException catch (e) { if (!context.mounted) return; final l10n = AppLocalizations.of(context)!; if (e.type == GalExceptionType.accessDenied) { showTopToast(context, message: l10n.photoPermissionDenied, backgroundColor: AppColors.fall); } else { showTopToast(context, message: l10n.saveFailed, backgroundColor: AppColors.fall); } } catch (e) { if (context.mounted) { showTopToast(context, message: AppLocalizations.of(context)!.saveFailed, backgroundColor: AppColors.fall); } } finally { if (mounted) setState(() => _saving = false); } } Future _doShare(BuildContext context) async { setState(() => _sharing = true); try { final bytes = await _renderCard(); if (bytes == null) return; final tmpDir = await getTemporaryDirectory(); final file = File('${tmpDir.path}/follow_share_${DateTime.now().millisecondsSinceEpoch}.png'); await file.writeAsBytes(bytes); if (!context.mounted) return; Navigator.of(context).pop(); await Share.shareXFiles( [XFile(file.path, mimeType: 'image/png')], subject: AppLocalizations.of(context)!.myCopyTradingProfit, ); } catch (e) { if (context.mounted) { showTopToast(context, message: AppLocalizations.of(context)!.shareFailed, backgroundColor: AppColors.fall); } } finally { if (mounted) setState(() => _sharing = false); } } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final p = widget.position; final pnlPositive = p.unrealizedPnl >= 0; final l10n = AppLocalizations.of(context)!; return Container( decoration: BoxDecoration( color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary, borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), ), padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 拖拽指示条 Container( width: 36, height: 4, decoration: BoxDecoration( color: cs.onSurface.withAlpha(60), borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 16), // 分享卡片预览 RepaintBoundary( key: _cardKey, child: _FollowShareCard( position: p, inviteCode: _inviteCode, inviteUrl: _inviteUrl, ), ), const SizedBox(height: 24), // 操作按钮行:取消 | 保存海报 | 分享 Row( children: [ Expanded( child: OutlinedButton( onPressed: () => Navigator.of(context).pop(), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), child: Text(l10n.cancelLabel, style: TextStyle(color: cs.onSurface, fontSize: 14)), ), ), const SizedBox(width: 8), Expanded( child: OutlinedButton( onPressed: _saving ? null : () => _doSave(context), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), child: _saving ? SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: cs.onSurface.withAlpha(153)), ) : Text(l10n.savePoster, style: TextStyle(color: cs.onSurface, fontSize: 14)), ), ), const SizedBox(width: 8), Expanded( child: ElevatedButton( onPressed: _sharing ? null : () => _doShare(context), style: ElevatedButton.styleFrom( backgroundColor: pnlPositive ? AppColors.rise : AppColors.fall, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), elevation: 0, ), child: _sharing ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ) : Text(l10n.shareLabel, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600)), ), ), ], ), ], ), ); } } // ── 跟单分享卡片内容 ───────────────────────────────────────── class _FollowShareCard extends StatelessWidget { const _FollowShareCard({ required this.position, this.inviteCode, this.inviteUrl, }); final CopyPosition position; final String? inviteCode; final String? inviteUrl; String _baseCoin(String sym) { if (sym.contains('/')) return sym.split('/').first; return sym.toUpperCase().replaceFirst(RegExp(r'USDT$'), ''); } String _fmtDate(DateTime dt) { 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 '${dt.year}-$mo-$d $h:$mi:$s'; } @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final l10n = AppLocalizations.of(context)!; final p = position; final isLong = p.isLong; final sideColor = isLong ? AppColors.rise : AppColors.fall; final pnlPositive = p.unrealizedPnl >= 0; final pnlColor = pnlPositive ? AppColors.rise : AppColors.fall; final coinSymbol = _baseCoin(p.symbol); final roiStr = '${pnlPositive ? '+' : ''}${formatAmount(p.roi)}%'; final closePrice = p.closePrice; final closeTime = p.closeTime; // 主题色变量 final bgColors = isDark ? const [Color(0xFF1A1F2E), Color(0xFF0D1117)] : const [Color(0xFFF8F9FB), Color(0xFFEEF0F3)]; final textPrimary = isDark ? Colors.white : const Color(0xFF1A1F2E); final textSecondary = isDark ? Colors.white.withAlpha(120) : const Color(0xFF1A1F2E).withAlpha(120); final textMuted = isDark ? Colors.white.withAlpha(80) : const Color(0xFF1A1F2E).withAlpha(80); final borderColor = isDark ? Colors.white.withAlpha(40) : const Color(0xFF1A1F2E).withAlpha(30); final qrFgColor = isDark ? Colors.white : Colors.black; final qrBgColor = isDark ? const Color(0xFF1A1F2E) : Colors.white; return Container( width: double.infinity, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: bgColors, ), borderRadius: BorderRadius.circular(16), ), clipBehavior: Clip.antiAlias, child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // LOGO + 品牌名 Row( children: [ Image.asset( 'assets/images/app_icon.png', height: 28, width: 28, errorBuilder: (_, __, ___) => const SizedBox.shrink(), ), const SizedBox(width: 8), Text( 'iBit', style: TextStyle( color: textPrimary, fontSize: 14, fontWeight: FontWeight.w700, letterSpacing: 0.5), ), ], ), const SizedBox(height: 14), // 币对 + 永续 tag Row( children: [ Text( '${coinSymbol}USDT', style: TextStyle( color: textPrimary, fontSize: 22, fontWeight: FontWeight.w800), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), decoration: BoxDecoration( color: const Color(0xFFFFAB00), borderRadius: BorderRadius.circular(4), ), child: Text(l10n.perpetual, style: const TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.w700)), ), ], ), const SizedBox(height: 4), // 方向 + 杠杆 Text( '${isLong ? l10n.openLong : l10n.openShort} ${p.leverage}X', style: TextStyle( color: sideColor, fontSize: 15, fontWeight: FontWeight.w700), ), const SizedBox(height: 14), // 收益率(大字) Text(l10n.returnRate, style: TextStyle(color: textSecondary, fontSize: 12)), const SizedBox(height: 4), Text(roiStr, style: TextStyle( color: pnlColor, fontSize: 36, fontWeight: FontWeight.w800, letterSpacing: -0.5)), const SizedBox(height: 16), // 开仓均价 + 平仓均价 Row( children: [ Expanded( child: _FollowShareDataItem( label: l10n.openAvgPrice, value: formatAmount(p.openPrice), textPrimary: textPrimary, textSecondary: textSecondary, ), ), Expanded( child: _FollowShareDataItem( label: l10n.avgClosePrice, value: closePrice != null ? formatAmount(closePrice) : '--', align: CrossAxisAlignment.end, textPrimary: textPrimary, textSecondary: textSecondary, ), ), ], ), const SizedBox(height: 10), // 时间 Text(closeTime != null ? _fmtDate(closeTime) : _fmtDate(p.openTime), style: TextStyle(color: textMuted, fontSize: 11)), const SizedBox(height: 14), // 分隔线 Divider(color: borderColor, height: 1), const SizedBox(height: 14), // 邀请码 + 二维码 Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (inviteCode != null) RichText( text: TextSpan( style: const TextStyle(fontSize: 15), children: [ TextSpan( text: l10n.inviteCodeLabel, style: TextStyle(color: textSecondary), ), TextSpan( text: inviteCode!, style: const TextStyle( color: AppColors.brand, fontWeight: FontWeight.w700), ), ], ), ), const SizedBox(height: 4), Text(l10n.registerAndEarnRebate, style: TextStyle(color: textMuted, fontSize: 12)), ], ), ), Container( decoration: BoxDecoration( border: Border.all(color: borderColor, width: 1), borderRadius: BorderRadius.circular(6), ), padding: const EdgeInsets.all(4), child: inviteUrl != null ? QrImageView( data: inviteUrl!, version: QrVersions.auto, size: 80, eyeStyle: QrEyeStyle( eyeShape: QrEyeShape.square, color: qrFgColor, ), dataModuleStyle: QrDataModuleStyle( dataModuleShape: QrDataModuleShape.square, color: qrFgColor, ), backgroundColor: qrBgColor, errorCorrectionLevel: QrErrorCorrectLevel.M, ) : const SizedBox(width: 80, height: 80), ), ], ), ], ), ), ); } } class _FollowShareDataItem extends StatelessWidget { const _FollowShareDataItem({ required this.label, required this.value, required this.textPrimary, required this.textSecondary, this.align = CrossAxisAlignment.start, }); final String label; final String value; final Color textPrimary; final Color textSecondary; final CrossAxisAlignment align; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: align, children: [ Text(label, style: TextStyle(color: textSecondary, fontSize: 11)), const SizedBox(height: 2), Text(value, style: TextStyle( color: textPrimary, fontSize: 13, fontWeight: FontWeight.w600)), ], ); } } // ── 交易员头像 ──────────────────────────────────────────── class _TraderAvatar extends StatelessWidget { const _TraderAvatar({ 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)), ), ); } // ── 我的交易员卡片(原型风格) ───────────────────────────── class _MyTraderCard extends StatelessWidget { const _MyTraderCard({required this.trader, required this.onUnfollow}); final Trader trader; final VoidCallback onUnfollow; static const _avatarColors = [ Color(0xFFf7931a), Color(0xFF627eea), Color(0xFF9945ff), Color(0xFFf3ba2f), Color(0xFF2775ca), ]; Color get _bg => _avatarColors[trader.avatarLetter.codeUnitAt(0) % _avatarColors.length]; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; return Container( margin: const EdgeInsets.fromLTRB(16, 8, 16, 0), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: isDark ? AppColors.darkBgSecondary : AppColors.lightBg, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow(color: Colors.black.withAlpha(15), blurRadius: 8, offset: const Offset(0, 2)), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 头部:头像 + 名称+描述 + 取消跟随按钮 Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 头像 + 等级角标 SizedBox( width: 52, height: 60, child: Stack( clipBehavior: Clip.none, children: [ _TraderAvatar( name: trader.name, avatarUrl: trader.avatarUrl, bgColor: _bg, size: 52, ), if (trader.levelName != null && trader.levelName!.isNotEmpty) Positioned( bottom: 0, left: 0, right: 0, child: Center( child: Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), decoration: BoxDecoration( color: AppColors.darkBadgeBg, borderRadius: BorderRadius.circular(20), border: Border.all(color: AppColors.darkBgMid), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.link, size: 8, color: AppColors.rankPurple), const SizedBox(width: 2), Text(trader.levelName!, style: const TextStyle( color: AppColors.rankPurple, fontSize: 9, fontWeight: FontWeight.w700)), ], ), ), ), ), ], ), ), const SizedBox(width: 12), // 名称 + 描述 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(trader.name, style: TextStyle( color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w600)), if (trader.description != null && trader.description!.isNotEmpty) ...[ const SizedBox(height: 2), Text(trader.description!, style: TextStyle(color: cs.onSurface.withAlpha(130), fontSize: 12), maxLines: 1, overflow: TextOverflow.ellipsis), ], if (trader.tags.isNotEmpty) ...[ const SizedBox(height: 6), Wrap( spacing: 6, runSpacing: 4, children: trader.tags .map((tag) => Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3), decoration: BoxDecoration( color: isDark ? AppColors.tagIndigoBgDark : AppColors.tagIndigoBgLight, borderRadius: BorderRadius.circular(20), border: Border.all(color: AppColors.tagIndigo.withAlpha(80), width: 0.8), ), child: Text(tag, style: const TextStyle( color: AppColors.tagIndigo, fontSize: 11, fontWeight: FontWeight.w500)), )) .toList(), ), ], ], ), ), const SizedBox(width: 8), // 取消跟随按钮 OutlinedButton( onPressed: onUnfollow, style: OutlinedButton.styleFrom( backgroundColor: isDark ? AppColors.darkBgSecondary : AppColors.lightBg, side: BorderSide(color: cs.onSurface, width: 1.5), foregroundColor: cs.onSurface, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: Text(AppLocalizations.of(context)!.unfollow, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500)), ), ], ), const SizedBox(height: 12), Divider(height: 1, thickness: 0.5, color: cs.outlineVariant.withAlpha(80)), const SizedBox(height: 12), // 底部统计行(含竖向分隔线) Builder(builder: (context) { final l10n = AppLocalizations.of(context)!; return IntrinsicHeight( child: Row( children: [ _TraderStatCol( label: l10n.profitUsdt, value: trader.profitAmount == 0 ? '--' : '${trader.profitAmount >= 0 ? '+' : ''}${trader.profitAmount.toStringAsFixed(2)}', valueColor: trader.profitAmount >= 0 ? AppColors.rise : AppColors.fall, ), VerticalDivider(width: 1, thickness: 0.8, color: Colors.grey.withAlpha(100)), _TraderStatCol( label: l10n.cumFollowerCount, value: trader.followCustomer == 0 ? '--' : '${trader.followCustomer}', center: true, ), VerticalDivider(width: 1, thickness: 0.8, color: Colors.grey.withAlpha(100)), _TraderStatCol( label: l10n.cumTradingDays, value: trader.tradingDays == 0 ? '--' : '${trader.tradingDays}', alignEnd: true, ), ], ), ); }), ], ), ); } } class _TraderStatCol extends StatelessWidget { const _TraderStatCol({ required this.label, required this.value, this.valueColor, this.center = false, this.alignEnd = false, }); final String label; final String value; final Color? valueColor; final bool center; final bool alignEnd; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final align = alignEnd ? CrossAxisAlignment.end : center ? CrossAxisAlignment.center : CrossAxisAlignment.start; return Expanded( child: Column( crossAxisAlignment: align, children: [ Text(label, style: TextStyle(color: cs.onSurface.withAlpha(130), fontSize: 11)), const SizedBox(height: 3), Text(value, style: TextStyle( color: valueColor ?? cs.onSurface, fontSize: 14, fontWeight: FontWeight.w700)), ], ), ); } } // ── Badge / DataCell ────────────────────────────────────── class _Badge extends StatelessWidget { const _Badge({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, style: TextStyle(color: textColor, fontSize: 11, fontWeight: FontWeight.w500)), ); } } class _DataCell extends StatelessWidget { const _DataCell({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, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 11)), const SizedBox(height: 2), Text(value, style: TextStyle( color: valueColor ?? cs.onSurface, fontSize: 13, fontWeight: FontWeight.w500)), ], ), ); } } // ── 骨架屏 ──────────────────────────────────────────────── /// 「我的跟单」首次加载时的全页骨架 class _MyCopyTradingFullSkeleton extends StatelessWidget { const _MyCopyTradingFullSkeleton({required this.cardBg}); final Color cardBg; @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final cs = Theme.of(context).colorScheme; return Column( children: [ // 统计卡片骨架 AppShimmer( child: Container( margin: const EdgeInsets.fromLTRB(16, 12, 16, 4), padding: const EdgeInsets.fromLTRB(16, 10, 16, 16), decoration: BoxDecoration( color: AppColors.brand, borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.end, children: [shimmerBox(18, 18), const SizedBox(width: 10), shimmerBox(18, 18)], ), const SizedBox(height: 8), Row( children: List.generate(3, (i) => Expanded( child: Padding( padding: EdgeInsets.only(right: i < 2 ? 12 : 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ shimmerBox(70, 10), const SizedBox(height: 6), shimmerBox(55, 16), ], ), ), )), ), ], ), ), ), // Tab 骨架 Container( color: cardBg, child: AppShimmer( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Row( children: List.generate(3, (i) => Expanded( child: Padding( padding: EdgeInsets.symmetric(horizontal: i == 1 ? 8.0 : 0), child: shimmerFill(16, radius: 4), ), )), ), ), ), ), Divider(height: 1, thickness: 0.5, color: cs.outlineVariant.withAlpha(80)), // 列表骨架 Expanded( child: ListView.builder( padding: const EdgeInsets.fromLTRB(0, 8, 0, 16), itemCount: 4, itemBuilder: (_, __) => const _PositionCardSkeleton(), ), ), ], ); } } /// 仓位卡片骨架(对应 _PositionCard 当前跟单样式) class _PositionCardSkeleton extends StatelessWidget { const _PositionCardSkeleton(); @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return AppShimmer( child: 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: [ // 交易员头部行 Row( children: [ shimmerCircle(38), const SizedBox(width: 10), Expanded(child: shimmerBox(100, 15)), shimmerBox(60, 32, radius: 8), ], ), const SizedBox(height: 10), // 品种行 Row(children: [ shimmerBox(80, 14), const SizedBox(width: 8), shimmerBox(60, 20, radius: 4), const SizedBox(width: 6), shimmerBox(30, 20, radius: 4), const SizedBox(width: 6), shimmerBox(36, 20, radius: 4), ]), const SizedBox(height: 10), // 数据行 1 Row(children: List.generate(3, (i) => Expanded( child: Padding( padding: EdgeInsets.only(right: i < 2 ? 8 : 0), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ shimmerBox(70, 11), const SizedBox(height: 4), shimmerBox(55, 13), ]), ), ))), const SizedBox(height: 8), // 数据行 2 Row(children: List.generate(3, (i) => Expanded( child: Padding( padding: EdgeInsets.only(right: i < 2 ? 8 : 0), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ shimmerBox(70, 11), const SizedBox(height: 4), shimmerBox(55, 13), ]), ), ))), ], ), ), ); } } /// 我的交易员卡片骨架(对应 _MyTraderCard 样式) class _MyTraderCardSkeleton extends StatelessWidget { const _MyTraderCardSkeleton(); @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final cs = Theme.of(context).colorScheme; return AppShimmer( child: Container( margin: const EdgeInsets.fromLTRB(16, 8, 16, 0), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: isDark ? AppColors.darkBgSecondary : AppColors.lightBg, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow(color: Colors.black.withAlpha(15), blurRadius: 8, offset: const Offset(0, 2)), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ shimmerCircle(52), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ shimmerBox(110, 15), const SizedBox(height: 6), shimmerBox(160, 12), const SizedBox(height: 8), Row(children: [shimmerBox(55, 22, radius: 20), const SizedBox(width: 6), shimmerBox(45, 22, radius: 20)]), ], ), ), shimmerBox(72, 30, radius: 20), ], ), const SizedBox(height: 12), Divider(height: 1, thickness: 0.5, color: cs.outlineVariant.withAlpha(80)), const SizedBox(height: 12), Row( children: List.generate(3, (i) => Expanded( child: Padding( padding: EdgeInsets.symmetric(horizontal: i == 1 ? 8.0 : 0), child: Column( crossAxisAlignment: i == 0 ? CrossAxisAlignment.start : i == 1 ? CrossAxisAlignment.center : CrossAxisAlignment.end, children: [ shimmerBox(55, 11), const SizedBox(height: 5), shimmerBox(40, 14), ], ), ), )), ), ], ), ), ); } } // ── 空状态 ──────────────────────────────────────────────── class _EmptyState extends StatelessWidget { const _EmptyState({this.onGoMarket}); final VoidCallback? onGoMarket; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.people_alt_outlined, size: 64, color: cs.onSurface.withAlpha(80)), const SizedBox(height: 16), Text(AppLocalizations.of(context)!.noData, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 15)), ], ), ); } }