rwadurian/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart

718 lines
25 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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