import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.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 '../../providers/user_providers.dart'; import '../../providers/contribution_providers.dart'; import '../../widgets/shimmer_loading.dart'; class ContributionPage extends ConsumerWidget { const ContributionPage({super.key}); // 品牌色彩(不随主题变化) static const Color _orange = AppColors.orange; static const Color _green = AppColors.primary; @override Widget build(BuildContext context, WidgetRef ref) { final user = ref.watch(userNotifierProvider); final accountSequence = user.accountSequence ?? ''; final contributionAsync = ref.watch(contributionProvider(accountSequence)); // 获取预估收益 final estimatedEarnings = ref.watch(estimatedEarningsProvider(accountSequence)); final statsAsync = ref.watch(contributionStatsProvider); // 获取积分股池余量 final sharePoolAsync = ref.watch(sharePoolBalanceProvider); // Extract loading state and data from AsyncValue final isLoading = contributionAsync.isLoading; final contribution = contributionAsync.valueOrNull; final hasError = contributionAsync.hasError; final error = contributionAsync.error; final isStatsLoading = statsAsync.isLoading; final isSharePoolLoading = sharePoolAsync.isLoading; final sharePoolBalance = sharePoolAsync.valueOrNull; return Scaffold( backgroundColor: AppColors.backgroundOf(context), body: SafeArea( bottom: false, child: RefreshIndicator( onRefresh: () async { ref.invalidate(contributionProvider(accountSequence)); ref.invalidate(contributionStatsProvider); ref.invalidate(sharePoolBalanceProvider); }, child: hasError && contribution == null ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, size: 48, color: AppColors.error), const SizedBox(height: 16), Text('加载失败: $error'), const SizedBox(height: 16), ElevatedButton( onPressed: () => ref.invalidate(contributionProvider(accountSequence)), child: const Text('重试'), ), ], ), ) : CustomScrollView( slivers: [ // 顶部导航栏 SliverToBoxAdapter(child: _buildAppBar(context)), // 内容 SliverPadding( padding: const EdgeInsets.all(16), sliver: SliverList( delegate: SliverChildListDelegate([ // 总贡献值卡片 _buildTotalContributionCard(context, ref, contribution, isLoading, sharePoolBalance, isSharePoolLoading), const SizedBox(height: 16), // 三栏统计 _buildThreeColumnStats(context, ref, contribution, isLoading), const SizedBox(height: 16), // 今日预估收益 _buildTodayEstimateCard(context, ref, estimatedEarnings, isLoading || isStatsLoading), const SizedBox(height: 16), // 贡献值明细(三类汇总) _buildContributionDetailCard(context, ref, contribution, isLoading), const SizedBox(height: 16), // 团队下贡献值统计 _buildTeamStatsCard(context, contribution, isLoading), const SizedBox(height: 16), // 贡献值失效倒计时 _buildExpirationCard(context, contribution, isLoading), const SizedBox(height: 24), ]), ), ), ], ), ), ), ); } Widget _buildAppBar(BuildContext context) { final isDark = AppColors.isDark(context); return Container( color: AppColors.cardOf(context), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ // Logo Container( width: 32, height: 32, decoration: BoxDecoration( color: _orange.withOpacity(isDark ? 0.2 : 0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.eco, color: _orange, size: 20), ), const SizedBox(width: 8), Text( '股行', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.textPrimaryOf(context), letterSpacing: 0.45, ), ), const Spacer(), // 客服 IconButton( icon: Icon(Icons.headset_mic_outlined, color: AppColors.textSecondaryOf(context)), onPressed: () {}, ), // 通知(带红点) Stack( children: [ IconButton( icon: Icon(Icons.notifications_outlined, color: AppColors.textSecondaryOf(context)), onPressed: () {}, ), Positioned( right: 10, top: 10, child: Container( width: 8, height: 8, decoration: const BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), ), ), ], ), ], ), ); } Widget _buildTotalContributionCard( BuildContext context, WidgetRef ref, Contribution? contribution, bool isLoading, SharePoolBalance? sharePoolBalance, bool isSharePoolLoading, ) { final isDark = AppColors.isDark(context); final total = contribution?.totalContribution ?? '0'; final hideAmounts = ref.watch(hideAmountsProvider); return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.cardOf(context), borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '总贡献值', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.textSecondaryOf(context)), ), GestureDetector( onTap: () { ref.read(hideAmountsProvider.notifier).state = !hideAmounts; }, child: Icon( hideAmounts ? Icons.visibility_off_outlined : Icons.visibility_outlined, color: AppColors.textMutedOf(context), size: 18, ), ), ], ), const SizedBox(height: 8), DataText( data: isLoading ? null : (hideAmounts ? '******' : formatAmount(total)), isLoading: isLoading, placeholder: '----', style: const TextStyle( fontSize: 30, fontWeight: FontWeight.bold, color: _orange, letterSpacing: -0.75, ), ), const SizedBox(height: 12), // 积分股池实时余量 Row( children: [ Text( '积分股池实时余量: ', style: TextStyle(fontSize: 12, color: AppColors.textSecondaryOf(context)), ), isSharePoolLoading ? const ShimmerText( placeholder: '----', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: _orange), ) : Text( hideAmounts ? '******' : formatAmount(sharePoolBalance?.total ?? '0'), style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: _orange), ), ], ), const SizedBox(height: 8), // 有效期标签 Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: isDark ? AppColors.backgroundOf(context) : const Color(0xFFF9FAFB), borderRadius: BorderRadius.circular(999), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.info_outline, size: 14, color: AppColors.textMutedOf(context)), const SizedBox(width: 6), Text( '贡献值有效期: 730 天', style: TextStyle(fontSize: 12, color: AppColors.textSecondaryOf(context)), ), ], ), ), ], ), ); } Widget _buildThreeColumnStats(BuildContext context, WidgetRef ref, Contribution? contribution, bool isLoading) { final hideAmounts = ref.watch(hideAmountsProvider); return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.cardOf(context), borderRadius: BorderRadius.circular(16), ), child: Row( children: [ _buildStatColumn(context, '个人贡献值', contribution?.personalContribution, isLoading, false, hideAmounts), _buildStatColumn(context, '团队下贡献值', contribution?.teamLevelContribution, isLoading, true, hideAmounts), _buildStatColumn(context, '团队上贡献值', contribution?.teamBonusContribution, isLoading, true, hideAmounts), ], ), ); } Widget _buildStatColumn(BuildContext context, String label, String? value, bool isLoading, bool showLeftBorder, bool hideAmounts) { return Expanded( child: Container( decoration: showLeftBorder ? BoxDecoration( border: Border(left: BorderSide(color: AppColors.borderOf(context), width: 1)), ) : null, padding: const EdgeInsets.symmetric(horizontal: 4), child: Column( children: [ Text(label, style: TextStyle(fontSize: 12, color: AppColors.textSecondaryOf(context))), const SizedBox(height: 4), DataText( data: value != null ? (hideAmounts ? '****' : formatAmount(value)) : null, isLoading: isLoading, placeholder: '--', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: AppColors.textPrimaryOf(context)), textAlign: TextAlign.center, ), ], ), ), ); } Widget _buildTodayEstimateCard(BuildContext context, WidgetRef ref, EstimatedEarnings earnings, bool isLoading) { final isDark = AppColors.isDark(context); final hideAmounts = ref.watch(hideAmountsProvider); return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.cardOf(context), borderRadius: BorderRadius.circular(16), ), child: Row( children: [ // 图标 Container( width: 40, height: 40, decoration: BoxDecoration( color: _green.withOpacity(isDark ? 0.2 : 0.1), borderRadius: BorderRadius.circular(12), ), child: const Icon(Icons.trending_up, color: _green, size: 24), ), const SizedBox(width: 12), // 文字说明 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '今日预估收益', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.textSecondaryOf(context)), ), Text( '基于当前贡献值占比计算', style: TextStyle(fontSize: 12, color: AppColors.textMutedOf(context)), ), ], ), ), // 收益数值 Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ isLoading ? const ShimmerText( placeholder: '-- 积分股', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _green), ) : Text.rich( TextSpan( children: [ TextSpan( text: hideAmounts ? '****' : (earnings.isValid ? formatAmount(earnings.dailyShares) : '--'), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: _green, ), ), const TextSpan( text: ' 积分股', style: TextStyle(fontSize: 12, color: _green), ), ], ), ), ], ), ], ), ); } Widget _buildContributionDetailCard( BuildContext context, WidgetRef ref, Contribution? contribution, bool isLoading, ) { final isDark = AppColors.isDark(context); final hideAmounts = ref.watch(hideAmountsProvider); return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.cardOf(context), borderRadius: BorderRadius.circular(16), ), child: Column( children: [ // 标题行 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '贡献值明细', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: AppColors.textPrimaryOf(context)), ), GestureDetector( onTap: () { context.push(Routes.contributionRecords); }, child: const Row( children: [ Text('查看全部', style: TextStyle(fontSize: 12, color: _orange)), Icon(Icons.chevron_right, size: 14, color: _orange), ], ), ), ], ), const SizedBox(height: 16), // 三类汇总 if (isLoading) _buildDetailSummaryShimmer(context) else Column( children: [ _buildDetailSummaryRow( context: context, isDark: isDark, icon: Icons.eco_outlined, iconColor: _orange, title: '本人', subtitle: '个人参与产生的贡献值', amount: contribution?.personalContribution ?? '0', hideAmounts: hideAmounts, ), Divider(height: 24, color: AppColors.borderOf(context)), _buildDetailSummaryRow( context: context, isDark: isDark, icon: Icons.groups_outlined, iconColor: Colors.blue, title: '团队下贡献值', subtitle: '股行用户引荐', amount: contribution?.teamLevelContribution ?? '0', hideAmounts: hideAmounts, ), Divider(height: 24, color: AppColors.borderOf(context)), _buildDetailSummaryRow( context: context, isDark: isDark, icon: Icons.card_giftcard_outlined, iconColor: Colors.purple, title: '团队上贡献值', subtitle: '满足条件后获得的额外奖励贡献值', amount: contribution?.teamBonusContribution ?? '0', hideAmounts: hideAmounts, ), ], ), ], ), ); } Widget _buildDetailSummaryShimmer(BuildContext context) { return Column( children: [ _buildShimmerSummaryRow(context), Divider(height: 24, color: AppColors.borderOf(context)), _buildShimmerSummaryRow(context), Divider(height: 24, color: AppColors.borderOf(context)), _buildShimmerSummaryRow(context), ], ); } Widget _buildShimmerSummaryRow(BuildContext context) { return Row( children: [ const ShimmerBox(width: 40, height: 40), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ShimmerText( placeholder: '本人', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.textPrimaryOf(context)), ), const SizedBox(height: 2), ShimmerText( placeholder: '个人参与产生的贡献值', style: TextStyle(fontSize: 12, color: AppColors.textSecondaryOf(context)), ), ], ), ), const ShimmerText( placeholder: '+1,000', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: _green), ), ], ); } Widget _buildDetailSummaryRow({ required BuildContext context, required bool isDark, required IconData icon, required Color iconColor, required String title, required String subtitle, required String amount, required bool hideAmounts, }) { return Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: iconColor.withOpacity(isDark ? 0.2 : 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: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.textPrimaryOf(context)), ), const SizedBox(height: 2), Text( subtitle, style: TextStyle(fontSize: 11, color: AppColors.textMutedOf(context)), ), ], ), ), Text( hideAmounts ? '****' : formatAmount(amount), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: _green), ), ], ); } Widget _buildTeamStatsCard(BuildContext context, Contribution? contribution, bool isLoading) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.cardOf(context), borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '团队下贡献值统计', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: AppColors.textPrimaryOf(context)), ), const SizedBox(height: 16), // 第一行 Row( children: [ _buildTeamStatItem( context, '引荐人数', contribution?.directReferralAdoptedCount.toString(), '人', isLoading, ), const SizedBox(width: 16), _buildTeamStatItem( context, '已解锁上级', '15', '', isLoading, ), ], ), const SizedBox(height: 16), // 第二行 Row( children: [ _buildTeamStatItem( context, '已解锁下级', contribution?.unlockedLevelDepth.toString(), '', isLoading, ), const SizedBox(width: 16), _buildTeamStatItem( context, '是否参与', contribution != null ? (contribution.hasAdopted == true ? '是' : '否') : null, '', isLoading, ), ], ), ], ), ); } Widget _buildTeamStatItem(BuildContext context, String label, String? value, String unit, bool isLoading) { final isDark = AppColors.isDark(context); return Expanded( child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isDark ? AppColors.backgroundOf(context) : const Color(0xFFF3F4F6), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle(fontSize: 12, color: AppColors.textSecondaryOf(context))), const SizedBox(height: 4), isLoading ? ShimmerText( placeholder: unit.isNotEmpty ? '-- $unit' : '--', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: _orange), ) : Text.rich( TextSpan( children: [ TextSpan( text: '${value ?? '0'} ', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: _orange), ), if (unit.isNotEmpty) TextSpan( text: unit, style: TextStyle(fontSize: 12, color: AppColors.textSecondaryOf(context)), ), ], ), ), ], ), ), ); } Widget _buildExpirationCard( BuildContext context, Contribution? contribution, bool isLoading, ) { final isDark = AppColors.isDark(context); // 贡献值有效期为2年(730天) // 暂时使用固定信息,后续可从后端获取最近过期日期 const int validityDays = 730; final hasContribution = contribution != null && (double.tryParse(contribution.totalContribution) ?? 0) > 0; // 如果有贡献值,显示有效期提示 final String expireDateText = hasContribution ? '贡献值自生效日起 $validityDays 天内有效' : '暂无贡献值'; final double progress = hasContribution ? 1.0 : 0.0; final bgGrayColor = isDark ? AppColors.backgroundOf(context) : const Color(0xFFF3F4F6); return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.cardOf(context), borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题 Row( children: [ const Icon(Icons.timer_outlined, color: _orange, size: 24), const SizedBox(width: 8), Text( '贡献值失效倒计时', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: AppColors.textPrimaryOf(context)), ), ], ), const SizedBox(height: 12), // 进度条 ClipRRect( borderRadius: BorderRadius.circular(5), child: LinearProgressIndicator( value: isLoading ? 1.0 : progress, minHeight: 10, backgroundColor: bgGrayColor, valueColor: AlwaysStoppedAnimation( isLoading ? bgGrayColor : _orange, ), ), ), const SizedBox(height: 12), // 说明文字 isLoading ? ShimmerText( placeholder: '贡献值有效期信息加载中...', style: TextStyle(fontSize: 12, color: AppColors.textSecondaryOf(context)), ) : Text( expireDateText, style: TextStyle(fontSize: 12, color: AppColors.textSecondaryOf(context)), ), const SizedBox(height: 4), isLoading ? const ShimmerText( placeholder: '有效期 --- 天', style: TextStyle(fontSize: 12, color: _orange, fontWeight: FontWeight.w500), ) : const Text( '有效期 $validityDays 天', style: TextStyle(fontSize: 12, color: _orange, fontWeight: FontWeight.w500), ), ], ), ); } }