feat(mining-app): 添加补发记录功能
- 添加批量补发记录 API endpoint - 创建 BatchMiningRecord 实体和模型 - 添加批量补发记录 provider 和页面 - 在"我的"页面动态显示补发记录入口(仅当用户有记录时显示) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
134e45e0bf
commit
d5dc248a16
|
|
@ -18,6 +18,8 @@ class ApiEndpoints {
|
||||||
'/api/v2/mining/accounts/$accountSequence';
|
'/api/v2/mining/accounts/$accountSequence';
|
||||||
static String miningRecords(String accountSequence) =>
|
static String miningRecords(String accountSequence) =>
|
||||||
'/api/v2/mining/accounts/$accountSequence/records';
|
'/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 const String globalState = '/api/v2/mining/global-state';
|
||||||
static String realtimeEarning(String accountSequence) =>
|
static String realtimeEarning(String accountSequence) =>
|
||||||
'/api/v2/mining/accounts/$accountSequence/realtime';
|
'/api/v2/mining/accounts/$accountSequence/realtime';
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import '../../presentation/pages/asset/asset_page.dart';
|
||||||
import '../../presentation/pages/profile/profile_page.dart';
|
import '../../presentation/pages/profile/profile_page.dart';
|
||||||
import '../../presentation/pages/profile/edit_profile_page.dart';
|
import '../../presentation/pages/profile/edit_profile_page.dart';
|
||||||
import '../../presentation/pages/profile/mining_records_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/profile/planting_records_page.dart';
|
||||||
import '../../presentation/pages/asset/send_shares_page.dart';
|
import '../../presentation/pages/asset/send_shares_page.dart';
|
||||||
import '../../presentation/pages/asset/receive_shares_page.dart';
|
import '../../presentation/pages/asset/receive_shares_page.dart';
|
||||||
|
|
@ -119,6 +120,10 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||||
path: Routes.miningRecords,
|
path: Routes.miningRecords,
|
||||||
builder: (context, state) => const MiningRecordsListPage(),
|
builder: (context, state) => const MiningRecordsListPage(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.batchMiningRecords,
|
||||||
|
builder: (context, state) => const BatchMiningRecordsPage(),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: Routes.plantingRecords,
|
path: Routes.plantingRecords,
|
||||||
builder: (context, state) => const PlantingRecordsPage(),
|
builder: (context, state) => const PlantingRecordsPage(),
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ class Routes {
|
||||||
static const String profile = '/profile';
|
static const String profile = '/profile';
|
||||||
static const String editProfile = '/edit-profile';
|
static const String editProfile = '/edit-profile';
|
||||||
static const String miningRecords = '/mining-records';
|
static const String miningRecords = '/mining-records';
|
||||||
|
static const String batchMiningRecords = '/batch-mining-records';
|
||||||
static const String contributionRecords = '/contribution-records';
|
static const String contributionRecords = '/contribution-records';
|
||||||
static const String plantingRecords = '/planting-records';
|
static const String plantingRecords = '/planting-records';
|
||||||
static const String orders = '/orders';
|
static const String orders = '/orders';
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@ import '../../models/share_account_model.dart';
|
||||||
import '../../models/mining_record_model.dart';
|
import '../../models/mining_record_model.dart';
|
||||||
import '../../models/global_state_model.dart';
|
import '../../models/global_state_model.dart';
|
||||||
import '../../models/planting_record_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_client.dart';
|
||||||
import '../../../core/network/api_endpoints.dart';
|
import '../../../core/network/api_endpoints.dart';
|
||||||
import '../../../core/error/exceptions.dart';
|
import '../../../core/error/exceptions.dart';
|
||||||
import '../../../domain/repositories/mining_repository.dart';
|
import '../../../domain/repositories/mining_repository.dart';
|
||||||
|
import '../../../domain/entities/batch_mining_record.dart';
|
||||||
|
|
||||||
abstract class MiningRemoteDataSource {
|
abstract class MiningRemoteDataSource {
|
||||||
Future<ShareAccountModel> getShareAccount(String accountSequence);
|
Future<ShareAccountModel> getShareAccount(String accountSequence);
|
||||||
|
|
@ -20,6 +22,11 @@ abstract class MiningRemoteDataSource {
|
||||||
int page = 1,
|
int page = 1,
|
||||||
int pageSize = 10,
|
int pageSize = 10,
|
||||||
});
|
});
|
||||||
|
Future<BatchMiningRecordsPage> getBatchMiningRecords(
|
||||||
|
String accountSequence, {
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 20,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class MiningRemoteDataSourceImpl implements MiningRemoteDataSource {
|
class MiningRemoteDataSourceImpl implements MiningRemoteDataSource {
|
||||||
|
|
@ -99,4 +106,21 @@ class MiningRemoteDataSourceImpl implements MiningRemoteDataSource {
|
||||||
throw ServerException(e.toString());
|
throw ServerException(e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<BatchMiningRecordsPage> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
final total = json['total'] as int? ?? items.length;
|
||||||
|
return BatchMiningRecordsPageModel(
|
||||||
|
items: items,
|
||||||
|
total: total,
|
||||||
|
page: page,
|
||||||
|
pageSize: pageSize,
|
||||||
|
totalPages: (total / pageSize).ceil(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:dartz/dartz.dart';
|
||||||
import '../../domain/entities/share_account.dart';
|
import '../../domain/entities/share_account.dart';
|
||||||
import '../../domain/entities/global_state.dart';
|
import '../../domain/entities/global_state.dart';
|
||||||
import '../../domain/entities/planting_record.dart';
|
import '../../domain/entities/planting_record.dart';
|
||||||
|
import '../../domain/entities/batch_mining_record.dart';
|
||||||
import '../../domain/repositories/mining_repository.dart';
|
import '../../domain/repositories/mining_repository.dart';
|
||||||
import '../../core/error/exceptions.dart';
|
import '../../core/error/exceptions.dart';
|
||||||
import '../../core/error/failures.dart';
|
import '../../core/error/failures.dart';
|
||||||
|
|
@ -90,4 +91,24 @@ class MiningRepositoryImpl implements MiningRepository {
|
||||||
return Left(const NetworkFailure());
|
return Left(const NetworkFailure());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, BatchMiningRecordsPage>> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<Object?> get props => [
|
||||||
|
id,
|
||||||
|
accountSequence,
|
||||||
|
batch,
|
||||||
|
phase,
|
||||||
|
treeCount,
|
||||||
|
daysInPhase,
|
||||||
|
preMineDays,
|
||||||
|
amount,
|
||||||
|
remark,
|
||||||
|
createdAt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 批量补发记录分页数据
|
||||||
|
class BatchMiningRecordsPage {
|
||||||
|
final List<BatchMiningRecord> 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import '../entities/share_account.dart';
|
||||||
import '../entities/mining_record.dart';
|
import '../entities/mining_record.dart';
|
||||||
import '../entities/global_state.dart';
|
import '../entities/global_state.dart';
|
||||||
import '../entities/planting_record.dart';
|
import '../entities/planting_record.dart';
|
||||||
|
import '../entities/batch_mining_record.dart';
|
||||||
|
|
||||||
/// 挖矿记录分页数据
|
/// 挖矿记录分页数据
|
||||||
class MiningRecordsPage {
|
class MiningRecordsPage {
|
||||||
|
|
@ -38,4 +39,10 @@ abstract class MiningRepository {
|
||||||
int page = 1,
|
int page = 1,
|
||||||
int pageSize = 10,
|
int pageSize = 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Future<Either<Failure, BatchMiningRecordsPage>> getBatchMiningRecords(
|
||||||
|
String accountSequence, {
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 20,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<BatchMiningRecordsPage> createState() => _BatchMiningRecordsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BatchMiningRecordsPageState extends ConsumerState<BatchMiningRecordsPage> {
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import '../../../core/router/routes.dart';
|
||||||
import '../../providers/user_providers.dart';
|
import '../../providers/user_providers.dart';
|
||||||
import '../../providers/profile_providers.dart';
|
import '../../providers/profile_providers.dart';
|
||||||
import '../../providers/settings_providers.dart';
|
import '../../providers/settings_providers.dart';
|
||||||
|
import '../../providers/mining_providers.dart';
|
||||||
import '../../widgets/shimmer_loading.dart';
|
import '../../widgets/shimmer_loading.dart';
|
||||||
|
|
||||||
class ProfilePage extends ConsumerWidget {
|
class ProfilePage extends ConsumerWidget {
|
||||||
|
|
@ -89,7 +90,7 @@ class ProfilePage extends ConsumerWidget {
|
||||||
// const SizedBox(height: 16),
|
// const SizedBox(height: 16),
|
||||||
|
|
||||||
// 记录入口
|
// 记录入口
|
||||||
_buildRecordsSection(context),
|
_buildRecordsSection(context, ref, user.accountSequence ?? ''),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
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 = <Widget>[
|
||||||
|
_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(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
|
|
@ -400,29 +439,19 @@ class ProfilePage extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
// 使用 Wrap 或根据数量动态布局
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
if (recordIcons.length <= 4)
|
||||||
children: [
|
Row(
|
||||||
_buildRecordIcon(
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
context: context,
|
children: recordIcons,
|
||||||
icon: Icons.eco,
|
)
|
||||||
label: '参与记录',
|
else
|
||||||
onTap: () => context.push(Routes.plantingRecords),
|
Wrap(
|
||||||
),
|
alignment: WrapAlignment.spaceAround,
|
||||||
_buildRecordIcon(
|
spacing: 16,
|
||||||
context: context,
|
runSpacing: 16,
|
||||||
icon: Icons.assignment,
|
children: recordIcons,
|
||||||
label: '分配记录',
|
),
|
||||||
onTap: () => context.push(Routes.miningRecords),
|
|
||||||
),
|
|
||||||
_buildRecordIcon(
|
|
||||||
context: context,
|
|
||||||
icon: Icons.receipt_long,
|
|
||||||
label: '交易记录',
|
|
||||||
onTap: () => context.push(Routes.tradingRecords),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../domain/entities/share_account.dart';
|
import '../../domain/entities/share_account.dart';
|
||||||
import '../../domain/entities/global_state.dart';
|
import '../../domain/entities/global_state.dart';
|
||||||
import '../../domain/entities/planting_record.dart';
|
import '../../domain/entities/planting_record.dart';
|
||||||
|
import '../../domain/entities/batch_mining_record.dart';
|
||||||
import '../../domain/repositories/mining_repository.dart';
|
import '../../domain/repositories/mining_repository.dart';
|
||||||
import '../../domain/usecases/mining/get_share_account.dart';
|
import '../../domain/usecases/mining/get_share_account.dart';
|
||||||
import '../../domain/usecases/mining/get_global_state.dart';
|
import '../../domain/usecases/mining/get_global_state.dart';
|
||||||
|
|
@ -177,3 +178,82 @@ final plantingRecordsProvider = FutureProvider.family<PlantingLedgerPage?, Plant
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// 批量补发记录请求参数
|
||||||
|
class BatchMiningRecordsParams {
|
||||||
|
final String accountSequence;
|
||||||
|
final int page;
|
||||||
|
final int pageSize;
|
||||||
|
|
||||||
|
const BatchMiningRecordsParams({
|
||||||
|
required this.accountSequence,
|
||||||
|
this.page = 1,
|
||||||
|
this.pageSize = 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
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<BatchMiningRecordsPage?, BatchMiningRecordsParams>(
|
||||||
|
(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<bool, String>(
|
||||||
|
(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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue