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/dialog_utils.dart' show showConfirmDialog, extractErrorMessage; import '../../../core/utils/number_format.dart'; import '../../../core/utils/top_toast.dart'; import '../../../providers/asset_provider.dart'; import '../../../providers/my_copy_trading_provider.dart'; import '../../widgets/common/app_shimmer.dart'; import '../../widgets/common/app_refresh_indicator.dart'; import '../../widgets/copy_position_card.dart'; /// 跟单 Tab class AssetCopyTradingTab extends ConsumerWidget { const AssetCopyTradingTab({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 obscure = state.obscureBalance; final followBalance = state.walletBalance('FOLLOW').toDouble(); final display = obscure ? '******' : formatPrice(followBalance, decimalPlaces: 2); // 未实现盈亏:累加所有当前跟单仓位的收益 final copyState = ref.watch(myCopyTradingProvider); final followPnl = copyState.currentPositions.fold(0.0, (sum, p) => sum + p.unrealizedPnl); return AppRefreshIndicator( onRefresh: () => Future.wait([ notifier.silentRefresh(), ref.read(myCopyTradingProvider.notifier).silentRefresh(), ]), child: ListView( children: [ // 跟单账户余额 Padding( padding: const EdgeInsets.fromLTRB(16, 20, 16, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text(AppLocalizations.of(context)!.copyAccount, 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(AppLocalizations.of(context)!.copyBalance, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13)), Text( obscure ? '******' : formatPrice(state.accountBalance('FOLLOW').toDouble(), decimalPlaces: 2), style: TextStyle(color: cs.onSurface, fontSize: 13, fontWeight: FontWeight.w500), ), ], ), const SizedBox(height: 8), // 未实现盈亏:从当前跟单仓位累加 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(AppLocalizations.of(context)!.unrealizedPnl, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13)), Text( obscure ? '******' : '${followPnl >= 0 ? '+' : ''}${formatPrice(followPnl, decimalPlaces: 2)}', style: TextStyle( color: followPnl > 0 ? AppColors.rise : followPnl < 0 ? AppColors.fall : cs.onSurface, fontSize: 13, fontWeight: FontWeight.w500, ), ), ], ), ], ), ), // 跟单 + 划转 按钮 Padding( padding: const EdgeInsets.fromLTRB(16, 20, 16, 0), child: Row( children: [ Expanded( child: GestureDetector( onTap: () => context.go('/copy-trading'), child: Container( height: 44, decoration: BoxDecoration(color: AppColors.brand, borderRadius: BorderRadius.circular(22)), child: Center(child: Text(AppLocalizations.of(context)!.copyTrading, style: const TextStyle(color: Colors.black, fontSize: 15, fontWeight: FontWeight.w600))), ), ), ), const SizedBox(width: 12), Expanded( child: GestureDetector( onTap: () => context.push('/asset/transfer?from=SPOT&to=FOLLOW'), child: Container( height: 44, decoration: BoxDecoration(color: AppColors.brand, borderRadius: BorderRadius.circular(22)), child: Center(child: Text(AppLocalizations.of(context)!.transfer, style: const TextStyle(color: Colors.black, fontSize: 15, fontWeight: FontWeight.w600))), ), ), ), ], ), ), const SizedBox(height: 24), // 当前跟单标题 Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), child: Text(AppLocalizations.of(context)!.currentCopyTrading, style: TextStyle(color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w600)), ), // 跟单持仓卡片(加载中显示骨架) _buildPositions(ref, context), const SizedBox(height: 32), ], ), ); } Widget _buildPositions(WidgetRef ref, BuildContext context) { final copyState = ref.watch(myCopyTradingProvider); if (copyState.isLoading && copyState.currentPositions.isEmpty) { return Column( children: List.generate(2, (_) => const _CopyPositionSkeleton()), ); } return _CurrentPositionsList( positions: copyState.currentPositions, onClose: (positionId) async { final confirmed = await showConfirmDialog( context, content: AppLocalizations.of(context)!.closePositionConfirm, ); if (!confirmed || !context.mounted) return; try { await ref.read(myCopyTradingProvider.notifier).closePosition(positionId); 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); } } }, ); } } /// 跟单持仓卡片骨架(资产页跟单 Tab 使用) class _CopyPositionSkeleton extends StatelessWidget { const _CopyPositionSkeleton(); @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return AppShimmer( child: Container( margin: const EdgeInsets.fromLTRB(16, 0, 16, 12), 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(height: 10), Row(children: List.generate(3, (i) => Expanded( child: Padding( padding: EdgeInsets.only(right: i < 2 ? 8 : 0), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ shimmerBox(65, 11), const SizedBox(height: 4), shimmerBox(50, 13), ]), ), ))), const SizedBox(height: 8), Row(children: List.generate(3, (i) => Expanded( child: Padding( padding: EdgeInsets.only(right: i < 2 ? 8 : 0), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ shimmerBox(65, 11), const SizedBox(height: 4), shimmerBox(50, 13), ]), ), ))), ], ), ), ); } } /// Widget to display current copy trading positions class _CurrentPositionsList extends StatelessWidget { const _CurrentPositionsList({ required this.positions, required this.onClose, }); final List positions; final Future Function(String positionId) onClose; @override Widget build(BuildContext context) { if (positions.isEmpty) { return Padding( padding: const EdgeInsets.symmetric(vertical: 32), child: Center( child: Text( AppLocalizations.of(context)!.noFollowing, style: TextStyle( color: Theme.of(context).colorScheme.onSurface.withAlpha(153), fontSize: 14, ), ), ), ); } return Column( children: positions .map((position) => CopyPositionCard( position: position, isHistory: false, onClose: (_) async => onClose(position.id), )) .toList(), ); } }