687 lines
23 KiB
Dart
687 lines
23 KiB
Dart
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(ref, contribution, isLoading),
|
||
const SizedBox(height: 16),
|
||
// 三栏统计
|
||
_buildThreeColumnStats(ref, contribution, isLoading),
|
||
const SizedBox(height: 16),
|
||
// 今日预估收益
|
||
_buildTodayEstimateCard(ref, estimatedEarnings, isLoading || isStatsLoading),
|
||
const SizedBox(height: 16),
|
||
// 贡献值明细(三类汇总)
|
||
_buildContributionDetailCard(context, ref, contribution, isLoading),
|
||
const SizedBox(height: 16),
|
||
// 团队层级统计
|
||
_buildTeamStatsCard(contribution, isLoading),
|
||
const SizedBox(height: 16),
|
||
// 贡献值失效倒计时
|
||
_buildExpirationCard(contribution, isLoading),
|
||
const SizedBox(height: 24),
|
||
]),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
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(WidgetRef ref, Contribution? contribution, bool isLoading) {
|
||
final total = contribution?.totalContribution ?? '0';
|
||
final hideAmounts = ref.watch(hideAmountsProvider);
|
||
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),
|
||
),
|
||
GestureDetector(
|
||
onTap: () {
|
||
ref.read(hideAmountsProvider.notifier).state = !hideAmounts;
|
||
},
|
||
child: Icon(
|
||
hideAmounts ? Icons.visibility_off_outlined : Icons.visibility_outlined,
|
||
color: _grayText.withOpacity(0.5),
|
||
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),
|
||
// 有效期标签
|
||
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(WidgetRef ref, Contribution? contribution, bool isLoading) {
|
||
final hideAmounts = ref.watch(hideAmountsProvider);
|
||
return Container(
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
_buildStatColumn('个人贡献值', contribution?.personalContribution, isLoading, false, hideAmounts),
|
||
_buildStatColumn('团队层级', contribution?.teamLevelContribution, isLoading, true, hideAmounts),
|
||
_buildStatColumn('团队奖励', contribution?.teamBonusContribution, isLoading, true, hideAmounts),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildStatColumn(String label, String? value, bool isLoading, bool showLeftBorder, bool hideAmounts) {
|
||
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 ? (hideAmounts ? '****' : formatAmount(value)) : null,
|
||
isLoading: isLoading,
|
||
placeholder: '--',
|
||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _darkText),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildTodayEstimateCard(WidgetRef ref, EstimatedEarnings earnings, bool isLoading) {
|
||
final hideAmounts = ref.watch(hideAmountsProvider);
|
||
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: 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 hideAmounts = ref.watch(hideAmountsProvider);
|
||
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',
|
||
hideAmounts: hideAmounts,
|
||
),
|
||
const Divider(height: 24),
|
||
_buildDetailSummaryRow(
|
||
icon: Icons.groups_outlined,
|
||
iconColor: Colors.blue,
|
||
title: '团队层级',
|
||
subtitle: '直推及间推用户认种产生的贡献值',
|
||
amount: contribution?.teamLevelContribution ?? '0',
|
||
hideAmounts: hideAmounts,
|
||
),
|
||
const Divider(height: 24),
|
||
_buildDetailSummaryRow(
|
||
icon: Icons.card_giftcard_outlined,
|
||
iconColor: Colors.purple,
|
||
title: '团队奖励',
|
||
subtitle: '满足条件后获得的额外奖励贡献值',
|
||
amount: contribution?.teamBonusContribution ?? '0',
|
||
hideAmounts: hideAmounts,
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
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,
|
||
required bool hideAmounts,
|
||
}) {
|
||
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(
|
||
hideAmounts ? '****' : 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),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|