statement_provider.dart 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. import 'package:flutter_riverpod/flutter_riverpod.dart';
  2. import 'package:intl/intl.dart';
  3. import '../core/network/dio_client.dart';
  4. import '../data/models/asset/asset_statement.dart';
  5. import '../data/services/asset_service.dart';
  6. // ══════════════════════════════════════════════════════════════
  7. // 全局缓存:币种列表 + 类型列表(不随页面销毁)
  8. // ══════════════════════════════════════════════════════════════
  9. class _StatementFilterCache extends Notifier<({List<StatementCoin> coins, List<StatementType> types})> {
  10. @override
  11. ({List<StatementCoin> coins, List<StatementType> types}) build() {
  12. Future.microtask(_load);
  13. return (coins: const [], types: const []);
  14. }
  15. bool get isLoaded => state.coins.isNotEmpty || state.types.isNotEmpty;
  16. Future<void> _load() async {
  17. try {
  18. final dio = ref.read(dioClientProvider);
  19. final service = AssetService(dio);
  20. final results = await Future.wait([
  21. service.getStatementCoins(),
  22. service.getStatementTypes(),
  23. ]);
  24. state = (
  25. coins: results[0] as List<StatementCoin>,
  26. types: results[1] as List<StatementType>,
  27. );
  28. } catch (_) {}
  29. }
  30. /// 强制刷新(下拉刷新时调用)
  31. Future<void> reload() => _load();
  32. }
  33. final statementFilterCacheProvider =
  34. NotifierProvider<_StatementFilterCache, ({List<StatementCoin> coins, List<StatementType> types})>(
  35. _StatementFilterCache.new,
  36. );
  37. // ══════════════════════════════════════════════════════════════
  38. // Statement State
  39. // ══════════════════════════════════════════════════════════════
  40. class StatementState {
  41. /// 流水记录
  42. final List<AssetStatement> records;
  43. // ── 当前筛选条件(UI 选择,点搜索后才生效)──
  44. final String selectedCoinCode;
  45. final String selectedTypeId;
  46. final DateTime? startDate;
  47. final DateTime? endDate;
  48. final bool isLoading;
  49. final bool hasMore;
  50. final int currentPage;
  51. final String? errorMessage;
  52. const StatementState({
  53. this.records = const [],
  54. this.selectedCoinCode = '',
  55. this.selectedTypeId = '',
  56. this.startDate,
  57. this.endDate,
  58. this.isLoading = false,
  59. this.hasMore = true,
  60. this.currentPage = 1,
  61. this.errorMessage,
  62. });
  63. StatementState copyWith({
  64. List<AssetStatement>? records,
  65. String? selectedCoinCode,
  66. String? selectedTypeId,
  67. DateTime? startDate,
  68. DateTime? endDate,
  69. bool clearStartDate = false,
  70. bool clearEndDate = false,
  71. bool? isLoading,
  72. bool? hasMore,
  73. int? currentPage,
  74. String? errorMessage,
  75. }) =>
  76. StatementState(
  77. records: records ?? this.records,
  78. selectedCoinCode: selectedCoinCode ?? this.selectedCoinCode,
  79. selectedTypeId: selectedTypeId ?? this.selectedTypeId,
  80. startDate: clearStartDate ? null : (startDate ?? this.startDate),
  81. endDate: clearEndDate ? null : (endDate ?? this.endDate),
  82. isLoading: isLoading ?? this.isLoading,
  83. hasMore: hasMore ?? this.hasMore,
  84. currentPage: currentPage ?? this.currentPage,
  85. errorMessage: errorMessage,
  86. );
  87. }
  88. // ══════════════════════════════════════════════════════════════
  89. // Statement Notifier
  90. // ══════════════════════════════════════════════════════════════
  91. class StatementNotifier extends AutoDisposeNotifier<StatementState> {
  92. static const _pageSize = 10;
  93. static final _dateFmt = DateFormat('yyyy-MM-dd HH:mm:ss');
  94. // 搜索时才生效的筛选参数
  95. String _searchCoin = '';
  96. String _searchType = '';
  97. String _searchStart = '';
  98. String _searchEnd = '';
  99. @override
  100. StatementState build() {
  101. // 只触发缓存加载,不建立 rebuild 依赖(避免缓存更新时重置用户选择)
  102. ref.read(statementFilterCacheProvider.notifier);
  103. Future.microtask(_loadRecords);
  104. return const StatementState(isLoading: true);
  105. }
  106. /// 只加载记录(币种/类型从缓存读取)
  107. Future<void> _loadRecords() async {
  108. state = state.copyWith(isLoading: true, errorMessage: null);
  109. try {
  110. final dio = ref.read(dioClientProvider);
  111. final records = await AssetService(dio).getStatementList(
  112. pageNo: 1,
  113. pageSize: _pageSize,
  114. );
  115. state = state.copyWith(
  116. records: records,
  117. isLoading: false,
  118. currentPage: 1,
  119. hasMore: records.length >= _pageSize,
  120. );
  121. } catch (e) {
  122. state = state.copyWith(isLoading: false, errorMessage: e.toString());
  123. }
  124. }
  125. // ── 筛选条件选择(仅更新 UI,不请求)──
  126. void selectCoin(String code) {
  127. state = state.copyWith(selectedCoinCode: code);
  128. }
  129. void selectType(String typeId) {
  130. state = state.copyWith(selectedTypeId: typeId);
  131. }
  132. void selectStartDate(DateTime date) {
  133. if (state.endDate != null && date.isAfter(state.endDate!)) return;
  134. state = state.copyWith(startDate: date);
  135. }
  136. void selectEndDate(DateTime date) {
  137. if (state.startDate != null && state.startDate!.isAfter(date)) return;
  138. state = state.copyWith(endDate: date);
  139. }
  140. // ── 搜索(收集筛选条件,重新请求)──
  141. Future<void> search() async {
  142. if (state.startDate != null && state.endDate == null) {
  143. state = state.copyWith(errorMessage: 'errEnterEndTime');
  144. return;
  145. }
  146. if (state.endDate != null && state.startDate == null) {
  147. state = state.copyWith(errorMessage: 'errEnterStartTime');
  148. return;
  149. }
  150. _searchCoin = state.selectedCoinCode;
  151. _searchType = state.selectedTypeId;
  152. _searchStart = state.startDate != null ? _dateFmt.format(state.startDate!) : '';
  153. _searchEnd = state.endDate != null
  154. ? _dateFmt.format(DateTime(state.endDate!.year, state.endDate!.month, state.endDate!.day, 23, 59, 59))
  155. : '';
  156. state = state.copyWith(isLoading: true, errorMessage: null);
  157. try {
  158. final dio = ref.read(dioClientProvider);
  159. final records = await AssetService(dio).getStatementList(
  160. type: _searchType,
  161. symbol: _searchCoin,
  162. startTime: _searchStart,
  163. endTime: _searchEnd,
  164. pageNo: 1,
  165. pageSize: _pageSize,
  166. );
  167. state = state.copyWith(
  168. records: records,
  169. isLoading: false,
  170. currentPage: 1,
  171. hasMore: records.length >= _pageSize,
  172. );
  173. } catch (e) {
  174. state = state.copyWith(isLoading: false, errorMessage: e.toString());
  175. }
  176. }
  177. // ── 重置 ──
  178. void reset() {
  179. _searchCoin = '';
  180. _searchType = '';
  181. _searchStart = '';
  182. _searchEnd = '';
  183. state = state.copyWith(
  184. selectedCoinCode: '',
  185. selectedTypeId: '',
  186. clearStartDate: true,
  187. clearEndDate: true,
  188. );
  189. search();
  190. }
  191. // ── 下拉刷新(同时刷新缓存)──
  192. Future<void> refresh() async {
  193. ref.read(statementFilterCacheProvider.notifier).reload();
  194. await search();
  195. }
  196. // ── 上拉加载更多 ──
  197. Future<void> loadMore() async {
  198. if (state.isLoading || !state.hasMore) return;
  199. state = state.copyWith(isLoading: true);
  200. try {
  201. final nextPage = state.currentPage + 1;
  202. final dio = ref.read(dioClientProvider);
  203. final records = await AssetService(dio).getStatementList(
  204. type: _searchType,
  205. symbol: _searchCoin,
  206. startTime: _searchStart,
  207. endTime: _searchEnd,
  208. pageNo: nextPage,
  209. pageSize: _pageSize,
  210. );
  211. state = state.copyWith(
  212. records: [...state.records, ...records],
  213. isLoading: false,
  214. currentPage: nextPage,
  215. hasMore: records.length >= _pageSize,
  216. );
  217. } catch (e) {
  218. state = state.copyWith(isLoading: false, errorMessage: e.toString());
  219. }
  220. }
  221. }
  222. final statementProvider =
  223. AutoDisposeNotifierProvider<StatementNotifier, StatementState>(
  224. StatementNotifier.new,
  225. );