feat(profile): 支持显示多笔待领取奖励明细

- 新增 PendingRewardItem 数据模型,对接 GET /rewards/pending 接口
- 修改 ProfilePage 并行加载汇总数据和待领取列表
- 重构收益区域 UI,展示每笔奖励的权益类型、金额和独立倒计时

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-14 01:33:13 -08:00
parent cd742856c0
commit 6eea4463f8
2 changed files with 361 additions and 103 deletions

View File

@ -1,6 +1,65 @@
import 'package:flutter/foundation.dart';
import '../network/api_client.dart';
/// ( GET /rewards/pending )
class PendingRewardItem {
final String id;
final String rightType;
final double usdtAmount;
final double hashpowerAmount;
final DateTime createdAt;
final DateTime expireAt;
final int remainingTimeMs;
final String memo;
PendingRewardItem({
required this.id,
required this.rightType,
required this.usdtAmount,
required this.hashpowerAmount,
required this.createdAt,
required this.expireAt,
required this.remainingTimeMs,
required this.memo,
});
factory PendingRewardItem.fromJson(Map<String, dynamic> json) {
return PendingRewardItem(
id: json['id']?.toString() ?? '',
rightType: json['rightType'] ?? '',
usdtAmount: (json['usdtAmount'] ?? 0).toDouble(),
hashpowerAmount: (json['hashpowerAmount'] ?? 0).toDouble(),
createdAt: DateTime.tryParse(json['createdAt'] ?? '') ?? DateTime.now(),
expireAt: DateTime.tryParse(json['expireAt'] ?? '') ?? DateTime.now(),
remainingTimeMs: (json['remainingTimeMs'] ?? 0).toInt(),
memo: json['memo'] ?? '',
);
}
///
int get remainingSeconds => (remainingTimeMs / 1000).round();
///
String get rightTypeName {
switch (rightType) {
case 'SHARE_RIGHT':
return '分享权益';
case 'PROVINCE_AREA_RIGHT':
return '省区域权益';
case 'PROVINCE_TEAM_RIGHT':
return '省团队权益';
case 'CITY_AREA_RIGHT':
return '市区域权益';
case 'CITY_TEAM_RIGHT':
return '市团队权益';
case 'COMMUNITY_RIGHT':
return '社区权益';
default:
return rightType;
}
}
}
/// ( reward-service )
class RewardSummary {
final double pendingUsdt;
@ -107,4 +166,57 @@ class RewardService {
rethrow;
}
}
///
///
/// GET /rewards/pending (reward-service)
///
Future<List<PendingRewardItem>> getPendingRewards() async {
try {
debugPrint('[RewardService] ========== 获取待领取奖励列表 ==========');
debugPrint('[RewardService] 请求: GET /rewards/pending');
final response = await _apiClient.get('/rewards/pending');
debugPrint('[RewardService] 响应状态码: ${response.statusCode}');
debugPrint('[RewardService] 响应数据类型: ${response.data.runtimeType}');
if (response.statusCode == 200) {
final responseData = response.data;
debugPrint('[RewardService] 原始响应数据: $responseData');
// API { data: [...] }
List<dynamic> dataList;
if (responseData is List) {
dataList = responseData;
} else if (responseData is Map<String, dynamic>) {
dataList = responseData['data'] as List<dynamic>? ?? [];
} else {
dataList = [];
}
debugPrint('[RewardService] 解析到 ${dataList.length} 条待领取奖励');
final items = dataList
.map((item) => PendingRewardItem.fromJson(item as Map<String, dynamic>))
.toList();
for (var item in items) {
debugPrint('[RewardService] - ${item.rightTypeName}: ${item.usdtAmount} USDT, ${item.hashpowerAmount} 算力, 剩余 ${item.remainingSeconds}s');
}
debugPrint('[RewardService] ================================');
return items;
}
debugPrint('[RewardService] 请求失败,状态码: ${response.statusCode}');
debugPrint('[RewardService] 响应内容: ${response.data}');
throw Exception('获取待领取奖励列表失败: ${response.statusCode}');
} catch (e, stackTrace) {
debugPrint('[RewardService] !!!!!!!!!! 获取待领取奖励列表异常 !!!!!!!!!!');
debugPrint('[RewardService] 错误: $e');
debugPrint('[RewardService] 堆栈: $stackTrace');
rethrow;
}
}
}

View File

