| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- 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<dynamic> positions;
- final Future<void> 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(),
- );
- }
- }
|