| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434 |
- 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<Trader> 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<String, dynamic>? wallet;
- /// 带单员专属统计(follow/customer/trader-info/{id})
- /// 字段:currentClearedProfit、totalFollowProfit、following、maxFollow
- final Map<String, dynamic>? 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<Trader>? traders,
- bool? isLoading,
- String? error,
- String? searchKeyword,
- int? tabIndex,
- TraderSort? sort,
- bool? isTrader,
- Map<String, dynamic>? wallet,
- Map<String, dynamic>? 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<Trader> 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<CopyTradingState> {
- static const _pageSize = 10;
- Timer? _searchDebounce;
- @override
- CopyTradingState build() {
- // 用 listen 代替 watch,避免登录/登出时 notifier 整体重建与同帧
- // context.go('/') 导航并发,触发 element._lifecycleState 断言崩溃。
- // (同 app_router.dart 中登录时不调用 notifyListeners 的原因一致)
- ref.listen<bool>(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<List<Trader>> _mergeFavoriteFlags(List<Trader> traders) async {
- if (traders.isEmpty) {
- return traders;
- }
- try {
- final rows = await _repo.getFavoriteList(currentPage: 1, pageSize: 200);
- final favIds = <String>{};
- 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<void> _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<String, dynamic>?;
- final rawList = favoriteResults[1] as List<Map<String, dynamic>>;
- final followWallet = favoriteResults[2] as Map<String, dynamic>?;
- final contractWallet = favoriteResults[3] as Map<String, dynamic>?;
- final traderLevel = followerInfo?['trader']?.toString() ??
- followerInfo?['traderLevel']?.toString() ??
- '';
- final isTrader = traderLevel == '20';
- final traderId = followerInfo?['id']?.toString() ?? '';
- Map<String, dynamic>? 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<String, dynamic>?;
- final rawList = results[1] as List<Map<String, dynamic>>;
- final followWallet = results[2] as Map<String, dynamic>?;
- final contractWallet = results[3] as Map<String, dynamic>?;
- final traderLevel = followerInfo?['trader']?.toString() ??
- followerInfo?['traderLevel']?.toString() ??
- '';
- final isTrader = traderLevel == '20';
- final traderId = followerInfo?['id']?.toString() ?? '';
- Map<String, dynamic>? 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<void> _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<void> 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<Map<String, dynamic>> 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<void> refresh() {
- final isLoggedIn = ref.read(isLoggedInProvider);
- return isLoggedIn ? _load() : _loadPublic();
- }
- Future<void> silentRefresh() {
- final isLoggedIn = ref.read(isLoggedInProvider);
- return isLoggedIn ? _load(silent: true) : _loadPublic();
- }
- /// 收藏 / 取消收藏。成功返回新的收藏状态,失败还原列表并返回 null。
- Future<bool?> toggleFavorite(Trader trader) async {
- if (!ref.read(isLoggedInProvider)) {
- return null;
- }
- final was = trader.isFavorited;
- final id = trader.id;
- if (id.isEmpty) {
- return null;
- }
- List<Trader> optimistic(List<Trader> 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, CopyTradingState>(
- CopyTradingNotifier.new,
- );
|