import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../data/models/copy_trading/trader.dart'; import '../data/repositories/copy_trading_repository.dart'; import 'auth_provider.dart'; // ── 排序枚举 ───────────────────────────────────────────── enum TraderSort { comprehensive, // 综合排序 → API orderBy=10 winRate30d, // 近14天胜率 → orderBy=30 roi30d, // 近14天收益率 → orderBy=20 } extension TraderSortExt on TraderSort { String get apiOrderBy { switch (this) { case TraderSort.comprehensive: return '10'; case TraderSort.winRate30d: return '30'; case TraderSort.roi30d: return '20'; } } } // ── UI State ───────────────────────────────────────────── class CopyTradingState { final List traders; final bool isLoading; final String? error; final String searchKeyword; /// 0=普通带单 1=无损带单 2=未登录时为「全部」;已登录时为「我的收藏」(与 Web 一致) final int tabIndex; final TraderSort sort; /// 是否是带单员(traderLevel == "20") final bool isTrader; /// 权益数据: /// - 跟单员:来自 swap/follow-wallet/get(balance 字段) /// - 带单员:来自 swap/wallet-new/get(currentCapital 字段) final Map? wallet; /// 带单员专属统计(follow/customer/trader-info/{id}) /// 字段:currentClearedProfit、totalFollowProfit、following、maxFollow final Map? traderInfo; // ── 分页 ────────────────────────────────────────────── final int page; final bool hasMore; final bool isLoadingMore; const CopyTradingState({ this.traders = const [], this.isLoading = false, this.error, this.searchKeyword = '', this.tabIndex = 0, this.sort = TraderSort.comprehensive, this.isTrader = false, this.wallet, this.traderInfo, this.page = 1, this.hasMore = true, this.isLoadingMore = false, }); CopyTradingState copyWith({ List? traders, bool? isLoading, String? error, String? searchKeyword, int? tabIndex, TraderSort? sort, bool? isTrader, Map? wallet, Map? traderInfo, int? page, bool? hasMore, bool? isLoadingMore, }) => CopyTradingState( traders: traders ?? this.traders, isLoading: isLoading ?? this.isLoading, error: error, searchKeyword: searchKeyword ?? this.searchKeyword, tabIndex: tabIndex ?? this.tabIndex, sort: sort ?? this.sort, isTrader: isTrader ?? this.isTrader, wallet: wallet ?? this.wallet, traderInfo: traderInfo ?? this.traderInfo, page: page ?? this.page, hasMore: hasMore ?? this.hasMore, isLoadingMore: isLoadingMore ?? this.isLoadingMore, ); String get _traderTypeForList { if (tabIndex == 2) { return ''; // 未登录第三 tab「全部」;已登录不会在公开列表链路使用 } switch (tabIndex) { case 0: return '0'; case 1: return '1'; default: return ''; } } List get displayTraders { if (searchKeyword.isEmpty) return traders; final kw = searchKeyword.toLowerCase(); return traders.where((t) => t.name.toLowerCase().contains(kw)).toList(); } } // ── Notifier ───────────────────────────────────────────── class CopyTradingNotifier extends Notifier { static const _pageSize = 10; Timer? _searchDebounce; @override CopyTradingState build() { // 用 listen 代替 watch,避免登录/登出时 notifier 整体重建与同帧 // context.go('/') 导航并发,触发 element._lifecycleState 断言崩溃。 // (同 app_router.dart 中登录时不调用 notifyListeners 的原因一致) ref.listen(isLoggedInProvider, (prev, next) { if (prev == next) return; // 推迟到下一帧:确保 GoRouter 导航操作完成后再重置 state, // 防止两者同帧操作 element tree 产生冲突。 WidgetsBinding.instance.addPostFrameCallback((_) { state = CopyTradingState(isLoading: true); if (next) { _load(); } else { _loadPublic(); } }); }); final isLoggedIn = ref.read(isLoggedInProvider); if (isLoggedIn) { Future.microtask(_load); } else { Future.microtask(_loadPublic); } return CopyTradingState(isLoading: true); } CopyTradingRepository get _repo => ref.read(copyTradingRepositoryProvider); bool _favoriteTabLoggedIn() => state.tabIndex == 2 && ref.read(isLoggedInProvider); Future> _mergeFavoriteFlags(List traders) async { if (traders.isEmpty) { return traders; } try { final rows = await _repo.getFavoriteList(currentPage: 1, pageSize: 200); final favIds = {}; for (final r in rows) { final sid = '${r['id'] ?? r['traderId'] ?? r['trader_id'] ?? ''}'.trim(); if (sid.isNotEmpty) { favIds.add(sid); } } return traders .map((t) => t.copyWith(isFavorited: favIds.contains(t.id))) .toList(); } catch (_) { return traders; } } Future _load({bool silent = false}) async { if (!silent) { state = state.copyWith(isLoading: true, error: null); } else { state = state.copyWith(error: null); } try { if (_favoriteTabLoggedIn()) { final favoriteResults = await Future.wait([ _repo.getFollowerInfo(), _repo.getFavoriteList(currentPage: 1, pageSize: 200), _repo.getFollowWallet(), _repo.getContractWallet(), ]); final followerInfo = favoriteResults[0] as Map?; final rawList = favoriteResults[1] as List>; final followWallet = favoriteResults[2] as Map?; final contractWallet = favoriteResults[3] as Map?; final traderLevel = followerInfo?['trader']?.toString() ?? followerInfo?['traderLevel']?.toString() ?? ''; final isTrader = traderLevel == '20'; final traderId = followerInfo?['id']?.toString() ?? ''; Map? traderInfoData; if (isTrader && traderId.isNotEmpty) { traderInfoData = await _repo.getTraderInfo(traderId); } final wallet = isTrader ? contractWallet : followWallet; final traders = rawList .map((e) => Trader.fromApi(e, isFavoritedOverride: true)) .toList(); state = state.copyWith( isLoading: false, error: null, traders: traders, isTrader: isTrader, wallet: wallet, traderInfo: traderInfoData, page: 1, hasMore: false, isLoadingMore: false, ); return; } final results = await Future.wait([ _repo.getFollowerInfo(), _repo.getTraderList( orderBy: state.sort.apiOrderBy, traderType: state._traderTypeForList, nickName: state.searchKeyword, page: 1, pageSize: _pageSize, ), _repo.getFollowWallet(), _repo.getContractWallet(), ]); final followerInfo = results[0] as Map?; final rawList = results[1] as List>; final followWallet = results[2] as Map?; final contractWallet = results[3] as Map?; final traderLevel = followerInfo?['trader']?.toString() ?? followerInfo?['traderLevel']?.toString() ?? ''; final isTrader = traderLevel == '20'; final traderId = followerInfo?['id']?.toString() ?? ''; Map? traderInfoData; if (isTrader && traderId.isNotEmpty) { traderInfoData = await _repo.getTraderInfo(traderId); } final wallet = isTrader ? contractWallet : followWallet; final isNoLossTab = state.tabIndex == 1; var traders = rawList .map((e) => Trader.fromApi(e, isNoLoss: isNoLossTab)) .toList(); traders = await _mergeFavoriteFlags(traders); state = state.copyWith( isLoading: false, traders: traders, isTrader: isTrader, wallet: wallet, traderInfo: traderInfoData, page: 1, hasMore: rawList.length >= _pageSize, isLoadingMore: false, ); } catch (e) { state = state.copyWith( isLoading: false, error: e.toString(), traders: [], page: 1, hasMore: false, ); } } Future _loadPublic() async { state = state.copyWith(isLoading: true, error: null); try { final rawList = await _repo.getPublicTraderList( page: 1, pageSize: _pageSize, traderType: state._traderTypeForList, orderBy: state.sort.apiOrderBy, ); final isNoLossTab = state.tabIndex == 1; final traders = rawList.map((e) => Trader.fromApi(e, isNoLoss: isNoLossTab)).toList(); state = state.copyWith( isLoading: false, traders: traders, page: 1, hasMore: rawList.length >= _pageSize, isLoadingMore: false, ); } catch (_) { state = state.copyWith(isLoading: false, traders: [], page: 1, hasMore: false); } } Future loadMore() async { if (_favoriteTabLoggedIn()) { return; } if (!state.hasMore || state.isLoadingMore || state.isLoading) { return; } final nextPage = state.page + 1; state = state.copyWith(isLoadingMore: true); try { final isLoggedIn = ref.read(isLoggedInProvider); List> rawList; if (isLoggedIn) { rawList = await _repo.getTraderList( orderBy: state.sort.apiOrderBy, traderType: state._traderTypeForList, nickName: state.searchKeyword, page: nextPage, pageSize: _pageSize, ); } else { rawList = await _repo.getPublicTraderList( page: nextPage, pageSize: _pageSize, traderType: state._traderTypeForList, orderBy: state.sort.apiOrderBy, ); } final isNoLossTab = state.tabIndex == 1; final more = rawList .map((e) => Trader.fromApi(e, isNoLoss: isNoLossTab)) .toList(); final mergedMore = await _mergeFavoriteFlags(more); state = state.copyWith( isLoadingMore: false, traders: [...state.traders, ...mergedMore], page: nextPage, hasMore: rawList.length >= _pageSize, ); } catch (_) { state = state.copyWith(isLoadingMore: false); } } void setTab(int index) { state = state.copyWith(tabIndex: index, traders: [], page: 1, hasMore: true, isLoadingMore: false, isLoading: true); final isLoggedIn = ref.read(isLoggedInProvider); Future.microtask(isLoggedIn ? _load : _loadPublic); } void setSearch(String kw) { state = state.copyWith(searchKeyword: kw); if (_favoriteTabLoggedIn()) { return; } _searchDebounce?.cancel(); _searchDebounce = Timer(const Duration(milliseconds: 400), () { final isLoggedIn = ref.read(isLoggedInProvider); Future.microtask(isLoggedIn ? _load : _loadPublic); }); } void setSort(TraderSort sort) { if (_favoriteTabLoggedIn()) { state = state.copyWith(sort: sort); return; } state = state.copyWith(sort: sort, traders: [], page: 1, hasMore: true, isLoadingMore: false); final isLoggedIn = ref.read(isLoggedInProvider); Future.microtask(isLoggedIn ? _load : _loadPublic); } Future refresh() { final isLoggedIn = ref.read(isLoggedInProvider); return isLoggedIn ? _load() : _loadPublic(); } Future silentRefresh() { final isLoggedIn = ref.read(isLoggedInProvider); return isLoggedIn ? _load(silent: true) : _loadPublic(); } /// 收藏 / 取消收藏。成功返回新的收藏状态,失败还原列表并返回 null。 Future toggleFavorite(Trader trader) async { if (!ref.read(isLoggedInProvider)) { return null; } final was = trader.isFavorited; final id = trader.id; if (id.isEmpty) { return null; } List optimistic(List list) { return list .map((t) => t.id == id ? t.copyWith(isFavorited: !was) : t) .toList(); } final prevList = state.traders; var nextList = optimistic(prevList); if (_favoriteTabLoggedIn() && was) { nextList = nextList.where((t) => t.id != id).toList(); } state = state.copyWith(traders: nextList); try { if (was) { await _repo.unfavoriteTrader(id); } else { await _repo.favoriteTrader(id); } return !was; } catch (_) { state = state.copyWith(traders: prevList); return null; } } } final copyTradingProvider = NotifierProvider( CopyTradingNotifier.new, );