From d5dc248a16e56b884ef5e7377808367d1e48b892 Mon Sep 17 00:00:00 2001 From: hailin Date: Thu, 22 Jan 2026 01:35:53 -0800 Subject: [PATCH] =?UTF-8?q?feat(mining-app):=20=E6=B7=BB=E5=8A=A0=E8=A1=A5?= =?UTF-8?q?=E5=8F=91=E8=AE=B0=E5=BD=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加批量补发记录 API endpoint - 创建 BatchMiningRecord 实体和模型 - 添加批量补发记录 provider 和页面 - 在"我的"页面动态显示补发记录入口(仅当用户有记录时显示) Co-Authored-By: Claude Opus 4.5 --- .../lib/core/network/api_endpoints.dart | 2 + .../lib/core/router/app_router.dart | 5 + .../mining-app/lib/core/router/routes.dart | 1 + .../remote/mining_remote_datasource.dart | 24 ++ .../models/batch_mining_record_model.dart | 62 +++ .../repositories/mining_repository_impl.dart | 21 + .../domain/entities/batch_mining_record.dart | 59 +++ .../repositories/mining_repository.dart | 7 + .../profile/batch_mining_records_page.dart | 383 ++++++++++++++++++ .../pages/profile/profile_page.dart | 79 ++-- .../providers/mining_providers.dart | 80 ++++ 11 files changed, 698 insertions(+), 25 deletions(-) create mode 100644 frontend/mining-app/lib/data/models/batch_mining_record_model.dart create mode 100644 frontend/mining-app/lib/domain/entities/batch_mining_record.dart create mode 100644 frontend/mining-app/lib/presentation/pages/profile/batch_mining_records_page.dart diff --git a/frontend/mining-app/lib/core/network/api_endpoints.dart b/frontend/mining-app/lib/core/network/api_endpoints.dart index 598c84d7..c907e3f7 100644 --- a/frontend/mining-app/lib/core/network/api_endpoints.dart +++ b/frontend/mining-app/lib/core/network/api_endpoints.dart @@ -18,6 +18,8 @@ class ApiEndpoints { '/api/v2/mining/accounts/$accountSequence'; static String miningRecords(String accountSequence) => '/api/v2/mining/accounts/$accountSequence/records'; + static String batchMiningRecords(String accountSequence) => + '/api/v2/mining/batch-mining/records/$accountSequence'; static const String globalState = '/api/v2/mining/global-state'; static String realtimeEarning(String accountSequence) => '/api/v2/mining/accounts/$accountSequence/realtime'; diff --git a/frontend/mining-app/lib/core/router/app_router.dart b/frontend/mining-app/lib/core/router/app_router.dart index e7076c5c..2333579c 100644 --- a/frontend/mining-app/lib/core/router/app_router.dart +++ b/frontend/mining-app/lib/core/router/app_router.dart @@ -13,6 +13,7 @@ import '../../presentation/pages/asset/asset_page.dart'; import '../../presentation/pages/profile/profile_page.dart'; import '../../presentation/pages/profile/edit_profile_page.dart'; import '../../presentation/pages/profile/mining_records_page.dart'; +import '../../presentation/pages/profile/batch_mining_records_page.dart'; import '../../presentation/pages/profile/planting_records_page.dart'; import '../../presentation/pages/asset/send_shares_page.dart'; import '../../presentation/pages/asset/receive_shares_page.dart'; @@ -119,6 +120,10 @@ final appRouterProvider = Provider((ref) { path: Routes.miningRecords, builder: (context, state) => const MiningRecordsListPage(), ), + GoRoute( + path: Routes.batchMiningRecords, + builder: (context, state) => const BatchMiningRecordsPage(), + ), GoRoute( path: Routes.plantingRecords, builder: (context, state) => const PlantingRecordsPage(), diff --git a/frontend/mining-app/lib/core/router/routes.dart b/frontend/mining-app/lib/core/router/routes.dart index e1e81113..00e054d7 100644 --- a/frontend/mining-app/lib/core/router/routes.dart +++ b/frontend/mining-app/lib/core/router/routes.dart @@ -10,6 +10,7 @@ class Routes { static const String profile = '/profile'; static const String editProfile = '/edit-profile'; static const String miningRecords = '/mining-records'; + static const String batchMiningRecords = '/batch-mining-records'; static const String contributionRecords = '/contribution-records'; static const String plantingRecords = '/planting-records'; static const String orders = '/orders'; diff --git a/frontend/mining-app/lib/data/datasources/remote/mining_remote_datasource.dart b/frontend/mining-app/lib/data/datasources/remote/mining_remote_datasource.dart index 40c8c8a5..4ad18dcb 100644 --- a/frontend/mining-app/lib/data/datasources/remote/mining_remote_datasource.dart +++ b/frontend/mining-app/lib/data/datasources/remote/mining_remote_datasource.dart @@ -2,10 +2,12 @@ import '../../models/share_account_model.dart'; import '../../models/mining_record_model.dart'; import '../../models/global_state_model.dart'; import '../../models/planting_record_model.dart'; +import '../../models/batch_mining_record_model.dart'; import '../../../core/network/api_client.dart'; import '../../../core/network/api_endpoints.dart'; import '../../../core/error/exceptions.dart'; import '../../../domain/repositories/mining_repository.dart'; +import '../../../domain/entities/batch_mining_record.dart'; abstract class MiningRemoteDataSource { Future getShareAccount(String accountSequence); @@ -20,6 +22,11 @@ abstract class MiningRemoteDataSource { int page = 1, int pageSize = 10, }); + Future getBatchMiningRecords( + String accountSequence, { + int page = 1, + int pageSize = 20, + }); } class MiningRemoteDataSourceImpl implements MiningRemoteDataSource { @@ -99,4 +106,21 @@ class MiningRemoteDataSourceImpl implements MiningRemoteDataSource { throw ServerException(e.toString()); } } + + @override + Future getBatchMiningRecords( + String accountSequence, { + int page = 1, + int pageSize = 20, + }) async { + try { + final response = await client.get( + ApiEndpoints.batchMiningRecords(accountSequence), + queryParameters: {'page': page, 'pageSize': pageSize}, + ); + return BatchMiningRecordsPageModel.fromJson(response.data, page, pageSize); + } catch (e) { + throw ServerException(e.toString()); + } + } } diff --git a/frontend/mining-app/lib/data/models/batch_mining_record_model.dart b/frontend/mining-app/lib/data/models/batch_mining_record_model.dart new file mode 100644 index 00000000..03d82c42 --- /dev/null +++ b/frontend/mining-app/lib/data/models/batch_mining_record_model.dart @@ -0,0 +1,62 @@ +import '../../domain/entities/batch_mining_record.dart'; + +class BatchMiningRecordModel extends BatchMiningRecord { + const BatchMiningRecordModel({ + required super.id, + required super.accountSequence, + required super.batch, + required super.phase, + required super.treeCount, + required super.daysInPhase, + required super.preMineDays, + required super.amount, + super.remark, + required super.createdAt, + }); + + factory BatchMiningRecordModel.fromJson(Map json) { + return BatchMiningRecordModel( + id: json['id']?.toString() ?? '', + accountSequence: json['accountSequence']?.toString() ?? '', + batch: json['batch'] as int? ?? 0, + phase: json['phase'] as int? ?? 0, + treeCount: json['treeCount'] as int? ?? 0, + daysInPhase: json['daysInPhase'] as int? ?? 0, + preMineDays: json['preMineDays'] as int? ?? 0, + amount: json['amount']?.toString() ?? '0', + remark: json['remark']?.toString(), + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt'].toString()) + : DateTime.now(), + ); + } +} + +class BatchMiningRecordsPageModel extends BatchMiningRecordsPage { + const BatchMiningRecordsPageModel({ + required super.items, + required super.total, + required super.page, + required super.pageSize, + required super.totalPages, + }); + + factory BatchMiningRecordsPageModel.fromJson( + Map json, + int page, + int pageSize, + ) { + final rawItems = json['data'] as List? ?? json['items'] as List? ?? []; + final items = rawItems + .map((item) => BatchMiningRecordModel.fromJson(item as Map)) + .toList(); + final total = json['total'] as int? ?? items.length; + return BatchMiningRecordsPageModel( + items: items, + total: total, + page: page, + pageSize: pageSize, + totalPages: (total / pageSize).ceil(), + ); + } +} diff --git a/frontend/mining-app/lib/data/repositories/mining_repository_impl.dart b/frontend/mining-app/lib/data/repositories/mining_repository_impl.dart index ea7b4b61..342d45f3 100644 --- a/frontend/mining-app/lib/data/repositories/mining_repository_impl.dart +++ b/frontend/mining-app/lib/data/repositories/mining_repository_impl.dart @@ -2,6 +2,7 @@ import 'package:dartz/dartz.dart'; import '../../domain/entities/share_account.dart'; import '../../domain/entities/global_state.dart'; import '../../domain/entities/planting_record.dart'; +import '../../domain/entities/batch_mining_record.dart'; import '../../domain/repositories/mining_repository.dart'; import '../../core/error/exceptions.dart'; import '../../core/error/failures.dart'; @@ -90,4 +91,24 @@ class MiningRepositoryImpl implements MiningRepository { return Left(const NetworkFailure()); } } + + @override + Future> getBatchMiningRecords( + String accountSequence, { + int page = 1, + int pageSize = 20, + }) async { + try { + final result = await remoteDataSource.getBatchMiningRecords( + accountSequence, + page: page, + pageSize: pageSize, + ); + return Right(result); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } on NetworkException { + return Left(const NetworkFailure()); + } + } } diff --git a/frontend/mining-app/lib/domain/entities/batch_mining_record.dart b/frontend/mining-app/lib/domain/entities/batch_mining_record.dart new file mode 100644 index 00000000..d29bca9d --- /dev/null +++ b/frontend/mining-app/lib/domain/entities/batch_mining_record.dart @@ -0,0 +1,59 @@ +import 'package:equatable/equatable.dart'; + +/// 批量补发记录 +class BatchMiningRecord extends Equatable { + final String id; + final String accountSequence; + final int batch; + final int phase; + final int treeCount; + final int daysInPhase; + final int preMineDays; + final String amount; + final String? remark; + final DateTime createdAt; + + const BatchMiningRecord({ + required this.id, + required this.accountSequence, + required this.batch, + required this.phase, + required this.treeCount, + required this.daysInPhase, + required this.preMineDays, + required this.amount, + this.remark, + required this.createdAt, + }); + + @override + List get props => [ + id, + accountSequence, + batch, + phase, + treeCount, + daysInPhase, + preMineDays, + amount, + remark, + createdAt, + ]; +} + +/// 批量补发记录分页数据 +class BatchMiningRecordsPage { + final List items; + final int total; + final int page; + final int pageSize; + final int totalPages; + + const BatchMiningRecordsPage({ + required this.items, + required this.total, + required this.page, + required this.pageSize, + required this.totalPages, + }); +} diff --git a/frontend/mining-app/lib/domain/repositories/mining_repository.dart b/frontend/mining-app/lib/domain/repositories/mining_repository.dart index 36ed2534..7e24ba35 100644 --- a/frontend/mining-app/lib/domain/repositories/mining_repository.dart +++ b/frontend/mining-app/lib/domain/repositories/mining_repository.dart @@ -4,6 +4,7 @@ import '../entities/share_account.dart'; import '../entities/mining_record.dart'; import '../entities/global_state.dart'; import '../entities/planting_record.dart'; +import '../entities/batch_mining_record.dart'; /// 挖矿记录分页数据 class MiningRecordsPage { @@ -38,4 +39,10 @@ abstract class MiningRepository { int page = 1, int pageSize = 10, }); + + Future> getBatchMiningRecords( + String accountSequence, { + int page = 1, + int pageSize = 20, + }); } diff --git a/frontend/mining-app/lib/presentation/pages/profile/batch_mining_records_page.dart b/frontend/mining-app/lib/presentation/pages/profile/batch_mining_records_page.dart new file mode 100644 index 00000000..822c0b47 --- /dev/null +++ b/frontend/mining-app/lib/presentation/pages/profile/batch_mining_records_page.dart @@ -0,0 +1,383 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import '../../../core/constants/app_colors.dart'; +import '../../../core/utils/format_utils.dart'; +import '../../../domain/entities/batch_mining_record.dart'; +import '../../providers/user_providers.dart'; +import '../../providers/mining_providers.dart'; + +/// 补发记录页面 +class BatchMiningRecordsPage extends ConsumerStatefulWidget { + const BatchMiningRecordsPage({super.key}); + + @override + ConsumerState createState() => _BatchMiningRecordsPageState(); +} + +class _BatchMiningRecordsPageState extends ConsumerState { + static const Color _orange = Color(0xFFFF6B00); + static const Color _green = Color(0xFF22C55E); + static const Color _grayText = Color(0xFF6B7280); + static const Color _darkText = Color(0xFF1F2937); + + int _currentPage = 1; + static const int _pageSize = 20; + + @override + Widget build(BuildContext context) { + final user = ref.watch(userNotifierProvider); + final accountSequence = user.accountSequence ?? ''; + + final recordsParams = BatchMiningRecordsParams( + accountSequence: accountSequence, + page: _currentPage, + pageSize: _pageSize, + ); + final recordsAsync = ref.watch(batchMiningRecordsProvider(recordsParams)); + + return Scaffold( + backgroundColor: const Color(0xFFF5F5F5), + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: _darkText, size: 20), + onPressed: () => Navigator.of(context).pop(), + ), + title: const Text( + '补发记录', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: _darkText, + ), + ), + centerTitle: true, + ), + body: RefreshIndicator( + onRefresh: () async { + setState(() => _currentPage = 1); + ref.invalidate(batchMiningRecordsProvider(recordsParams)); + }, + child: recordsAsync.when( + loading: () => _buildLoadingList(), + error: (error, stack) => _buildErrorView(error.toString(), recordsParams), + data: (recordsPage) { + if (recordsPage == null || recordsPage.items.isEmpty) { + return _buildEmptyView(); + } + return _buildRecordsList(recordsPage); + }, + ), + ), + ); + } + + Widget _buildLoadingList() { + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: 10, + itemBuilder: (context, index) => _buildShimmerCard(), + ); + } + + Widget _buildShimmerCard() { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: 120, + height: 16, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(4), + ), + ), + Container( + width: 80, + height: 20, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Container( + width: 60, + height: 12, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(width: 16), + Container( + width: 80, + height: 12, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildErrorView(String error, BatchMiningRecordsParams params) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, size: 48, color: AppColors.error), + const SizedBox(height: 16), + Text('加载失败', style: TextStyle(fontSize: 16, color: _grayText)), + const SizedBox(height: 8), + Text(error, style: TextStyle(fontSize: 12, color: _grayText)), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + ref.invalidate(batchMiningRecordsProvider(params)); + }, + style: ElevatedButton.styleFrom(backgroundColor: _orange), + child: const Text('重试', style: TextStyle(color: Colors.white)), + ), + ], + ), + ); + } + + Widget _buildEmptyView() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.inbox_outlined, size: 64, color: _grayText.withOpacity(0.5)), + const SizedBox(height: 16), + Text( + '暂无补发记录', + style: TextStyle(fontSize: 16, color: _grayText), + ), + ], + ), + ); + } + + Widget _buildRecordsList(BatchMiningRecordsPage recordsPage) { + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: recordsPage.items.length + 1, + itemBuilder: (context, index) { + if (index == recordsPage.items.length) { + return _buildPaginationInfo(recordsPage); + } + return _buildRecordCard(recordsPage.items[index]); + }, + ); + } + + Widget _buildRecordCard(BatchMiningRecord record) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 第一行:批次/阶段信息 + 补发金额 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: _orange.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '第${record.batch}批', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: _orange, + ), + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '阶段${record.phase}', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.blue, + ), + ), + ), + ], + ), + Text( + '+${formatAmount(record.amount)}', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: _green, + ), + ), + ], + ), + const SizedBox(height: 12), + + // 第二行:认种量 + 阶段天数 + Row( + children: [ + Expanded( + child: _buildInfoItem('认种量', '${record.treeCount} 棵'), + ), + const SizedBox(width: 16), + Expanded( + child: _buildInfoItem('阶段天数', '${record.daysInPhase} 天'), + ), + ], + ), + const SizedBox(height: 8), + + // 第三行:提前天数 + Row( + children: [ + Expanded( + child: _buildInfoItem('提前天数', '${record.preMineDays} 天'), + ), + const SizedBox(width: 16), + const Expanded(child: SizedBox()), + ], + ), + + // 备注(如果有) + if (record.remark != null && record.remark!.isNotEmpty) ...[ + const SizedBox(height: 8), + Row( + children: [ + Icon(Icons.note_outlined, size: 12, color: _grayText.withOpacity(0.7)), + const SizedBox(width: 4), + Expanded( + child: Text( + record.remark!, + style: TextStyle(fontSize: 11, color: _grayText.withOpacity(0.7)), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + + const SizedBox(height: 8), + // 记录时间 + Row( + children: [ + Icon(Icons.access_time, size: 12, color: _grayText.withOpacity(0.7)), + const SizedBox(width: 4), + Text( + DateFormat('yyyy-MM-dd HH:mm:ss').format(record.createdAt), + style: TextStyle(fontSize: 11, color: _grayText.withOpacity(0.7)), + ), + ], + ), + ], + ), + ); + } + + Widget _buildInfoItem(String label, String value) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '$label: ', + style: TextStyle(fontSize: 12, color: _grayText.withOpacity(0.7)), + ), + Flexible( + child: Text( + value, + style: const TextStyle( + fontSize: 12, + color: _darkText, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } + + Widget _buildPaginationInfo(BatchMiningRecordsPage recordsPage) { + final totalPages = recordsPage.totalPages > 0 + ? recordsPage.totalPages + : (recordsPage.total / _pageSize).ceil(); + + return Container( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + children: [ + Text( + '共 ${recordsPage.total} 条记录,第 $_currentPage / $totalPages 页', + style: TextStyle(fontSize: 12, color: _grayText), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_currentPage > 1) + TextButton.icon( + onPressed: () { + setState(() => _currentPage--); + }, + icon: const Icon(Icons.chevron_left, size: 18), + label: const Text('上一页'), + style: TextButton.styleFrom(foregroundColor: _orange), + ), + const SizedBox(width: 16), + if (_currentPage < totalPages) + TextButton.icon( + onPressed: () { + setState(() => _currentPage++); + }, + icon: const Text('下一页'), + label: const Icon(Icons.chevron_right, size: 18), + style: TextButton.styleFrom(foregroundColor: _orange), + ), + ], + ), + ], + ), + ); + } +} diff --git a/frontend/mining-app/lib/presentation/pages/profile/profile_page.dart b/frontend/mining-app/lib/presentation/pages/profile/profile_page.dart index 5e65de5a..97d75031 100644 --- a/frontend/mining-app/lib/presentation/pages/profile/profile_page.dart +++ b/frontend/mining-app/lib/presentation/pages/profile/profile_page.dart @@ -5,6 +5,7 @@ import '../../../core/router/routes.dart'; import '../../providers/user_providers.dart'; import '../../providers/profile_providers.dart'; import '../../providers/settings_providers.dart'; +import '../../providers/mining_providers.dart'; import '../../widgets/shimmer_loading.dart'; class ProfilePage extends ConsumerWidget { @@ -89,7 +90,7 @@ class ProfilePage extends ConsumerWidget { // const SizedBox(height: 16), // 记录入口 - _buildRecordsSection(context), + _buildRecordsSection(context, ref, user.accountSequence ?? ''), const SizedBox(height: 16), @@ -380,7 +381,45 @@ class ProfilePage extends ConsumerWidget { ); } - Widget _buildRecordsSection(BuildContext context) { + Widget _buildRecordsSection(BuildContext context, WidgetRef ref, String accountSequence) { + // 检查是否有批量补发记录 + final hasBatchRecordsAsync = ref.watch(hasBatchMiningRecordsProvider(accountSequence)); + final hasBatchRecords = hasBatchRecordsAsync.valueOrNull ?? false; + + // 构建记录图标列表 + final recordIcons = [ + _buildRecordIcon( + context: context, + icon: Icons.eco, + label: '参与记录', + onTap: () => context.push(Routes.plantingRecords), + ), + _buildRecordIcon( + context: context, + icon: Icons.assignment, + label: '分配记录', + onTap: () => context.push(Routes.miningRecords), + ), + _buildRecordIcon( + context: context, + icon: Icons.receipt_long, + label: '交易记录', + onTap: () => context.push(Routes.tradingRecords), + ), + ]; + + // 如果有批量补发记录,添加补发记录入口 + if (hasBatchRecords) { + recordIcons.add( + _buildRecordIcon( + context: context, + icon: Icons.card_giftcard, + label: '补发记录', + onTap: () => context.push(Routes.batchMiningRecords), + ), + ); + } + return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(16), @@ -400,29 +439,19 @@ class ProfilePage extends ConsumerWidget { ), ), const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _buildRecordIcon( - context: context, - icon: Icons.eco, - label: '参与记录', - onTap: () => context.push(Routes.plantingRecords), - ), - _buildRecordIcon( - context: context, - icon: Icons.assignment, - label: '分配记录', - onTap: () => context.push(Routes.miningRecords), - ), - _buildRecordIcon( - context: context, - icon: Icons.receipt_long, - label: '交易记录', - onTap: () => context.push(Routes.tradingRecords), - ), - ], - ), + // 使用 Wrap 或根据数量动态布局 + if (recordIcons.length <= 4) + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: recordIcons, + ) + else + Wrap( + alignment: WrapAlignment.spaceAround, + spacing: 16, + runSpacing: 16, + children: recordIcons, + ), ], ), ); diff --git a/frontend/mining-app/lib/presentation/providers/mining_providers.dart b/frontend/mining-app/lib/presentation/providers/mining_providers.dart index 37624989..83b9071c 100644 --- a/frontend/mining-app/lib/presentation/providers/mining_providers.dart +++ b/frontend/mining-app/lib/presentation/providers/mining_providers.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../domain/entities/share_account.dart'; import '../../domain/entities/global_state.dart'; import '../../domain/entities/planting_record.dart'; +import '../../domain/entities/batch_mining_record.dart'; import '../../domain/repositories/mining_repository.dart'; import '../../domain/usecases/mining/get_share_account.dart'; import '../../domain/usecases/mining/get_global_state.dart'; @@ -177,3 +178,82 @@ final plantingRecordsProvider = FutureProvider.family + identical(this, other) || + other is BatchMiningRecordsParams && + runtimeType == other.runtimeType && + accountSequence == other.accountSequence && + page == other.page && + pageSize == other.pageSize; + + @override + int get hashCode => accountSequence.hashCode ^ page.hashCode ^ pageSize.hashCode; +} + +/// 批量补发记录 Provider +final batchMiningRecordsProvider = FutureProvider.family( + (ref, params) async { + if (params.accountSequence.isEmpty) { + return null; + } + + final repository = ref.watch(miningRepositoryProvider); + final result = await repository.getBatchMiningRecords( + params.accountSequence, + page: params.page, + pageSize: params.pageSize, + ); + + ref.keepAlive(); + final timer = Timer(const Duration(minutes: 5), () { + ref.invalidateSelf(); + }); + ref.onDispose(() => timer.cancel()); + + return result.fold( + (failure) => throw Exception(failure.message), + (records) => records, + ); + }, +); + +/// 检查用户是否有批量补发记录的 Provider +final hasBatchMiningRecordsProvider = FutureProvider.family( + (ref, accountSequence) async { + if (accountSequence.isEmpty) { + return false; + } + + final repository = ref.watch(miningRepositoryProvider); + final result = await repository.getBatchMiningRecords( + accountSequence, + page: 1, + pageSize: 1, + ); + + ref.keepAlive(); + final timer = Timer(const Duration(minutes: 10), () { + ref.invalidateSelf(); + }); + ref.onDispose(() => timer.cancel()); + + return result.fold( + (failure) => false, + (records) => records.total > 0, + ); + }, +);