my_copy_trading_provider.dart 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. import 'package:flutter_riverpod/flutter_riverpod.dart';
  2. import '../data/models/copy_trading/copy_account.dart';
  3. import '../data/models/copy_trading/copy_position.dart';
  4. import '../data/models/copy_trading/trader.dart';
  5. import '../data/repositories/copy_trading_repository.dart';
  6. import 'auth_provider.dart';
  7. // ── UI State ─────────────────────────────────────────────
  8. class MyCopyTradingState {
  9. final CopyAccount? account;
  10. final List<CopyPosition> currentPositions;
  11. final List<CopyPosition> historyPositions;
  12. final List<Trader> myTraders;
  13. final bool isLoading;
  14. final int tabIndex; // 0=当前跟单 1=我的交易员 2=历史跟单
  15. // ── 分页状态(每个 tab 独立) ─────────────────────────
  16. final int currentPage;
  17. final bool currentHasMore;
  18. final bool currentLoadingMore;
  19. final int tradersPage;
  20. final bool tradersHasMore;
  21. final bool tradersLoadingMore;
  22. final int historyPage;
  23. final bool historyHasMore;
  24. final bool historyLoadingMore;
  25. const MyCopyTradingState({
  26. this.account,
  27. this.currentPositions = const [],
  28. this.historyPositions = const [],
  29. this.myTraders = const [],
  30. this.isLoading = false,
  31. this.tabIndex = 0,
  32. this.currentPage = 1,
  33. this.currentHasMore = true,
  34. this.currentLoadingMore = false,
  35. this.tradersPage = 1,
  36. this.tradersHasMore = true,
  37. this.tradersLoadingMore = false,
  38. this.historyPage = 1,
  39. this.historyHasMore = true,
  40. this.historyLoadingMore = false,
  41. });
  42. MyCopyTradingState copyWith({
  43. CopyAccount? account,
  44. List<CopyPosition>? currentPositions,
  45. List<CopyPosition>? historyPositions,
  46. List<Trader>? myTraders,
  47. bool? isLoading,
  48. int? tabIndex,
  49. int? currentPage,
  50. bool? currentHasMore,
  51. bool? currentLoadingMore,
  52. int? tradersPage,
  53. bool? tradersHasMore,
  54. bool? tradersLoadingMore,
  55. int? historyPage,
  56. bool? historyHasMore,
  57. bool? historyLoadingMore,
  58. }) =>
  59. MyCopyTradingState(
  60. account: account ?? this.account,
  61. currentPositions: currentPositions ?? this.currentPositions,
  62. historyPositions: historyPositions ?? this.historyPositions,
  63. myTraders: myTraders ?? this.myTraders,
  64. isLoading: isLoading ?? this.isLoading,
  65. tabIndex: tabIndex ?? this.tabIndex,
  66. currentPage: currentPage ?? this.currentPage,
  67. currentHasMore: currentHasMore ?? this.currentHasMore,
  68. currentLoadingMore: currentLoadingMore ?? this.currentLoadingMore,
  69. tradersPage: tradersPage ?? this.tradersPage,
  70. tradersHasMore: tradersHasMore ?? this.tradersHasMore,
  71. tradersLoadingMore: tradersLoadingMore ?? this.tradersLoadingMore,
  72. historyPage: historyPage ?? this.historyPage,
  73. historyHasMore: historyHasMore ?? this.historyHasMore,
  74. historyLoadingMore: historyLoadingMore ?? this.historyLoadingMore,
  75. );
  76. }
  77. // ── Notifier ─────────────────────────────────────────────
  78. class MyCopyTradingNotifier extends Notifier<MyCopyTradingState> {
  79. static const _pageSize = 10;
  80. @override
  81. MyCopyTradingState build() {
  82. final isLoggedIn = ref.watch(isLoggedInProvider);
  83. if (isLoggedIn) Future.microtask(_load);
  84. return const MyCopyTradingState(isLoading: true);
  85. }
  86. CopyTradingRepository get _repo => ref.read(copyTradingRepositoryProvider);
  87. Future<void> _load({bool silent = false}) async {
  88. if (!silent) state = state.copyWith(isLoading: true);
  89. try {
  90. // 并行加载全部数据(第1页,每页10条)
  91. final results = await Future.wait([
  92. _repo.getFollowWallet(),
  93. _repo.getCurrentCopyPositions(page: 1, pageSize: _pageSize),
  94. _repo.getFollowingTraders(page: 1, pageSize: _pageSize),
  95. _repo.getHistoryCopyPositions(page: 1, pageSize: _pageSize),
  96. ]);
  97. final walletMap = results[0] as Map<String, dynamic>?;
  98. final currentMaps = results[1] as List<Map<String, dynamic>>;
  99. final traderMaps = results[2] as List<Map<String, dynamic>>;
  100. final historyMaps = results[3] as List<Map<String, dynamic>>;
  101. // 未实现盈亏 = 当前所有持仓的 profit 之和
  102. // (follow-wallet/get 的 currentRevenue 字段服务端返回 0,需客户端汇总)
  103. final totalUnrealizedPnl = currentMaps.fold<double>(
  104. 0.0,
  105. (sum, m) => sum + (double.tryParse((m['profit'] ?? '0').toString()) ?? 0.0),
  106. );
  107. CopyAccount? account;
  108. if (walletMap != null) {
  109. account = CopyAccount.fromApi(walletMap).copyWith(unrealizedPnl: totalUnrealizedPnl);
  110. }
  111. state = state.copyWith(
  112. isLoading: false,
  113. account: account,
  114. currentPositions: currentMaps.map(CopyPosition.fromApi).toList(),
  115. myTraders: traderMaps.map((e) => Trader.fromApi(e)).toList(),
  116. historyPositions: historyMaps.map(CopyPosition.fromApi).toList(),
  117. currentPage: 1,
  118. currentHasMore: currentMaps.length >= _pageSize,
  119. currentLoadingMore: false,
  120. tradersPage: 1,
  121. tradersHasMore: traderMaps.length >= _pageSize,
  122. tradersLoadingMore: false,
  123. historyPage: 1,
  124. historyHasMore: historyMaps.length >= _pageSize,
  125. historyLoadingMore: false,
  126. );
  127. } catch (e) {
  128. state = state.copyWith(isLoading: false);
  129. }
  130. }
  131. void setTab(int index) {
  132. state = state.copyWith(tabIndex: index);
  133. Future.microtask(silentRefresh);
  134. }
  135. /// 首次加载(显示全屏 spinner)
  136. Future<void> refresh() => _load();
  137. /// 静默刷新,不显示全屏 spinner(用于下拉刷新、tab 切换)
  138. Future<void> silentRefresh() => _load(silent: true);
  139. Future<void> loadMoreCurrent() async {
  140. if (!state.currentHasMore || state.currentLoadingMore || state.isLoading) return;
  141. final nextPage = state.currentPage + 1;
  142. state = state.copyWith(currentLoadingMore: true);
  143. try {
  144. final maps = await _repo.getCurrentCopyPositions(page: nextPage, pageSize: _pageSize);
  145. state = state.copyWith(
  146. currentLoadingMore: false,
  147. currentPositions: [...state.currentPositions, ...maps.map(CopyPosition.fromApi)],
  148. currentPage: nextPage,
  149. currentHasMore: maps.length >= _pageSize,
  150. );
  151. } catch (_) {
  152. state = state.copyWith(currentLoadingMore: false);
  153. }
  154. }
  155. Future<void> loadMoreTraders() async {
  156. if (!state.tradersHasMore || state.tradersLoadingMore || state.isLoading) return;
  157. final nextPage = state.tradersPage + 1;
  158. state = state.copyWith(tradersLoadingMore: true);
  159. try {
  160. final maps = await _repo.getFollowingTraders(page: nextPage, pageSize: _pageSize);
  161. state = state.copyWith(
  162. tradersLoadingMore: false,
  163. myTraders: [...state.myTraders, ...maps.map((e) => Trader.fromApi(e))],
  164. tradersPage: nextPage,
  165. tradersHasMore: maps.length >= _pageSize,
  166. );
  167. } catch (_) {
  168. state = state.copyWith(tradersLoadingMore: false);
  169. }
  170. }
  171. Future<void> loadMoreHistory() async {
  172. if (!state.historyHasMore || state.historyLoadingMore || state.isLoading) return;
  173. final nextPage = state.historyPage + 1;
  174. state = state.copyWith(historyLoadingMore: true);
  175. try {
  176. final maps = await _repo.getHistoryCopyPositions(page: nextPage, pageSize: _pageSize);
  177. state = state.copyWith(
  178. historyLoadingMore: false,
  179. historyPositions: [...state.historyPositions, ...maps.map(CopyPosition.fromApi)],
  180. historyPage: nextPage,
  181. historyHasMore: maps.length >= _pageSize,
  182. );
  183. } catch (_) {
  184. state = state.copyWith(historyLoadingMore: false);
  185. }
  186. }
  187. Future<void> unfollowTrader(String traderId) async {
  188. await _repo.unfollowTrader(traderId);
  189. await _load();
  190. }
  191. Future<void> closePosition(String positionId) async {
  192. // 乐观更新:立即从列表移除,让用户感知即时响应
  193. final original = state.currentPositions;
  194. state = state.copyWith(
  195. currentPositions: original.where((p) => p.id != positionId).toList(),
  196. );
  197. try {
  198. await _repo.closeCopyPosition(positionId);
  199. // 静默刷新当前持仓列表(不影响其他 tab 数据、不显示全屏 loading)
  200. await _refreshCurrentPositions();
  201. } catch (e) {
  202. // API 失败则还原列表
  203. state = state.copyWith(currentPositions: original);
  204. rethrow;
  205. }
  206. }
  207. Future<void> _refreshCurrentPositions() async {
  208. try {
  209. final maps = await _repo.getCurrentCopyPositions(page: 1, pageSize: _pageSize);
  210. final totalUnrealizedPnl = maps.fold<double>(
  211. 0.0,
  212. (sum, m) => sum + (double.tryParse((m['profit'] ?? '0').toString()) ?? 0.0),
  213. );
  214. final account = state.account?.copyWith(unrealizedPnl: totalUnrealizedPnl);
  215. state = state.copyWith(
  216. currentPositions: maps.map(CopyPosition.fromApi).toList(),
  217. account: account ?? state.account,
  218. currentPage: 1,
  219. currentHasMore: maps.length >= _pageSize,
  220. currentLoadingMore: false,
  221. );
  222. } catch (_) {}
  223. }
  224. }
  225. final myCopyTradingProvider =
  226. NotifierProvider<MyCopyTradingNotifier, MyCopyTradingState>(
  227. MyCopyTradingNotifier.new,
  228. );