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:
hailin 2026-01-14 23:23:31 -08:00
parent b81ae634a6
commit 546c0060da
12 changed files with 1293 additions and 25 deletions

View File

@ -55,4 +55,8 @@ class ApiEndpoints {
'/api/v2/contribution/accounts/$accountSequence';
static String contributionRecords(String accountSequence) =>
'/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';
}

View File

@ -11,6 +11,8 @@ import '../../presentation/pages/contribution/contribution_records_page.dart';
import '../../presentation/pages/trading/trading_page.dart';
import '../../presentation/pages/asset/asset_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/providers/user_providers.dart';
import 'routes.dart';
@ -102,6 +104,14 @@ final appRouterProvider = Provider<GoRouter>((ref) {
path: Routes.contributionRecords,
builder: (context, state) => const ContributionRecordsListPage(),
),
GoRoute(
path: Routes.miningRecords,
builder: (context, state) => const MiningRecordsPage(),
),
GoRoute(
path: Routes.plantingRecords,
builder: (context, state) => const PlantingRecordsPage(),
),
ShellRoute(
builder: (context, state, child) => MainShell(child: child),
routes: [

View File

@ -10,5 +10,6 @@ class Routes {
static const String profile = '/profile';
static const String miningRecords = '/mining-records';
static const String contributionRecords = '/contribution-records';
static const String plantingRecords = '/planting-records';
static const String orders = '/orders';
}

View File

@ -1,18 +1,25 @@
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 '../../../core/network/api_client.dart';
import '../../../core/network/api_endpoints.dart';
import '../../../core/error/exceptions.dart';
import '../../../domain/repositories/mining_repository.dart';
abstract class MiningRemoteDataSource {
Future<ShareAccountModel> getShareAccount(String accountSequence);
Future<List<MiningRecordModel>> getMiningRecords(
Future<MiningRecordsPage> getMiningRecords(
String accountSequence, {
int page = 1,
int limit = 20,
int pageSize = 20,
});
Future<GlobalStateModel> getGlobalState();
Future<PlantingLedgerPageModel> getPlantingLedger(
String accountSequence, {
int page = 1,
int pageSize = 10,
});
}
class MiningRemoteDataSourceImpl implements MiningRemoteDataSource {
@ -31,18 +38,28 @@ class MiningRemoteDataSourceImpl implements MiningRemoteDataSource {
}
@override
Future<List<MiningRecordModel>> getMiningRecords(
Future<MiningRecordsPage> getMiningRecords(
String accountSequence, {
int page = 1,
int limit = 20,
int pageSize = 20,
}) async {
try {
final response = await client.get(
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) {
throw ServerException(e.toString());
}
@ -57,4 +74,21 @@ class MiningRemoteDataSourceImpl implements MiningRemoteDataSource {
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());
}
}
}

View File

@ -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,
);
}
}

View File

@ -1,7 +1,7 @@
import 'package:dartz/dartz.dart';
import '../../domain/entities/share_account.dart';
import '../../domain/entities/mining_record.dart';
import '../../domain/entities/global_state.dart';
import '../../domain/entities/planting_record.dart';
import '../../domain/repositories/mining_repository.dart';
import '../../core/error/exceptions.dart';
import '../../core/error/failures.dart';
@ -30,16 +30,16 @@ class MiningRepositoryImpl implements MiningRepository {
}
@override
Future<Either<Failure, List<MiningRecord>>> getMiningRecords(
Future<Either<Failure, MiningRecordsPage>> getMiningRecords(
String accountSequence, {
int page = 1,
int limit = 20,
int pageSize = 20,
}) async {
try {
final result = await remoteDataSource.getMiningRecords(
accountSequence,
page: page,
limit: limit,
pageSize: pageSize,
);
return Right(result);
} on ServerException catch (e) {
@ -70,4 +70,24 @@ class MiningRepositoryImpl implements MiningRepository {
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());
}
}
}

View File

@ -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];
}

View File

@ -3,15 +3,39 @@ import '../../core/error/failures.dart';
import '../entities/share_account.dart';
import '../entities/mining_record.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 {
Future<Either<Failure, ShareAccount>> getShareAccount(String accountSequence);
Future<Either<Failure, List<MiningRecord>>> getMiningRecords(
Future<Either<Failure, MiningRecordsPage>> getMiningRecords(
String accountSequence, {
int page = 1,
int limit = 20,
int pageSize = 20,
});
Future<Either<Failure, GlobalState>> getGlobalState();
Future<Either<Failure, PlantingLedgerPage>> getPlantingLedger(
String accountSequence, {
int page = 1,
int pageSize = 10,
});
}

View File

@ -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),
),
],
),
],
),
);
}
}

View File

@ -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),
),
],
),
],
),
);
}
}

View File

@ -410,12 +410,12 @@ class ProfilePage extends ConsumerWidget {
_buildRecordIcon(
icon: Icons.eco,
label: '认种记录',
onTap: () {},
onTap: () => context.push(Routes.plantingRecords),
),
_buildRecordIcon(
icon: Icons.assignment,
label: '分配记录',
onTap: () {},
onTap: () => context.push(Routes.miningRecords),
),
_buildRecordIcon(
icon: Icons.receipt_long,
@ -491,16 +491,6 @@ class ProfilePage extends ConsumerWidget {
icon: Icons.people,
label: '我的团队',
onTap: () {},
),
_buildSettingItem(
icon: Icons.trending_up,
label: '收益明细',
onTap: () {},
),
_buildSettingItem(
icon: Icons.card_giftcard,
label: '推广奖励',
onTap: () {},
showDivider: false,
),
],

View File

@ -2,10 +2,16 @@ import 'dart:async';
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/repositories/mining_repository.dart';
import '../../domain/usecases/mining/get_share_account.dart';
import '../../domain/usecases/mining/get_global_state.dart';
import '../../core/di/injection.dart';
final miningRepositoryProvider = Provider<MiningRepository>((ref) {
return getIt<MiningRepository>();
});
// Use Cases Providers
final getShareAccountUseCaseProvider = Provider<GetShareAccount>((ref) {
return getIt<GetShareAccount>();
@ -67,3 +73,107 @@ final currentPriceProvider = Provider<String>((ref) {
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,
);
},
);