feat(mining-app): 贡献值730天失效倒计时功能
将贡献值页面的"贡献值失效倒计时"从硬编码静态文字改为基于用户 首次挖矿时间的真实730天倒计时。纯新增方式实现,不影响现有功能。 后端 (mining-service): - get-mining-account.query.ts: MiningAccountDto 新增 firstMiningDate 字段,在 Promise.all 中并行查询用户最早的 miningRecord,利用 @@unique([accountSequence, miningMinute]) 索引高效查询 前端实体/模型: - share_account.dart: 新增 DateTime? firstMiningDate(可空,向后兼容) - share_account_model.dart: fromJson/toJson 解析和序列化 firstMiningDate 前端 UI (contribution_page.dart): - watch shareAccountProvider 获取首次挖矿时间 - 计算已过天数和剩余天数(730 - 已过天数) - 进度条显示实际已用时间占比 - 显示具体失效日期和剩余天数 - 无挖矿记录 → 显示"暂无挖矿记录" - 已过期 → 显示"贡献值已失效" - 剩余 ≤30 天 → 进度条和文字变红色警告 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6082725c80
commit
bc3d800936
|
|
@ -12,6 +12,7 @@ export interface MiningAccountDto {
|
|||
totalContribution: string;
|
||||
perSecondEarning: string; // 每秒收益 = (用户贡献 / 全网贡献) × 每秒分配量
|
||||
lastSyncedAt: Date | null;
|
||||
firstMiningDate: Date | null; // 首次挖矿时间(用于730天倒计时)
|
||||
}
|
||||
|
||||
export interface MiningRecordDto {
|
||||
|
|
@ -53,12 +54,20 @@ export class GetMiningAccountQuery {
|
|||
// 计算每秒收益 = (用户贡献 / 全网贡献) × 每秒分配量
|
||||
// 只有在挖矿系统激活时才返回非零值
|
||||
let perSecondEarning = '0';
|
||||
let firstMiningDate: Date | null = null;
|
||||
try {
|
||||
const [config, totalContribution] = await Promise.all([
|
||||
const [config, totalContribution, firstRecord] = await Promise.all([
|
||||
this.configRepository.getConfig(),
|
||||
this.accountRepository.getTotalContribution(),
|
||||
this.prisma.miningRecord.findFirst({
|
||||
where: { accountSequence },
|
||||
orderBy: { miningMinute: 'asc' },
|
||||
select: { miningMinute: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
firstMiningDate = firstRecord?.miningMinute ?? null;
|
||||
|
||||
// 检查挖矿系统是否激活
|
||||
if (config && config.isActive && totalContribution.value.toNumber() > 0) {
|
||||
const userContribution = account.totalContribution.value.toNumber();
|
||||
|
|
@ -79,6 +88,7 @@ export class GetMiningAccountQuery {
|
|||
totalContribution: account.totalContribution.toString(),
|
||||
perSecondEarning,
|
||||
lastSyncedAt: account.lastSyncedAt,
|
||||
firstMiningDate,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ class ShareAccountModel extends ShareAccount {
|
|||
required super.totalMined,
|
||||
required super.perSecondEarning,
|
||||
required super.effectiveContribution,
|
||||
super.firstMiningDate,
|
||||
});
|
||||
|
||||
factory ShareAccountModel.fromJson(Map<String, dynamic> json) {
|
||||
|
|
@ -23,6 +24,9 @@ class ShareAccountModel extends ShareAccount {
|
|||
perSecondEarning: json['perSecondEarning']?.toString() ?? '0',
|
||||
// 后端返回 totalContribution,映射到 effectiveContribution
|
||||
effectiveContribution: json['totalContribution']?.toString() ?? json['effectiveContribution']?.toString() ?? '0',
|
||||
firstMiningDate: json['firstMiningDate'] != null
|
||||
? DateTime.tryParse(json['firstMiningDate'].toString())
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -35,6 +39,7 @@ class ShareAccountModel extends ShareAccount {
|
|||
'totalMined': totalMined,
|
||||
'perSecondEarning': perSecondEarning,
|
||||
'effectiveContribution': effectiveContribution,
|
||||
'firstMiningDate': firstMiningDate?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ class ShareAccount extends Equatable {
|
|||
final String totalMined;
|
||||
final String perSecondEarning;
|
||||
final String effectiveContribution;
|
||||
final DateTime? firstMiningDate;
|
||||
|
||||
const ShareAccount({
|
||||
required this.accountSequence,
|
||||
|
|
@ -17,6 +18,7 @@ class ShareAccount extends Equatable {
|
|||
required this.totalMined,
|
||||
required this.perSecondEarning,
|
||||
required this.effectiveContribution,
|
||||
this.firstMiningDate,
|
||||
});
|
||||
|
||||
String get totalBalance {
|
||||
|
|
@ -35,5 +37,6 @@ class ShareAccount extends Equatable {
|
|||
totalMined,
|
||||
perSecondEarning,
|
||||
effectiveContribution,
|
||||
firstMiningDate,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ import '../../../core/constants/app_colors.dart';
|
|||
import '../../../core/router/routes.dart';
|
||||
import '../../../core/utils/format_utils.dart';
|
||||
import '../../../domain/entities/contribution.dart';
|
||||
import '../../../domain/entities/share_account.dart';
|
||||
import '../../providers/user_providers.dart';
|
||||
import '../../providers/contribution_providers.dart';
|
||||
import '../../providers/mining_providers.dart';
|
||||
import '../../widgets/shimmer_loading.dart';
|
||||
|
||||
class ContributionPage extends ConsumerWidget {
|
||||
|
|
@ -25,6 +27,8 @@ class ContributionPage extends ConsumerWidget {
|
|||
final estimatedEarnings = ref.watch(estimatedEarningsProvider(accountSequence));
|
||||
// 获取积分股池余量
|
||||
final sharePoolAsync = ref.watch(sharePoolBalanceProvider);
|
||||
// 获取挖矿账户信息(含首次挖矿时间,用于730天倒计时)
|
||||
final shareAccountAsync = ref.watch(shareAccountProvider(accountSequence));
|
||||
|
||||
// Extract loading state and data from AsyncValue
|
||||
final isLoading = contributionAsync.isLoading;
|
||||
|
|
@ -42,6 +46,7 @@ class ContributionPage extends ConsumerWidget {
|
|||
onRefresh: () async {
|
||||
ref.invalidate(contributionProvider(accountSequence));
|
||||
ref.invalidate(sharePoolBalanceProvider);
|
||||
ref.invalidate(shareAccountProvider(accountSequence));
|
||||
},
|
||||
child: hasError && contribution == null
|
||||
? Center(
|
||||
|
|
@ -84,7 +89,7 @@ class ContributionPage extends ConsumerWidget {
|
|||
_buildTeamStatsCard(context, contribution, isLoading),
|
||||
const SizedBox(height: 16),
|
||||
// 贡献值失效倒计时
|
||||
_buildExpirationCard(context, contribution, isLoading),
|
||||
_buildExpirationCard(context, contribution, isLoading, shareAccountAsync.valueOrNull),
|
||||
const SizedBox(height: 24),
|
||||
]),
|
||||
),
|
||||
|
|
@ -648,19 +653,42 @@ class ContributionPage extends ConsumerWidget {
|
|||
BuildContext context,
|
||||
Contribution? contribution,
|
||||
bool isLoading,
|
||||
ShareAccount? shareAccount,
|
||||
) {
|
||||
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 DateTime? firstMiningDate = shareAccount?.firstMiningDate;
|
||||
|
||||
int daysElapsed = 0;
|
||||
int daysRemaining = validityDays;
|
||||
double progress = 0.0;
|
||||
String expireDateText;
|
||||
String countdownText;
|
||||
|
||||
if (firstMiningDate == null) {
|
||||
expireDateText = '暂无挖矿记录';
|
||||
countdownText = '';
|
||||
} else {
|
||||
daysElapsed = DateTime.now().difference(firstMiningDate).inDays;
|
||||
daysRemaining = (validityDays - daysElapsed).clamp(0, validityDays);
|
||||
progress = (daysElapsed / validityDays).clamp(0.0, 1.0);
|
||||
|
||||
if (daysRemaining <= 0) {
|
||||
expireDateText = '贡献值已失效';
|
||||
countdownText = '已超过 $validityDays 天有效期';
|
||||
} else {
|
||||
final expirationDate = firstMiningDate.add(const Duration(days: validityDays));
|
||||
final expirationStr =
|
||||
'${expirationDate.year}-${expirationDate.month.toString().padLeft(2, '0')}-${expirationDate.day.toString().padLeft(2, '0')}';
|
||||
expireDateText = '贡献值将于 $expirationStr 失效';
|
||||
countdownText = '剩余 $daysRemaining 天';
|
||||
}
|
||||
}
|
||||
|
||||
final bool isUrgent = firstMiningDate != null && daysRemaining <= 30 && daysRemaining > 0;
|
||||
final Color activeColor = isUrgent ? Colors.red : _orange;
|
||||
final bgGrayColor = isDark ? AppColors.backgroundOf(context) : const Color(0xFFF3F4F6);
|
||||
|
||||
return Container(
|
||||
|
|
@ -684,7 +712,7 @@ class ContributionPage extends ConsumerWidget {
|
|||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 进度条
|
||||
// 进度条(已用时间占比)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
child: LinearProgressIndicator(
|
||||
|
|
@ -692,7 +720,7 @@ class ContributionPage extends ConsumerWidget {
|
|||
minHeight: 10,
|
||||
backgroundColor: bgGrayColor,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
isLoading ? bgGrayColor : _orange,
|
||||
isLoading ? bgGrayColor : activeColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -707,16 +735,18 @@ class ContributionPage extends ConsumerWidget {
|
|||
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),
|
||||
),
|
||||
if (countdownText.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
isLoading
|
||||
? const ShimmerText(
|
||||
placeholder: '剩余 --- 天',
|
||||
style: TextStyle(fontSize: 12, color: _orange, fontWeight: FontWeight.w500),
|
||||
)
|
||||
: Text(
|
||||
countdownText,
|
||||
style: TextStyle(fontSize: 12, color: activeColor, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue