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:
parent
cd742856c0
commit
6eea4463f8
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建结算区域
|
||||
|
|
|
|||
Loading…
Reference in New Issue