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

669 lines
22 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 = 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);
static const Color _lightGray = Color(0xFFF9FAFB);
@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);
// 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;
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
body: SafeArea(
bottom: false,
child: RefreshIndicator(
onRefresh: () async {
ref.invalidate(contributionProvider(accountSequence));
ref.invalidate(contributionStatsProvider);
},
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(contribution, isLoading),
const SizedBox(height: 16),
// 三栏统计
_buildThreeColumnStats(contribution, isLoading),
const SizedBox(height: 16),
// 今日预估收益
_buildTodayEstimateCard(estimatedEarnings, isLoading || isStatsLoading),
const SizedBox(height: 16),
// 贡献值明细(三类汇总)
_buildContributionDetailCard(context, contribution, isLoading),
const SizedBox(height: 16),
// 团队层级统计
_buildTeamStatsCard(contribution, isLoading),
const SizedBox(height: 16),
// 贡献值失效倒计时
_buildExpirationCard(contribution, isLoading),
const SizedBox(height: 100),
]),
),
),
],
),
),
),
);
}
Widget _buildAppBar(BuildContext context) {
return Container(
color: _lightGray,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
// Logo
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: _orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.eco, color: _orange, size: 20),
),
const SizedBox(width: 8),
const Text(
'榴莲生态',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: _darkText,
letterSpacing: 0.45,
),
),
const Spacer(),
// 客服
IconButton(
icon: const Icon(Icons.headset_mic_outlined, color: _grayText),
onPressed: () {},
),
// 通知(带红点)
Stack(
children: [
IconButton(
icon: const Icon(Icons.notifications_outlined, color: _grayText),
onPressed: () {},
),
Positioned(
right: 10,
top: 10,
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
),
],
),
],
),
);
}
Widget _buildTotalContributionCard(Contribution? contribution, bool isLoading) {
final total = contribution?.totalContribution ?? '0';
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'总贡献值',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: _grayText),
),
Icon(Icons.visibility_outlined, color: _grayText.withOpacity(0.5), size: 18),
],
),
const SizedBox(height: 8),
DataText(
data: isLoading ? null : formatAmount(total),
isLoading: isLoading,
placeholder: '----',
style: const TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: _orange,
letterSpacing: -0.75,
),
),
const SizedBox(height: 12),
// 有效期标签
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _lightGray,
borderRadius: BorderRadius.circular(999),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.info_outline, size: 14, color: _grayText.withOpacity(0.7)),
const SizedBox(width: 6),
Text(
'贡献值有效期: 730 天',
style: TextStyle(fontSize: 12, color: _grayText.withOpacity(0.9)),
),
],
),
),
],
),
);
}
Widget _buildThreeColumnStats(Contribution? contribution, bool isLoading) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
_buildStatColumn('个人贡献值', contribution?.personalContribution, isLoading, false),
_buildStatColumn('团队层级', contribution?.teamLevelContribution, isLoading, true),
_buildStatColumn('团队奖励', contribution?.teamBonusContribution, isLoading, true),
],
),
);
}
Widget _buildStatColumn(String label, String? value, bool isLoading, bool showLeftBorder) {
return Expanded(
child: Container(
decoration: showLeftBorder
? const BoxDecoration(
border: Border(left: BorderSide(color: Color(0xFFE5E7EB), width: 1)),
)
: null,
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Column(
children: [
Text(label, style: const TextStyle(fontSize: 12, color: _grayText)),
const SizedBox(height: 4),
DataText(
data: value != null ? formatAmount(value) : null,
isLoading: isLoading,
placeholder: '--',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _darkText),
textAlign: TextAlign.center,
),
],
),
),
);
}
Widget _buildTodayEstimateCard(EstimatedEarnings earnings, bool isLoading) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
// 图标
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: _green.withOpacity(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: [
const Text(
'今日预估收益',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: _grayText),
),
Text(
'基于当前贡献值占比计算',
style: TextStyle(fontSize: 12, color: _grayText.withOpacity(0.7)),
),
],
),
),
// 收益数值
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
isLoading
? const ShimmerText(
placeholder: '-- 积分股',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _green),
)
: Text.rich(
TextSpan(
children: [
TextSpan(
text: 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,
Contribution? contribution,
bool isLoading,
) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
// 标题行
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'贡献值明细',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: _darkText),
),
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()
else
Column(
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 _buildDetailSummaryShimmer() {
return Column(
children: [
_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',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: _green),
),
],
);
}
Widget _buildDetailSummaryRow({
required IconData icon,
required Color iconColor,
required String title,
required String subtitle,
required String amount,
}) {
return Row(
children: [
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(
formatAmount(amount),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: _green),
),
],
);
}
Widget _buildTeamStatsCard(Contribution? contribution, bool isLoading) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'团队层级统计',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: _darkText),
),
const SizedBox(height: 16),
// 第一行
Row(
children: [
_buildTeamStatItem(
'直推人数',
contribution?.directReferralAdoptedCount.toString(),
'',
isLoading,
),
const SizedBox(width: 16),
_buildTeamStatItem(
'已解锁奖励',
contribution?.unlockedBonusTiers.toString(),
'',
isLoading,
),
],
),
const SizedBox(height: 16),
// 第二行
Row(
children: [
_buildTeamStatItem(
'已解锁层级',
contribution?.unlockedLevelDepth.toString(),
'',
isLoading,
),
const SizedBox(width: 16),
_buildTeamStatItem(
'是否认种',
contribution != null ? (contribution.hasAdopted == true ? '' : '') : null,
'',
isLoading,
),
],
),
],
),
);
}
Widget _buildTeamStatItem(String label, String? value, String unit, bool isLoading) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _bgGray,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontSize: 12, color: _grayText)),
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: const TextStyle(fontSize: 12, color: _grayText),
),
],
),
),
],
),
),
);
}
Widget _buildExpirationCard(
Contribution? contribution,
bool isLoading,
) {
// 贡献值有效期为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;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
const Row(
children: [
Icon(Icons.timer_outlined, color: _orange, size: 24),
SizedBox(width: 8),
Text(
'贡献值失效倒计时',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: _darkText),
),
],
),
const SizedBox(height: 12),
// 进度条
ClipRRect(
borderRadius: BorderRadius.circular(5),
child: LinearProgressIndicator(
value: isLoading ? 1.0 : progress,
minHeight: 10,
backgroundColor: _bgGray,
valueColor: AlwaysStoppedAnimation<Color>(
isLoading ? _bgGray : _orange,
),
),
),
const SizedBox(height: 12),
// 说明文字
isLoading
? const ShimmerText(
placeholder: '贡献值有效期信息加载中...',
style: TextStyle(fontSize: 12, color: _grayText),
)
: Text(
expireDateText,
style: const TextStyle(fontSize: 12, color: _grayText),
),
const SizedBox(height: 4),
isLoading
? const ShimmerText(
placeholder: '有效期 --- 天',
style: TextStyle(fontSize: 12, color: _orange, fontWeight: FontWeight.w500),
)
: Text(
'有效期 $validityDays',
style: const TextStyle(fontSize: 12, color: _orange, fontWeight: FontWeight.w500),
),
const SizedBox(height: 8),
// 提示
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _bgGray,
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'* 运营账号贡献值永不失效',
style: TextStyle(fontSize: 10, color: _orange),
),
),
],
),
);
}
}