@ -9,6 +9,7 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:device_info_plus/device_info_plus.dart';
import '../../../../core/di/injection_container.dart';
import '../../../../core/services/referral_service.dart';
import '../../../../core/services/reward_service.dart';
import '../../../../routes/route_paths.dart';
import '../../../../routes/app_router.dart';
@ -70,6 +71,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
bool _isLoadingWallet = true;
String? _walletError;
//
List<PendingRewardItem> _pendingRewards = [];
//
Timer? _timer;
int _remainingSeconds = 0;
@ -362,8 +366,16 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
debugPrint('[ProfilePage] 获取 rewardServiceProvider...');
final rewardService = ref.read(rewardServiceProvider);
debugPrint('[ProfilePage] 调用 getMyRewardSummary()...');
final summary = await rewardService.getMyRewardSummary();
//
debugPrint('[ProfilePage] 调用 getMyRewardSummary() 和 getPendingRewards()...');
final results = await Future.wait([
rewardService.getMyRewardSummary(),
rewardService.getPendingRewards(),
]);
final summary = results[0] as RewardSummary;
final pendingRewards = results[1] as List<PendingRewardItem>;
debugPrint('[ProfilePage] -------- 收益数据加载成功 --------');
debugPrint('[ProfilePage] 待领取 USDT: ${summary.pendingUsdt}');
@ -374,6 +386,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
debugPrint('[ProfilePage] 已过期 算力: ${summary.expiredTotalHashpower}');
debugPrint('[ProfilePage] 过期时间: ${summary.pendingExpireAt}');
debugPrint('[ProfilePage] 剩余秒数: ${summary.pendingRemainingSeconds}');
debugPrint('[ProfilePage] 待领取奖励条目数: ${pendingRewards.length}');
if (mounted) {
debugPrint('[ProfilePage] 更新 UI 状态...');
@ -385,12 +398,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_expiredUsdt = summary.expiredTotalUsdt;
_expiredPower = summary.expiredTotalHashpower;
_remainingSeconds = summary.pendingRemainingSeconds;
_pendingRewards = pendingRewards;
_isLoadingWallet = false;
});
debugPrint('[ProfilePage] UI 状态更新完成');
debugPrint('[ProfilePage] _pendingUsdt: $_pendingUsdt');
debugPrint('[ProfilePage] _remainingSeconds: $_remainingSeconds');
debugPrint('[ProfilePage] _pendingRewards: ${_pendingRewards.length}');
//
if (_remainingSeconds > 0) {
@ -1115,115 +1130,246 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
Column(
//
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'24 小时倒计时',
style: TextStyle(
fontSize: 12,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.5,
color: Color(0xCC5D4037),
),
),
Text(
_formatCountdown(),
style: const TextStyle(
fontSize: 14,
fontFamily: 'Consolas',
fontWeight: FontWeight.w700,
height: 1.25,
letterSpacing: 0.7,
color: Color(0xFFD4AF37),
),
),
],
),
const SizedBox(height: 15),
// USDT
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'待领取 (USDT)',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.5,
color: Color(0xCC5D4037),
),
),
Text(
_formatNumber(_pendingUsdt),
style: const TextStyle(
fontSize: 20,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.25,
color: Color(0xFF5D4037),
),
),
],
),
const SizedBox(height: 11),
//
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'待领取 (算力)',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.5,
color: Color(0xCC5D4037),
),
),
Text(
_formatNumber(_pendingPower),
style: const TextStyle(
fontSize: 20,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.25,
color: Color(0xFF5D4037),
),
),
],
),
const SizedBox(height: 15),
//
GestureDetector(
onTap: _claimAllEarnings,
child: Container(
width: double.infinity,
height: 40,
decoration: BoxDecoration(
color: const Color(0xFFD4AF37),
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Text(
'领取全部',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
letterSpacing: 0.21,
color: Colors.white,
//
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'24小时倒计时',
style: TextStyle(
fontSize: 12,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.5,
color: Color(0xCC5D4037),
),
),
Text(
_formatCountdown(),
style: const TextStyle(
fontSize: 14,
fontFamily: 'Consolas',
fontWeight: FontWeight.w700,
height: 1.25,
letterSpacing: 0.7,
color: Color(0xFFD4AF37),
),
),
],
),
),
// USDT
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'待领取 (USDT)',
style: TextStyle(
fontSize: 12,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.5,
color: Color(0xCC5D4037),
),
),
Text(
_formatNumber(_pendingUsdt),
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.25,
color: Color(0xFF5D4037),
),
),
],
),
),
//
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'待领取 (算力)',
style: TextStyle(
fontSize: 12,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.5,
color: Color(0xCC5D4037),
),
),
Text(
_formatNumber(_pendingPower),
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.25,
color: Color(0xFF5D4037),
),
),
],
),
),
],
),
//
if (_pendingRewards.isNotEmpty) ...[
const SizedBox(height: 16),
const Divider(color: Color(0x33D4AF37), height: 1),
const SizedBox(height: 12),
const Text(
'待领取明细',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFF5D4037),
),
),
const SizedBox(height: 8),
//
...(_pendingRewards.map((item) => _buildPendingRewardItem(item))),
],
const SizedBox(height: 15),
//
GestureDetector(
onTap: _claimAllEarnings,
child: Container(
width: double.infinity,
height: 40,
decoration: BoxDecoration(
color: const Color(0xFFD4AF37),
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Text(
'领取全部',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
letterSpacing: 0.21,
color: Colors.white,
),
),
),
),
),
],
);
}
///
Widget _buildPendingRewardItem(PendingRewardItem item) {
final remainingSeconds = item.remainingSeconds;
final hours = (remainingSeconds ~/ 3600).toString().padLeft(2, '0');
final minutes = ((remainingSeconds % 3600) ~/ 60).toString().padLeft(2, '0');
final seconds = (remainingSeconds % 60).toString().padLeft(2, '0');
final countdown = '$hours:$minutes:$seconds';
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0x22D4AF37),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// +
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item.rightTypeName,
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFF5D4037),
),
),
Row(
children: [
const Icon(
Icons.timer_outlined,
size: 14,
color: Color(0xFFD4AF37),
),
const SizedBox(width: 4),
Text(
countdown,
style: const TextStyle(
fontSize: 12,
fontFamily: 'Consolas',
fontWeight: FontWeight.w600,
color: Color(0xFFD4AF37),
),
),
],
),
],
),
const SizedBox(height: 8),
//
Row(
children: [
if (item.usdtAmount > 0) ...[
Text(
'${_formatNumber(item.usdtAmount)} USDT',
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
color: Color(0xFF5D4037),
),
),
if (item.hashpowerAmount > 0) const SizedBox(width: 16),
],
if (item.hashpowerAmount > 0)
Text(
'${_formatNumber(item.hashpowerAmount)} 算力',
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
color: Color(0xFF5D4037),
),
),
],
),
//
if (item.memo.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
item.memo,
style: const TextStyle(
fontSize: 11,
fontFamily: 'Inter',
color: Color(0x995D4037),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
);
),
);
}
///