feat(mining-app): add mining records and planting records pages
- Add mining records page showing distribution history with share amounts - Add planting records page with adoption summary and detailed records - Remove 推广奖励 and 收益明细 from profile page - Add planting-ledger API endpoint and data models Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b81ae634a6
commit
546c0060da
|
|
@ -55,4 +55,8 @@ class ApiEndpoints {
|
||||||
'/api/v2/contribution/accounts/$accountSequence';
|
'/api/v2/contribution/accounts/$accountSequence';
|
||||||
static String contributionRecords(String accountSequence) =>
|
static String contributionRecords(String accountSequence) =>
|
||||||
'/api/v2/contribution/accounts/$accountSequence/records';
|
'/api/v2/contribution/accounts/$accountSequence/records';
|
||||||
|
|
||||||
|
// User Service 2.0 (Kong路由: /api/v2/users)
|
||||||
|
static String plantingLedger(String accountSequence) =>
|
||||||
|
'/api/v2/users/$accountSequence/planting-ledger';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import '../../presentation/pages/contribution/contribution_records_page.dart';
|
||||||
import '../../presentation/pages/trading/trading_page.dart';
|
import '../../presentation/pages/trading/trading_page.dart';
|
||||||
import '../../presentation/pages/asset/asset_page.dart';
|
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/mining_records_page.dart';
|
||||||
|
import '../../presentation/pages/profile/planting_records_page.dart';
|
||||||
import '../../presentation/widgets/main_shell.dart';
|
import '../../presentation/widgets/main_shell.dart';
|
||||||
import '../../presentation/providers/user_providers.dart';
|
import '../../presentation/providers/user_providers.dart';
|
||||||
import 'routes.dart';
|
import 'routes.dart';
|
||||||
|
|
@ -102,6 +104,14 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||||
path: Routes.contributionRecords,
|
path: Routes.contributionRecords,
|
||||||
builder: (context, state) => const ContributionRecordsListPage(),
|
builder: (context, state) => const ContributionRecordsListPage(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.miningRecords,
|
||||||
|
builder: (context, state) => const MiningRecordsPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.plantingRecords,
|
||||||
|
builder: (context, state) => const PlantingRecordsPage(),
|
||||||
|
),
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
builder: (context, state, child) => MainShell(child: child),
|
builder: (context, state, child) => MainShell(child: child),
|
||||||
routes: [
|
routes: [
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,6 @@ class Routes {
|
||||||
static const String profile = '/profile';
|
static const String profile = '/profile';
|
||||||
static const String miningRecords = '/mining-records';
|
static const String miningRecords = '/mining-records';
|
||||||
static const String contributionRecords = '/contribution-records';
|
static const String contributionRecords = '/contribution-records';
|
||||||
|
static const String plantingRecords = '/planting-records';
|
||||||
static const String orders = '/orders';
|
static const String orders = '/orders';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,25 @@
|
||||||
import '../../models/share_account_model.dart';
|
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 '../../../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';
|
||||||
|
|
||||||
abstract class MiningRemoteDataSource {
|
abstract class MiningRemoteDataSource {
|
||||||
Future<ShareAccountModel> getShareAccount(String accountSequence);
|
Future<ShareAccountModel> getShareAccount(String accountSequence);
|
||||||
Future<List<MiningRecordModel>> getMiningRecords(
|
Future<MiningRecordsPage> getMiningRecords(
|
||||||
String accountSequence, {
|
String accountSequence, {
|
||||||
int page = 1,
|
int page = 1,
|
||||||
int limit = 20,
|
int pageSize = 20,
|
||||||
});
|
});
|
||||||
Future<GlobalStateModel> getGlobalState();
|
Future<GlobalStateModel> getGlobalState();
|
||||||
|
Future<PlantingLedgerPageModel> getPlantingLedger(
|
||||||
|
String accountSequence, {
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 10,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class MiningRemoteDataSourceImpl implements MiningRemoteDataSource {
|
class MiningRemoteDataSourceImpl implements MiningRemoteDataSource {
|
||||||
|
|
@ -31,18 +38,28 @@ class MiningRemoteDataSourceImpl implements MiningRemoteDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<MiningRecordModel>> getMiningRecords(
|
Future<MiningRecordsPage> getMiningRecords(
|
||||||
String accountSequence, {
|
String accountSequence, {
|
||||||
int page = 1,
|
int page = 1,
|
||||||
int limit = 20,
|
int pageSize = 20,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final response = await client.get(
|
final response = await client.get(
|
||||||
ApiEndpoints.miningRecords(accountSequence),
|
ApiEndpoints.miningRecords(accountSequence),
|
||||||
queryParameters: {'page': page, 'pageSize': limit},
|
queryParameters: {'page': page, 'pageSize': pageSize},
|
||||||
|
);
|
||||||
|
final data = response.data;
|
||||||
|
final items = (data['items'] as List? ?? data['records'] as List? ?? [])
|
||||||
|
.map((json) => MiningRecordModel.fromJson(json))
|
||||||
|
.toList();
|
||||||
|
final pagination = data['pagination'] ?? {};
|
||||||
|
return MiningRecordsPage(
|
||||||
|
items: items,
|
||||||
|
total: pagination['total'] ?? items.length,
|
||||||
|
page: pagination['page'] ?? page,
|
||||||
|
pageSize: pagination['pageSize'] ?? pageSize,
|
||||||
|
totalPages: pagination['totalPages'] ?? 1,
|
||||||
);
|
);
|
||||||
final items = response.data['items'] as List? ?? [];
|
|
||||||
return items.map((json) => MiningRecordModel.fromJson(json)).toList();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw ServerException(e.toString());
|
throw ServerException(e.toString());
|
||||||
}
|
}
|
||||||
|
|
@ -57,4 +74,21 @@ class MiningRemoteDataSourceImpl implements MiningRemoteDataSource {
|
||||||
throw ServerException(e.toString());
|
throw ServerException(e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PlantingLedgerPageModel> getPlantingLedger(
|
||||||
|
String accountSequence, {
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 10,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final response = await client.get(
|
||||||
|
ApiEndpoints.plantingLedger(accountSequence),
|
||||||
|
queryParameters: {'page': page, 'pageSize': pageSize},
|
||||||
|
);
|
||||||
|
return PlantingLedgerPageModel.fromJson(response.data);
|
||||||
|
} catch (e) {
|
||||||
|
throw ServerException(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
import '../../domain/entities/planting_record.dart';
|
||||||
|
|
||||||
|
class PlantingRecordModel extends PlantingRecord {
|
||||||
|
const PlantingRecordModel({
|
||||||
|
required super.orderId,
|
||||||
|
required super.orderNo,
|
||||||
|
super.originalAdoptionId,
|
||||||
|
required super.treeCount,
|
||||||
|
required super.contributionPerTree,
|
||||||
|
required super.totalContribution,
|
||||||
|
required super.totalAmount,
|
||||||
|
required super.status,
|
||||||
|
super.adoptionDate,
|
||||||
|
required super.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PlantingRecordModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return PlantingRecordModel(
|
||||||
|
orderId: json['orderId'] ?? json['id'] ?? '',
|
||||||
|
orderNo: json['orderNo'] ?? '',
|
||||||
|
originalAdoptionId: json['originalAdoptionId'],
|
||||||
|
treeCount: json['treeCount'] ?? 0,
|
||||||
|
contributionPerTree: json['contributionPerTree']?.toString() ?? '0',
|
||||||
|
totalContribution: json['totalContribution']?.toString() ?? json['totalAmount']?.toString() ?? '0',
|
||||||
|
totalAmount: json['totalAmount']?.toString() ?? '0',
|
||||||
|
status: _parseStatus(json['status']),
|
||||||
|
adoptionDate: json['adoptionDate'] != null ? DateTime.tryParse(json['adoptionDate']) : null,
|
||||||
|
createdAt: json['createdAt'] != null
|
||||||
|
? DateTime.parse(json['createdAt'])
|
||||||
|
: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PlantingStatus _parseStatus(String? status) {
|
||||||
|
switch (status?.toUpperCase()) {
|
||||||
|
case 'CREATED':
|
||||||
|
return PlantingStatus.created;
|
||||||
|
case 'PAID':
|
||||||
|
return PlantingStatus.paid;
|
||||||
|
case 'FUND_ALLOCATED':
|
||||||
|
return PlantingStatus.fundAllocated;
|
||||||
|
case 'MINING_ENABLED':
|
||||||
|
return PlantingStatus.miningEnabled;
|
||||||
|
case 'CANCELLED':
|
||||||
|
return PlantingStatus.cancelled;
|
||||||
|
case 'EXPIRED':
|
||||||
|
return PlantingStatus.expired;
|
||||||
|
default:
|
||||||
|
return PlantingStatus.created;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlantingSummaryModel extends PlantingSummary {
|
||||||
|
const PlantingSummaryModel({
|
||||||
|
required super.totalOrders,
|
||||||
|
required super.totalTreeCount,
|
||||||
|
required super.totalAmount,
|
||||||
|
required super.effectiveTreeCount,
|
||||||
|
super.firstPlantingAt,
|
||||||
|
super.lastPlantingAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PlantingSummaryModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return PlantingSummaryModel(
|
||||||
|
totalOrders: json['totalOrders'] ?? 0,
|
||||||
|
totalTreeCount: json['totalTreeCount'] ?? 0,
|
||||||
|
totalAmount: json['totalAmount']?.toString() ?? '0',
|
||||||
|
effectiveTreeCount: json['effectiveTreeCount'] ?? 0,
|
||||||
|
firstPlantingAt: json['firstPlantingAt'] != null
|
||||||
|
? DateTime.tryParse(json['firstPlantingAt'])
|
||||||
|
: null,
|
||||||
|
lastPlantingAt: json['lastPlantingAt'] != null
|
||||||
|
? DateTime.tryParse(json['lastPlantingAt'])
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlantingLedgerPageModel extends PlantingLedgerPage {
|
||||||
|
const PlantingLedgerPageModel({
|
||||||
|
required super.summary,
|
||||||
|
required super.items,
|
||||||
|
required super.total,
|
||||||
|
required super.page,
|
||||||
|
required super.pageSize,
|
||||||
|
required super.totalPages,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PlantingLedgerPageModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
final itemsList = json['items'] as List? ?? [];
|
||||||
|
return PlantingLedgerPageModel(
|
||||||
|
summary: PlantingSummaryModel.fromJson(json['summary'] ?? {}),
|
||||||
|
items: itemsList.map((e) => PlantingRecordModel.fromJson(e)).toList(),
|
||||||
|
total: json['total'] ?? 0,
|
||||||
|
page: json['page'] ?? 1,
|
||||||
|
pageSize: json['pageSize'] ?? 10,
|
||||||
|
totalPages: json['totalPages'] ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
import '../../domain/entities/share_account.dart';
|
import '../../domain/entities/share_account.dart';
|
||||||
import '../../domain/entities/mining_record.dart';
|
|
||||||
import '../../domain/entities/global_state.dart';
|
import '../../domain/entities/global_state.dart';
|
||||||
|
import '../../domain/entities/planting_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';
|
||||||
|
|
@ -30,16 +30,16 @@ class MiningRepositoryImpl implements MiningRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Either<Failure, List<MiningRecord>>> getMiningRecords(
|
Future<Either<Failure, MiningRecordsPage>> getMiningRecords(
|
||||||
String accountSequence, {
|
String accountSequence, {
|
||||||
int page = 1,
|
int page = 1,
|
||||||
int limit = 20,
|
int pageSize = 20,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final result = await remoteDataSource.getMiningRecords(
|
final result = await remoteDataSource.getMiningRecords(
|
||||||
accountSequence,
|
accountSequence,
|
||||||
page: page,
|
page: page,
|
||||||
limit: limit,
|
pageSize: pageSize,
|
||||||
);
|
);
|
||||||
return Right(result);
|
return Right(result);
|
||||||
} on ServerException catch (e) {
|
} on ServerException catch (e) {
|
||||||
|
|
@ -70,4 +70,24 @@ class MiningRepositoryImpl implements MiningRepository {
|
||||||
return Left(const NetworkFailure());
|
return Left(const NetworkFailure());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, PlantingLedgerPage>> getPlantingLedger(
|
||||||
|
String accountSequence, {
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 10,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final result = await remoteDataSource.getPlantingLedger(
|
||||||
|
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,132 @@
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// 认种状态枚举
|
||||||
|
enum PlantingStatus {
|
||||||
|
created,
|
||||||
|
paid,
|
||||||
|
fundAllocated,
|
||||||
|
miningEnabled,
|
||||||
|
cancelled,
|
||||||
|
expired,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 认种记录
|
||||||
|
class PlantingRecord extends Equatable {
|
||||||
|
/// 订单ID
|
||||||
|
final String orderId;
|
||||||
|
/// 订单号
|
||||||
|
final String orderNo;
|
||||||
|
/// 原始认种ID
|
||||||
|
final String? originalAdoptionId;
|
||||||
|
/// 认种数量
|
||||||
|
final int treeCount;
|
||||||
|
/// 单棵算力
|
||||||
|
final String contributionPerTree;
|
||||||
|
/// 总算力
|
||||||
|
final String totalContribution;
|
||||||
|
/// 认种金额
|
||||||
|
final String totalAmount;
|
||||||
|
/// 状态
|
||||||
|
final PlantingStatus status;
|
||||||
|
/// 认种日期
|
||||||
|
final DateTime? adoptionDate;
|
||||||
|
/// 创建时间
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
const PlantingRecord({
|
||||||
|
required this.orderId,
|
||||||
|
required this.orderNo,
|
||||||
|
this.originalAdoptionId,
|
||||||
|
required this.treeCount,
|
||||||
|
required this.contributionPerTree,
|
||||||
|
required this.totalContribution,
|
||||||
|
required this.totalAmount,
|
||||||
|
required this.status,
|
||||||
|
this.adoptionDate,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 状态显示文本
|
||||||
|
String get statusText {
|
||||||
|
switch (status) {
|
||||||
|
case PlantingStatus.created:
|
||||||
|
return '已创建';
|
||||||
|
case PlantingStatus.paid:
|
||||||
|
return '已支付';
|
||||||
|
case PlantingStatus.fundAllocated:
|
||||||
|
return '资金已分配';
|
||||||
|
case PlantingStatus.miningEnabled:
|
||||||
|
return '已开始挖矿';
|
||||||
|
case PlantingStatus.cancelled:
|
||||||
|
return '已取消';
|
||||||
|
case PlantingStatus.expired:
|
||||||
|
return '已过期';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 是否为有效状态
|
||||||
|
bool get isActive =>
|
||||||
|
status == PlantingStatus.miningEnabled ||
|
||||||
|
status == PlantingStatus.paid ||
|
||||||
|
status == PlantingStatus.fundAllocated;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [orderId, orderNo, treeCount, status];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 认种汇总
|
||||||
|
class PlantingSummary extends Equatable {
|
||||||
|
/// 总订单数
|
||||||
|
final int totalOrders;
|
||||||
|
/// 总认种量
|
||||||
|
final int totalTreeCount;
|
||||||
|
/// 总金额
|
||||||
|
final String totalAmount;
|
||||||
|
/// 有效认种量
|
||||||
|
final int effectiveTreeCount;
|
||||||
|
/// 首次认种时间
|
||||||
|
final DateTime? firstPlantingAt;
|
||||||
|
/// 最近认种时间
|
||||||
|
final DateTime? lastPlantingAt;
|
||||||
|
|
||||||
|
const PlantingSummary({
|
||||||
|
required this.totalOrders,
|
||||||
|
required this.totalTreeCount,
|
||||||
|
required this.totalAmount,
|
||||||
|
required this.effectiveTreeCount,
|
||||||
|
this.firstPlantingAt,
|
||||||
|
this.lastPlantingAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
totalOrders,
|
||||||
|
totalTreeCount,
|
||||||
|
totalAmount,
|
||||||
|
effectiveTreeCount,
|
||||||
|
firstPlantingAt,
|
||||||
|
lastPlantingAt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 认种分类账分页数据
|
||||||
|
class PlantingLedgerPage extends Equatable {
|
||||||
|
final PlantingSummary summary;
|
||||||
|
final List<PlantingRecord> items;
|
||||||
|
final int total;
|
||||||
|
final int page;
|
||||||
|
final int pageSize;
|
||||||
|
final int totalPages;
|
||||||
|
|
||||||
|
const PlantingLedgerPage({
|
||||||
|
required this.summary,
|
||||||
|
required this.items,
|
||||||
|
required this.total,
|
||||||
|
required this.page,
|
||||||
|
required this.pageSize,
|
||||||
|
required this.totalPages,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [summary, items, total, page, pageSize, totalPages];
|
||||||
|
}
|
||||||
|
|
@ -3,15 +3,39 @@ import '../../core/error/failures.dart';
|
||||||
import '../entities/share_account.dart';
|
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';
|
||||||
|
|
||||||
|
/// 挖矿记录分页数据
|
||||||
|
class MiningRecordsPage {
|
||||||
|
final List<MiningRecord> items;
|
||||||
|
final int total;
|
||||||
|
final int page;
|
||||||
|
final int pageSize;
|
||||||
|
final int totalPages;
|
||||||
|
|
||||||
|
const MiningRecordsPage({
|
||||||
|
required this.items,
|
||||||
|
required this.total,
|
||||||
|
required this.page,
|
||||||
|
required this.pageSize,
|
||||||
|
required this.totalPages,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
abstract class MiningRepository {
|
abstract class MiningRepository {
|
||||||
Future<Either<Failure, ShareAccount>> getShareAccount(String accountSequence);
|
Future<Either<Failure, ShareAccount>> getShareAccount(String accountSequence);
|
||||||
|
|
||||||
Future<Either<Failure, List<MiningRecord>>> getMiningRecords(
|
Future<Either<Failure, MiningRecordsPage>> getMiningRecords(
|
||||||
String accountSequence, {
|
String accountSequence, {
|
||||||
int page = 1,
|
int page = 1,
|
||||||
int limit = 20,
|
int pageSize = 20,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<Either<Failure, GlobalState>> getGlobalState();
|
Future<Either<Failure, GlobalState>> getGlobalState();
|
||||||
|
|
||||||
|
Future<Either<Failure, PlantingLedgerPage>> getPlantingLedger(
|
||||||
|
String accountSequence, {
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 10,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,346 @@
|
||||||
|
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/mining_record.dart';
|
||||||
|
import '../../../domain/repositories/mining_repository.dart';
|
||||||
|
import '../../providers/user_providers.dart';
|
||||||
|
import '../../providers/mining_providers.dart';
|
||||||
|
|
||||||
|
/// 分配记录页面(挖矿记录)
|
||||||
|
class MiningRecordsPage extends ConsumerStatefulWidget {
|
||||||
|
const MiningRecordsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<MiningRecordsPage> createState() => _MiningRecordsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MiningRecordsPageState extends ConsumerState<MiningRecordsPage> {
|
||||||
|
static const Color _orange = Color(0xFFFF6B00);
|
||||||
|
static const Color _green = Color(0xFF22C55E);
|
||||||
|
static const Color _grayText = Color(0xFF6B7280);
|
||||||
|
static const Color _darkText = Color(0xFF1F2937);
|
||||||
|
static const Color _bgGray = Color(0xFFF3F4F6);
|
||||||
|
|
||||||
|
int _currentPage = 1;
|
||||||
|
static const int _pageSize = 20;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final user = ref.watch(userNotifierProvider);
|
||||||
|
final accountSequence = user.accountSequence ?? '';
|
||||||
|
|
||||||
|
final recordsParams = MiningRecordsParams(
|
||||||
|
accountSequence: accountSequence,
|
||||||
|
page: _currentPage,
|
||||||
|
pageSize: _pageSize,
|
||||||
|
);
|
||||||
|
final recordsAsync = ref.watch(miningRecordsProvider(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(miningRecordsProvider(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, MiningRecordsParams 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(miningRecordsProvider(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),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'认种榴莲树后将开始产生收益',
|
||||||
|
style: TextStyle(fontSize: 14, color: _grayText.withOpacity(0.7)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRecordsList(MiningRecordsPage 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(MiningRecord 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: [
|
||||||
|
Text(
|
||||||
|
record.distributionMinute,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: _darkText,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'+${formatAmount(record.shareAmount)}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: _green,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// 第二行:算力占比 + 价格快照
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_buildInfoItem('算力占比', _formatPercent(record.contributionRatio)),
|
||||||
|
const SizedBox(width: 24),
|
||||||
|
_buildInfoItem('价格快照', _formatPrice(record.priceSnapshot)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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)),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: _darkText,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatPercent(String ratio) {
|
||||||
|
try {
|
||||||
|
final value = double.parse(ratio);
|
||||||
|
return '${(value * 100).toStringAsFixed(6)}%';
|
||||||
|
} catch (e) {
|
||||||
|
return ratio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatPrice(String price) {
|
||||||
|
try {
|
||||||
|
final value = double.parse(price);
|
||||||
|
return value.toStringAsFixed(8);
|
||||||
|
} catch (e) {
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPaginationInfo(MiningRecordsPage 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,496 @@
|
||||||
|
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/planting_record.dart';
|
||||||
|
import '../../providers/user_providers.dart';
|
||||||
|
import '../../providers/mining_providers.dart';
|
||||||
|
|
||||||
|
/// 认种记录页面
|
||||||
|
class PlantingRecordsPage extends ConsumerStatefulWidget {
|
||||||
|
const PlantingRecordsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<PlantingRecordsPage> createState() => _PlantingRecordsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PlantingRecordsPageState extends ConsumerState<PlantingRecordsPage> {
|
||||||
|
static const Color _orange = Color(0xFFFF6B00);
|
||||||
|
static const Color _green = Color(0xFF22C55E);
|
||||||
|
static const Color _blue = Color(0xFF3B82F6);
|
||||||
|
static const Color _grayText = Color(0xFF6B7280);
|
||||||
|
static const Color _darkText = Color(0xFF1F2937);
|
||||||
|
static const Color _bgGray = Color(0xFFF3F4F6);
|
||||||
|
|
||||||
|
int _currentPage = 1;
|
||||||
|
static const int _pageSize = 10;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final user = ref.watch(userNotifierProvider);
|
||||||
|
final accountSequence = user.accountSequence ?? '';
|
||||||
|
|
||||||
|
final recordsParams = PlantingRecordsParams(
|
||||||
|
accountSequence: accountSequence,
|
||||||
|
page: _currentPage,
|
||||||
|
pageSize: _pageSize,
|
||||||
|
);
|
||||||
|
final recordsAsync = ref.watch(plantingRecordsProvider(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(plantingRecordsProvider(recordsParams));
|
||||||
|
},
|
||||||
|
child: recordsAsync.when(
|
||||||
|
loading: () => _buildLoadingView(),
|
||||||
|
error: (error, stack) => _buildErrorView(error.toString(), recordsParams),
|
||||||
|
data: (ledgerPage) {
|
||||||
|
if (ledgerPage == null || ledgerPage.items.isEmpty) {
|
||||||
|
return _buildEmptyView();
|
||||||
|
}
|
||||||
|
return _buildContent(ledgerPage);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoadingView() {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// 汇总卡片骨架
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.all(16),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: List.generate(3, (index) => _buildShimmerStat()),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: List.generate(3, (index) => _buildShimmerStat()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 列表骨架
|
||||||
|
...List.generate(5, (index) => _buildShimmerCard()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildShimmerStat() {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 60,
|
||||||
|
height: 24,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[300],
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 12,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[300],
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildShimmerCard() {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.fromLTRB(16, 0, 16, 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: 100, height: 16, color: Colors.grey[300]),
|
||||||
|
Container(width: 60, height: 20, color: Colors.grey[300]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(width: 80, height: 12, color: Colors.grey[300]),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Container(width: 80, height: 12, color: Colors.grey[300]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildErrorView(String error, PlantingRecordsParams 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(plantingRecordsProvider(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.eco_outlined, size: 64, color: _grayText.withOpacity(0.5)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'暂无认种记录',
|
||||||
|
style: TextStyle(fontSize: 16, color: _grayText),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'认种榴莲树后将显示记录',
|
||||||
|
style: TextStyle(fontSize: 14, color: _grayText.withOpacity(0.7)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(PlantingLedgerPage ledgerPage) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// 汇总卡片
|
||||||
|
_buildSummaryCard(ledgerPage.summary),
|
||||||
|
// 记录列表
|
||||||
|
...ledgerPage.items.map((record) => _buildRecordCard(record)),
|
||||||
|
// 分页信息
|
||||||
|
_buildPaginationInfo(ledgerPage),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSummaryCard(PlantingSummary summary) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.all(16),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.eco, size: 20, color: _green),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Text(
|
||||||
|
'认种汇总',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: _darkText,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
_buildSummaryItem('总订单数', summary.totalOrders.toString()),
|
||||||
|
_buildSummaryItem('总认种量', summary.totalTreeCount.toString(), color: _green),
|
||||||
|
_buildSummaryItem('总金额', formatAmount(summary.totalAmount), color: _orange),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
_buildSummaryItem('有效认种', summary.effectiveTreeCount.toString(), color: _blue),
|
||||||
|
_buildSummaryItem(
|
||||||
|
'首次认种',
|
||||||
|
summary.firstPlantingAt != null
|
||||||
|
? DateFormat('MM-dd').format(summary.firstPlantingAt!)
|
||||||
|
: '-',
|
||||||
|
),
|
||||||
|
_buildSummaryItem(
|
||||||
|
'最近认种',
|
||||||
|
summary.lastPlantingAt != null
|
||||||
|
? DateFormat('MM-dd').format(summary.lastPlantingAt!)
|
||||||
|
: '-',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSummaryItem(String label, String value, {Color? color}) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: color ?? _darkText,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(fontSize: 12, color: _grayText),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRecordCard(PlantingRecord record) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: !record.isActive
|
||||||
|
? Border.all(color: Colors.grey.withOpacity(0.3))
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: Opacity(
|
||||||
|
opacity: record.isActive ? 1.0 : 0.6,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 第一行:订单号 + 状态
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
record.originalAdoptionId ?? record.orderNo,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: _darkText,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildStatusBadge(record.status),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// 第二行:认种数量 + 单棵算力 + 总算力
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_buildInfoItem('认种数量', '${record.treeCount}棵'),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
_buildInfoItem('单棵算力', formatAmount(record.contributionPerTree)),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
_buildInfoItem('总算力', formatAmount(record.totalContribution), isHighlight: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// 第三行:认种日期
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.calendar_today_outlined, size: 12, color: _grayText.withOpacity(0.7)),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
record.adoptionDate != null
|
||||||
|
? DateFormat('yyyy-MM-dd HH:mm').format(record.adoptionDate!)
|
||||||
|
: DateFormat('yyyy-MM-dd HH:mm').format(record.createdAt),
|
||||||
|
style: TextStyle(fontSize: 11, color: _grayText.withOpacity(0.7)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusBadge(PlantingStatus status) {
|
||||||
|
Color bgColor;
|
||||||
|
Color textColor;
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case PlantingStatus.miningEnabled:
|
||||||
|
bgColor = _green.withOpacity(0.1);
|
||||||
|
textColor = _green;
|
||||||
|
break;
|
||||||
|
case PlantingStatus.paid:
|
||||||
|
case PlantingStatus.fundAllocated:
|
||||||
|
bgColor = _blue.withOpacity(0.1);
|
||||||
|
textColor = _blue;
|
||||||
|
break;
|
||||||
|
case PlantingStatus.created:
|
||||||
|
bgColor = _orange.withOpacity(0.1);
|
||||||
|
textColor = _orange;
|
||||||
|
break;
|
||||||
|
case PlantingStatus.cancelled:
|
||||||
|
case PlantingStatus.expired:
|
||||||
|
bgColor = Colors.red.withOpacity(0.1);
|
||||||
|
textColor = Colors.red;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bgColor,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
status.toString().split('.').last == 'miningEnabled' ? '已开始挖矿' : _getStatusText(status),
|
||||||
|
style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: textColor),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getStatusText(PlantingStatus status) {
|
||||||
|
switch (status) {
|
||||||
|
case PlantingStatus.created:
|
||||||
|
return '已创建';
|
||||||
|
case PlantingStatus.paid:
|
||||||
|
return '已支付';
|
||||||
|
case PlantingStatus.fundAllocated:
|
||||||
|
return '资金已分配';
|
||||||
|
case PlantingStatus.miningEnabled:
|
||||||
|
return '已开始挖矿';
|
||||||
|
case PlantingStatus.cancelled:
|
||||||
|
return '已取消';
|
||||||
|
case PlantingStatus.expired:
|
||||||
|
return '已过期';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoItem(String label, String value, {bool isHighlight = false}) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'$label: ',
|
||||||
|
style: TextStyle(fontSize: 12, color: _grayText.withOpacity(0.7)),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: isHighlight ? _green : _darkText,
|
||||||
|
fontWeight: isHighlight ? FontWeight.w600 : FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPaginationInfo(PlantingLedgerPage ledgerPage) {
|
||||||
|
final totalPages = ledgerPage.totalPages > 0
|
||||||
|
? ledgerPage.totalPages
|
||||||
|
: (ledgerPage.total / _pageSize).ceil();
|
||||||
|
|
||||||
|
if (totalPages <= 1) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'共 ${ledgerPage.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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -410,12 +410,12 @@ class ProfilePage extends ConsumerWidget {
|
||||||
_buildRecordIcon(
|
_buildRecordIcon(
|
||||||
icon: Icons.eco,
|
icon: Icons.eco,
|
||||||
label: '认种记录',
|
label: '认种记录',
|
||||||
onTap: () {},
|
onTap: () => context.push(Routes.plantingRecords),
|
||||||
),
|
),
|
||||||
_buildRecordIcon(
|
_buildRecordIcon(
|
||||||
icon: Icons.assignment,
|
icon: Icons.assignment,
|
||||||
label: '分配记录',
|
label: '分配记录',
|
||||||
onTap: () {},
|
onTap: () => context.push(Routes.miningRecords),
|
||||||
),
|
),
|
||||||
_buildRecordIcon(
|
_buildRecordIcon(
|
||||||
icon: Icons.receipt_long,
|
icon: Icons.receipt_long,
|
||||||
|
|
@ -491,16 +491,6 @@ class ProfilePage extends ConsumerWidget {
|
||||||
icon: Icons.people,
|
icon: Icons.people,
|
||||||
label: '我的团队',
|
label: '我的团队',
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
),
|
|
||||||
_buildSettingItem(
|
|
||||||
icon: Icons.trending_up,
|
|
||||||
label: '收益明细',
|
|
||||||
onTap: () {},
|
|
||||||
),
|
|
||||||
_buildSettingItem(
|
|
||||||
icon: Icons.card_giftcard,
|
|
||||||
label: '推广奖励',
|
|
||||||
onTap: () {},
|
|
||||||
showDivider: false,
|
showDivider: false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,16 @@ import 'dart:async';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
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/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';
|
||||||
import '../../core/di/injection.dart';
|
import '../../core/di/injection.dart';
|
||||||
|
|
||||||
|
final miningRepositoryProvider = Provider<MiningRepository>((ref) {
|
||||||
|
return getIt<MiningRepository>();
|
||||||
|
});
|
||||||
|
|
||||||
// Use Cases Providers
|
// Use Cases Providers
|
||||||
final getShareAccountUseCaseProvider = Provider<GetShareAccount>((ref) {
|
final getShareAccountUseCaseProvider = Provider<GetShareAccount>((ref) {
|
||||||
return getIt<GetShareAccount>();
|
return getIt<GetShareAccount>();
|
||||||
|
|
@ -67,3 +73,107 @@ final currentPriceProvider = Provider<String>((ref) {
|
||||||
error: (_, __) => '0',
|
error: (_, __) => '0',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// 挖矿记录请求参数
|
||||||
|
class MiningRecordsParams {
|
||||||
|
final String accountSequence;
|
||||||
|
final int page;
|
||||||
|
final int pageSize;
|
||||||
|
|
||||||
|
const MiningRecordsParams({
|
||||||
|
required this.accountSequence,
|
||||||
|
this.page = 1,
|
||||||
|
this.pageSize = 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is MiningRecordsParams &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
accountSequence == other.accountSequence &&
|
||||||
|
page == other.page &&
|
||||||
|
pageSize == other.pageSize;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => accountSequence.hashCode ^ page.hashCode ^ pageSize.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 挖矿记录 Provider
|
||||||
|
final miningRecordsProvider = FutureProvider.family<MiningRecordsPage?, MiningRecordsParams>(
|
||||||
|
(ref, params) async {
|
||||||
|
if (params.accountSequence.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final repository = ref.watch(miningRepositoryProvider);
|
||||||
|
final result = await repository.getMiningRecords(
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 认种记录请求参数
|
||||||
|
class PlantingRecordsParams {
|
||||||
|
final String accountSequence;
|
||||||
|
final int page;
|
||||||
|
final int pageSize;
|
||||||
|
|
||||||
|
const PlantingRecordsParams({
|
||||||
|
required this.accountSequence,
|
||||||
|
this.page = 1,
|
||||||
|
this.pageSize = 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is PlantingRecordsParams &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
accountSequence == other.accountSequence &&
|
||||||
|
page == other.page &&
|
||||||
|
pageSize == other.pageSize;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => accountSequence.hashCode ^ page.hashCode ^ pageSize.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 认种记录 Provider
|
||||||
|
final plantingRecordsProvider = FutureProvider.family<PlantingLedgerPage?, PlantingRecordsParams>(
|
||||||
|
(ref, params) async {
|
||||||
|
if (params.accountSequence.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final repository = ref.watch(miningRepositoryProvider);
|
||||||
|
final result = await repository.getPlantingLedger(
|
||||||
|
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),
|
||||||
|
(ledger) => ledger,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue