From caffb124d2b5d90376a41a0c91c6ab783b532448 Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 14 Jan 2026 19:02:30 -0800 Subject: [PATCH] feat(mining-app): add contribution records page with category summary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create contribution_records_page.dart with full list view - Pagination support with page navigation - Filter by source type (personal, team level, team bonus) - Show detailed info: tree count, base contribution, rate, amount - Display effective/expire dates and status badges - Update contribution_page.dart detail card - Show category summary instead of record list - Display three categories with icons: personal, team level, team bonus - Add navigation to full records page via "查看全部" - Add route configuration for /contribution-records Co-Authored-By: Claude Opus 4.5 --- .../lib/core/router/app_router.dart | 5 + .../pages/contribution/contribution_page.dart | 251 ++++----- .../contribution_records_page.dart | 518 ++++++++++++++++++ 3 files changed, 640 insertions(+), 134 deletions(-) create mode 100644 frontend/mining-app/lib/presentation/pages/contribution/contribution_records_page.dart diff --git a/frontend/mining-app/lib/core/router/app_router.dart b/frontend/mining-app/lib/core/router/app_router.dart index 7f736409..da50be44 100644 --- a/frontend/mining-app/lib/core/router/app_router.dart +++ b/frontend/mining-app/lib/core/router/app_router.dart @@ -7,6 +7,7 @@ import '../../presentation/pages/auth/register_page.dart'; import '../../presentation/pages/auth/forgot_password_page.dart'; import '../../presentation/pages/auth/change_password_page.dart'; import '../../presentation/pages/contribution/contribution_page.dart'; +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'; @@ -97,6 +98,10 @@ final appRouterProvider = Provider((ref) { path: Routes.changePassword, builder: (context, state) => const ChangePasswordPage(), ), + GoRoute( + path: Routes.contributionRecords, + builder: (context, state) => const ContributionRecordsPage(), + ), ShellRoute( builder: (context, state, child) => MainShell(child: child), routes: [ diff --git a/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart b/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart index b981120f..e620d15f 100644 --- a/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart +++ b/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:intl/intl.dart'; +import 'package:go_router/go_router.dart'; import '../../../core/constants/app_colors.dart'; +import '../../../core/router/routes.dart'; import '../../../core/utils/format_utils.dart'; import '../../../domain/entities/contribution.dart'; -import '../../../domain/entities/contribution_record.dart'; import '../../providers/user_providers.dart'; import '../../providers/contribution_providers.dart'; import '../../widgets/shimmer_loading.dart'; @@ -25,13 +25,6 @@ class ContributionPage extends ConsumerWidget { final user = ref.watch(userNotifierProvider); final accountSequence = user.accountSequence ?? ''; final contributionAsync = ref.watch(contributionProvider(accountSequence)); - final recordsParams = ContributionRecordsParams( - accountSequence: accountSequence, - page: 1, - pageSize: 3, - ); - final recordsAsync = ref.watch(contributionRecordsProvider(recordsParams)); - // 获取预估收益 final estimatedEarnings = ref.watch(estimatedEarningsProvider(accountSequence)); final statsAsync = ref.watch(contributionStatsProvider); @@ -50,7 +43,6 @@ class ContributionPage extends ConsumerWidget { child: RefreshIndicator( onRefresh: () async { ref.invalidate(contributionProvider(accountSequence)); - ref.invalidate(contributionRecordsProvider(recordsParams)); ref.invalidate(contributionStatsProvider); }, child: hasError && contribution == null @@ -87,14 +79,14 @@ class ContributionPage extends ConsumerWidget { // 今日预估收益 _buildTodayEstimateCard(estimatedEarnings, isLoading || isStatsLoading), const SizedBox(height: 16), - // 贡献值明细 - _buildContributionDetailCard(context, ref, recordsAsync), + // 贡献值明细(三类汇总) + _buildContributionDetailCard(context, contribution, isLoading), const SizedBox(height: 16), // 团队层级统计 _buildTeamStatsCard(contribution, isLoading), const SizedBox(height: 16), // 贡献值失效倒计时 - _buildExpirationCard(contribution, recordsAsync, isLoading), + _buildExpirationCard(contribution, isLoading), const SizedBox(height: 100), ]), ), @@ -337,12 +329,9 @@ class ContributionPage extends ConsumerWidget { Widget _buildContributionDetailCard( BuildContext context, - WidgetRef ref, - AsyncValue recordsAsync, + Contribution? contribution, + bool isLoading, ) { - final isRecordsLoading = recordsAsync.isLoading; - final recordsPage = recordsAsync.valueOrNull; - return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -361,7 +350,7 @@ class ContributionPage extends ConsumerWidget { ), GestureDetector( onTap: () { - // TODO: 跳转到完整记录页面 + context.push(Routes.contributionRecords); }, child: const Row( children: [ @@ -373,75 +362,74 @@ class ContributionPage extends ConsumerWidget { ], ), const SizedBox(height: 16), - // 明细列表 - if (isRecordsLoading) - _buildRecordsShimmer() - else if (recordsAsync.hasError && recordsPage == null) - Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: Text( - '加载失败', - style: TextStyle(fontSize: 14, color: _grayText.withOpacity(0.7)), - ), - ) - else if (recordsPage == null || recordsPage.data.isEmpty) - const Padding( - padding: EdgeInsets.symmetric(vertical: 20), - child: Text( - '暂无贡献值记录', - style: TextStyle(fontSize: 14, color: _grayText), - ), - ) + // 三类汇总 + if (isLoading) + _buildDetailSummaryShimmer() else Column( - children: recordsPage.data.asMap().entries.map((entry) { - final index = entry.key; - final record = entry.value; - return Column( - children: [ - _buildDetailRow(record), - if (index < recordsPage.data.length - 1) const Divider(height: 24), - ], - ); - }).toList(), + children: [ + _buildDetailSummaryRow( + icon: Icons.eco_outlined, + iconColor: _orange, + title: '本人种植', + subtitle: '个人认种榴莲树产生的贡献值', + amount: contribution?.personalContribution ?? '0', + ), + const Divider(height: 24), + _buildDetailSummaryRow( + icon: Icons.groups_outlined, + iconColor: Colors.blue, + title: '团队层级', + subtitle: '直推及间推用户认种产生的贡献值', + amount: contribution?.teamLevelContribution ?? '0', + ), + const Divider(height: 24), + _buildDetailSummaryRow( + icon: Icons.card_giftcard_outlined, + iconColor: Colors.purple, + title: '团队奖励', + subtitle: '满足条件后获得的额外奖励贡献值', + amount: contribution?.teamBonusContribution ?? '0', + ), + ], ), ], ), ); } - Widget _buildRecordsShimmer() { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Column( - children: [ - _buildShimmerDetailRow(), - const Divider(height: 24), - _buildShimmerDetailRow(), - const Divider(height: 24), - _buildShimmerDetailRow(), - ], - ), - ); - } - - Widget _buildShimmerDetailRow() { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + Widget _buildDetailSummaryShimmer() { + return Column( children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - ShimmerText( - placeholder: '认种贡献', - style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: _darkText), - ), - SizedBox(height: 2), - ShimmerText( - placeholder: '2024-01-01 12:00', - style: TextStyle(fontSize: 12, color: _grayText), - ), - ], + _buildShimmerSummaryRow(), + const Divider(height: 24), + _buildShimmerSummaryRow(), + const Divider(height: 24), + _buildShimmerSummaryRow(), + ], + ); + } + + Widget _buildShimmerSummaryRow() { + return Row( + children: [ + const ShimmerBox(width: 40, height: 40), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + ShimmerText( + placeholder: '本人种植', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: _darkText), + ), + SizedBox(height: 2), + ShimmerText( + placeholder: '个人认种产生的贡献值', + style: TextStyle(fontSize: 12, color: _grayText), + ), + ], + ), ), const ShimmerText( placeholder: '+1,000', @@ -451,27 +439,43 @@ class ContributionPage extends ConsumerWidget { ); } - Widget _buildDetailRow(ContributionRecord record) { - final dateFormat = DateFormat('yyyy-MM-dd HH:mm'); - final formattedDate = dateFormat.format(record.createdAt); - final amount = '+${formatAmount(record.finalContribution)}'; - + Widget _buildDetailSummaryRow({ + required IconData icon, + required Color iconColor, + required String title, + required String subtitle, + required String amount, + }) { return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - record.displayTitle, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: _darkText), - ), - const SizedBox(height: 2), - Text(formattedDate, style: const TextStyle(fontSize: 12, color: _grayText)), - ], + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: iconColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, color: iconColor, size: 22), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: _darkText), + ), + const SizedBox(height: 2), + Text( + subtitle, + style: TextStyle(fontSize: 11, color: _grayText.withOpacity(0.8)), + ), + ], + ), ), Text( - amount, + formatAmount(amount), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: _green), ), ], @@ -576,40 +580,19 @@ class ContributionPage extends ConsumerWidget { Widget _buildExpirationCard( Contribution? contribution, - AsyncValue recordsAsync, bool isLoading, ) { - final isRecordsLoading = recordsAsync.isLoading; - final recordsPage = recordsAsync.valueOrNull; + // 贡献值有效期为2年(730天) + // 暂时使用固定信息,后续可从后端获取最近过期日期 + const int validityDays = 730; + final hasContribution = contribution != null && + (double.tryParse(contribution.totalContribution) ?? 0) > 0; - // 从记录中获取最近的过期日期 - DateTime? nearestExpireDate; - if (recordsPage != null && recordsPage.data.isNotEmpty) { - // 找到未过期记录中最近的过期日期 - final activeRecords = recordsPage.data.where((r) => !r.isExpired).toList(); - if (activeRecords.isNotEmpty) { - activeRecords.sort((a, b) => a.expireDate.compareTo(b.expireDate)); - nearestExpireDate = activeRecords.first.expireDate; - } - } - - // 计算剩余天数和进度 - final now = DateTime.now(); - int daysRemaining = 730; // 默认值 - double progress = 1.0; - String expireDateText = '暂无过期信息'; - - if (nearestExpireDate != null) { - daysRemaining = nearestExpireDate.difference(now).inDays; - if (daysRemaining < 0) daysRemaining = 0; - // 假设总有效期为730天 - progress = daysRemaining / 730; - if (progress > 1) progress = 1; - if (progress < 0) progress = 0; - expireDateText = '您的贡献值将于 ${DateFormat('yyyy-MM-dd').format(nearestExpireDate)} 失效'; - } - - final showShimmer = isLoading || isRecordsLoading; + // 如果有贡献值,显示有效期提示 + final String expireDateText = hasContribution + ? '贡献值自生效日起 $validityDays 天内有效' + : '暂无贡献值'; + final double progress = hasContribution ? 1.0 : 0.0; return Container( padding: const EdgeInsets.all(20), @@ -636,19 +619,19 @@ class ContributionPage extends ConsumerWidget { ClipRRect( borderRadius: BorderRadius.circular(5), child: LinearProgressIndicator( - value: showShimmer ? 1.0 : progress, + value: isLoading ? 1.0 : progress, minHeight: 10, backgroundColor: _bgGray, valueColor: AlwaysStoppedAnimation( - showShimmer ? _bgGray : _orange, + isLoading ? _bgGray : _orange, ), ), ), const SizedBox(height: 12), // 说明文字 - showShimmer + isLoading ? const ShimmerText( - placeholder: '您的贡献值将于 ---- 失效', + placeholder: '贡献值有效期信息加载中...', style: TextStyle(fontSize: 12, color: _grayText), ) : Text( @@ -656,13 +639,13 @@ class ContributionPage extends ConsumerWidget { style: const TextStyle(fontSize: 12, color: _grayText), ), const SizedBox(height: 4), - showShimmer + isLoading ? const ShimmerText( - placeholder: '剩余 --- 天', + placeholder: '有效期 --- 天', style: TextStyle(fontSize: 12, color: _orange, fontWeight: FontWeight.w500), ) : Text( - '剩余 $daysRemaining 天', + '有效期 $validityDays 天', style: const TextStyle(fontSize: 12, color: _orange, fontWeight: FontWeight.w500), ), const SizedBox(height: 8), diff --git a/frontend/mining-app/lib/presentation/pages/contribution/contribution_records_page.dart b/frontend/mining-app/lib/presentation/pages/contribution/contribution_records_page.dart new file mode 100644 index 00000000..e7c59641 --- /dev/null +++ b/frontend/mining-app/lib/presentation/pages/contribution/contribution_records_page.dart @@ -0,0 +1,518 @@ +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/contribution_record.dart'; +import '../../providers/user_providers.dart'; +import '../../providers/contribution_providers.dart'; +import '../../widgets/shimmer_loading.dart'; + +/// 贡献值记录完整列表页面 +class ContributionRecordsPage extends ConsumerStatefulWidget { + const ContributionRecordsPage({super.key}); + + @override + ConsumerState createState() => _ContributionRecordsPageState(); +} + +class _ContributionRecordsPageState extends ConsumerState { + // 设计色彩 + static const Color _orange = Color(0xFFFF6B00); + static const Color _green = Color(0xFF22C55E); + static const Color _grayText = Color(0xFF6B7280); + static const Color _darkText = Color(0xFF1F2937); + static const Color _bgGray = Color(0xFFF3F4F6); + + int _currentPage = 1; + static const int _pageSize = 20; + + // 筛选条件 + ContributionSourceType? _selectedSourceType; + + @override + Widget build(BuildContext context) { + final user = ref.watch(userNotifierProvider); + final accountSequence = user.accountSequence ?? ''; + + final recordsParams = ContributionRecordsParams( + accountSequence: accountSequence, + page: _currentPage, + pageSize: _pageSize, + ); + final recordsAsync = ref.watch(contributionRecordsProvider(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: Column( + children: [ + // 筛选栏 + _buildFilterBar(), + // 列表 + Expanded( + child: RefreshIndicator( + onRefresh: () async { + setState(() => _currentPage = 1); + ref.invalidate(contributionRecordsProvider(recordsParams)); + }, + child: recordsAsync.when( + loading: () => _buildLoadingList(), + error: (error, stack) => _buildErrorView(error.toString()), + data: (recordsPage) { + if (recordsPage == null || recordsPage.data.isEmpty) { + return _buildEmptyView(); + } + return _buildRecordsList(recordsPage, accountSequence); + }, + ), + ), + ), + ], + ), + ); + } + + Widget _buildFilterBar() { + return Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + _buildFilterChip('全部', null), + const SizedBox(width: 8), + _buildFilterChip('个人', ContributionSourceType.personal), + const SizedBox(width: 8), + _buildFilterChip('团队层级', ContributionSourceType.teamLevel), + const SizedBox(width: 8), + _buildFilterChip('团队奖励', ContributionSourceType.teamBonus), + ], + ), + ); + } + + Widget _buildFilterChip(String label, ContributionSourceType? type) { + final isSelected = _selectedSourceType == type; + return GestureDetector( + onTap: () { + setState(() { + _selectedSourceType = type; + _currentPage = 1; + }); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: isSelected ? _orange : _bgGray, + borderRadius: BorderRadius.circular(16), + ), + child: Text( + label, + style: TextStyle( + fontSize: 13, + color: isSelected ? Colors.white : _grayText, + fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal, + ), + ), + ), + ); + } + + 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: const [ + ShimmerBox(width: 80, height: 24), + ShimmerBox(width: 100, height: 20), + ], + ), + const SizedBox(height: 12), + const ShimmerBox(width: 150, height: 14), + const SizedBox(height: 8), + Row( + children: const [ + ShimmerBox(width: 60, height: 12), + SizedBox(width: 16), + ShimmerBox(width: 80, height: 12), + SizedBox(width: 16), + ShimmerBox(width: 60, height: 12), + ], + ), + ], + ), + ); + } + + Widget _buildErrorView(String error) { + 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: () { + final user = ref.read(userNotifierProvider); + final accountSequence = user.accountSequence ?? ''; + ref.invalidate(contributionRecordsProvider(ContributionRecordsParams( + accountSequence: accountSequence, + page: _currentPage, + pageSize: _pageSize, + ))); + }, + 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(ContributionRecordsPage recordsPage, String currentAccountSequence) { + // 根据筛选条件过滤 + final filteredRecords = _selectedSourceType == null + ? recordsPage.data + : recordsPage.data.where((r) => r.sourceType == _selectedSourceType).toList(); + + if (filteredRecords.isEmpty) { + return _buildEmptyView(); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: filteredRecords.length + 1, // +1 for pagination info + itemBuilder: (context, index) { + if (index == filteredRecords.length) { + return _buildPaginationInfo(recordsPage); + } + return _buildRecordCard(filteredRecords[index], currentAccountSequence); + }, + ); + } + + Widget _buildRecordCard(ContributionRecord record, String currentAccountSequence) { + final dateFormat = DateFormat('yyyy-MM-dd HH:mm'); + + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: record.isExpired + ? Border.all(color: Colors.grey.withOpacity(0.3)) + : null, + ), + child: Opacity( + opacity: record.isExpired ? 0.6 : 1.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 第一行:来源类型 + 获得算力 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + _buildSourceTypeBadge(record.sourceType), + const SizedBox(width: 8), + if (record.levelDepth != null) + _buildLevelBadge('L${record.levelDepth}') + else if (record.bonusTier != null) + _buildLevelBadge('T${record.bonusTier}'), + ], + ), + Text( + '+${formatAmount(record.finalContribution)}', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: record.isExpired ? _grayText : _green, + ), + ), + ], + ), + const SizedBox(height: 12), + + // 第二行:来源用户 + Row( + children: [ + Icon(Icons.person_outline, size: 14, color: _grayText), + const SizedBox(width: 4), + Text( + record.sourceType == ContributionSourceType.personal + ? '本人认种' + : record.sourceAccountSequence ?? '未知用户', + style: TextStyle(fontSize: 13, color: _grayText), + ), + ], + ), + const SizedBox(height: 8), + + // 第三行:详细信息 + Wrap( + spacing: 16, + runSpacing: 4, + children: [ + _buildInfoItem('棵数', '${record.treeCount}'), + _buildInfoItem('基础算力', formatAmount(record.baseContribution)), + _buildInfoItem('分配比例', _formatPercent(record.distributionRate)), + ], + ), + const SizedBox(height: 8), + + // 第四行:日期信息 + Row( + children: [ + Expanded( + child: Row( + children: [ + Icon(Icons.calendar_today_outlined, size: 12, color: _grayText.withOpacity(0.7)), + const SizedBox(width: 4), + Text( + '生效: ${DateFormat('yyyy-MM-dd').format(record.effectiveDate)}', + style: TextStyle(fontSize: 11, color: _grayText.withOpacity(0.7)), + ), + ], + ), + ), + Row( + children: [ + Icon( + record.isExpired ? Icons.event_busy_outlined : Icons.event_available_outlined, + size: 12, + color: record.isExpired ? Colors.red.withOpacity(0.7) : _grayText.withOpacity(0.7), + ), + const SizedBox(width: 4), + Text( + '过期: ${DateFormat('yyyy-MM-dd').format(record.expireDate)}', + style: TextStyle( + fontSize: 11, + color: record.isExpired ? Colors.red.withOpacity(0.7) : _grayText.withOpacity(0.7), + ), + ), + ], + ), + ], + ), + + // 状态标签 + if (record.isExpired) ...[ + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + '已过期', + style: TextStyle(fontSize: 10, color: Colors.red), + ), + ), + ], + ], + ), + ), + ); + } + + Widget _buildSourceTypeBadge(ContributionSourceType type) { + String label; + Color bgColor; + Color textColor; + + switch (type) { + case ContributionSourceType.personal: + label = '个人认种'; + bgColor = _orange.withOpacity(0.1); + textColor = _orange; + break; + case ContributionSourceType.teamLevel: + label = '团队层级'; + bgColor = Colors.blue.withOpacity(0.1); + textColor = Colors.blue; + break; + case ContributionSourceType.teamBonus: + label = '团队奖励'; + bgColor = Colors.purple.withOpacity(0.1); + textColor = Colors.purple; + break; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + label, + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: textColor), + ), + ); + } + + Widget _buildLevelBadge(String label) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: _bgGray, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + label, + style: TextStyle(fontSize: 11, color: _grayText, fontWeight: FontWeight.w500), + ), + ); + } + + 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: TextStyle(fontSize: 12, color: _darkText, fontWeight: FontWeight.w500), + ), + ], + ); + } + + String _formatPercent(String rate) { + try { + final value = double.parse(rate); + return '${(value * 100).toStringAsFixed(1)}%'; + } catch (e) { + return rate; + } + } + + Widget _buildPaginationInfo(ContributionRecordsPage recordsPage) { + final 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), + ), + ], + ), + ], + ), + ); + } +} + +/// Shimmer box widget for loading state +class ShimmerBox extends StatelessWidget { + final double width; + final double height; + + const ShimmerBox({ + super.key, + required this.width, + required this.height, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: width, + height: height, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(4), + ), + ); + } +}