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

686 lines
23 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:intl/intl.dart';
import '../../../core/constants/app_colors.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';
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 recordsParams = ContributionRecordsParams(
accountSequence: accountSequence,
page: 1,
pageSize: 3,
);
final recordsAsync = ref.watch(contributionRecordsProvider(recordsParams));
// Extract loading state and data from AsyncValue
final isLoading = contributionAsync.isLoading;
final contribution = contributionAsync.valueOrNull;
final hasError = contributionAsync.hasError;
final error = contributionAsync.error;
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
body: SafeArea(
bottom: false,
child: RefreshIndicator(
onRefresh: () async {
ref.invalidate(contributionProvider(accountSequence));
ref.invalidate(contributionRecordsProvider(recordsParams));
},
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(contribution, isLoading),
const SizedBox(height: 16),
// 贡献值明细
_buildContributionDetailCard(context, ref, recordsAsync),
const SizedBox(height: 16),
// 团队层级统计
_buildTeamStatsCard(contribution, isLoading),
const SizedBox(height: 16),
// 贡献值失效倒计时
_buildExpirationCard(contribution, recordsAsync, 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?.effectiveContribution ?? '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?.systemContribution, 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(Contribution? contribution, bool isLoading) {
// 基于贡献值计算预估收益暂时显示占位符后续可接入实际计算API
final effectiveContribution = double.tryParse(contribution?.effectiveContribution ?? '0') ?? 0;
// 简单估算:假设每日发放总量为 10000 积分股,按贡献值占比分配
// 这里先显示"--"表示暂无数据,后续可接入实际计算
final hasContribution = effectiveContribution > 0;
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: hasContribution ? '计算中' : '--',
style: TextStyle(
fontSize: hasContribution ? 14 : 18,
fontWeight: FontWeight.bold,
color: _green,
),
),
const TextSpan(
text: ' 积分股',
style: TextStyle(fontSize: 12, color: _green),
),
],
),
),
],
),
],
),
);
}
Widget _buildContributionDetailCard(
BuildContext context,
WidgetRef ref,
AsyncValue<ContributionRecordsPage?> recordsAsync,
) {
final isRecordsLoading = recordsAsync.isLoading;
final recordsPage = recordsAsync.valueOrNull;
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: () {
// TODO: 跳转到完整记录页面
},
child: const Row(
children: [
Text('查看全部', style: TextStyle(fontSize: 12, color: _orange)),
Icon(Icons.chevron_right, size: 14, color: _orange),
],
),
),
],
),
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),
),
)
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(),
),
],
),
);
}
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,
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),
),
],
),
const ShimmerText(
placeholder: '+1,000',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: _green),
),
],
);
}
Widget _buildDetailRow(ContributionRecord record) {
final dateFormat = DateFormat('yyyy-MM-dd HH:mm');
final formattedDate = dateFormat.format(record.createdAt);
final amount = '+${formatAmount(record.finalContribution)}';
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)),
],
),
Text(
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,
AsyncValue<ContributionRecordsPage?> recordsAsync,
bool isLoading,
) {
final isRecordsLoading = recordsAsync.isLoading;
final recordsPage = recordsAsync.valueOrNull;
// 从记录中获取最近的过期日期
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;
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: showShimmer ? 1.0 : progress,
minHeight: 10,
backgroundColor: _bgGray,
valueColor: AlwaysStoppedAnimation<Color>(
showShimmer ? _bgGray : _orange,
),
),
),
const SizedBox(height: 12),
// 说明文字
showShimmer
? const ShimmerText(
placeholder: '您的贡献值将于 ---- 失效',
style: TextStyle(fontSize: 12, color: _grayText),
)
: Text(
expireDateText,
style: const TextStyle(fontSize: 12, color: _grayText),
),
const SizedBox(height: 4),
showShimmer
? const ShimmerText(
placeholder: '剩余 --- 天',
style: TextStyle(fontSize: 12, color: _orange, fontWeight: FontWeight.w500),
)
: Text(
'剩余 $daysRemaining',
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),
),
),
],
),
);
}
}