From 6eea4463f8002a47b63e695dfcb09bbb52a0702b Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 14 Dec 2025 01:33:13 -0800 Subject: [PATCH] =?UTF-8?q?feat(profile):=20=E6=94=AF=E6=8C=81=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=A4=9A=E7=AC=94=E5=BE=85=E9=A2=86=E5=8F=96=E5=A5=96?= =?UTF-8?q?=E5=8A=B1=E6=98=8E=E7=BB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 PendingRewardItem 数据模型,对接 GET /rewards/pending 接口 - 修改 ProfilePage 并行加载汇总数据和待领取列表 - 重构收益区域 UI,展示每笔奖励的权益类型、金额和独立倒计时 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../lib/core/services/reward_service.dart | 112 ++++++ .../presentation/pages/profile_page.dart | 352 +++++++++++++----- 2 files changed, 361 insertions(+), 103 deletions(-) diff --git a/frontend/mobile-app/lib/core/services/reward_service.dart b/frontend/mobile-app/lib/core/services/reward_service.dart index 83d34f8d..498d3460 100644 --- a/frontend/mobile-app/lib/core/services/reward_service.dart +++ b/frontend/mobile-app/lib/core/services/reward_service.dart @@ -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 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> 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 dataList; + if (responseData is List) { + dataList = responseData; + } else if (responseData is Map) { + dataList = responseData['data'] as List? ?? []; + } else { + dataList = []; + } + + debugPrint('[RewardService] 解析到 ${dataList.length} 条待领取奖励'); + + final items = dataList + .map((item) => PendingRewardItem.fromJson(item as Map)) + .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; + } + } } diff --git a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart index da2b70e7..1ff28b84 100644 --- a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart +++ b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart @@ -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 { bool _isLoadingWallet = true; String? _walletError; + // 待领取奖励列表(每一笔单独显示) + List _pendingRewards = []; + // 倒计时 Timer? _timer; int _remainingSeconds = 0; @@ -362,8 +366,16 @@ class _ProfilePageState extends ConsumerState { 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; debugPrint('[ProfilePage] -------- 收益数据加载成功 --------'); debugPrint('[ProfilePage] 待领取 USDT: ${summary.pendingUsdt}'); @@ -374,6 +386,7 @@ class _ProfilePageState extends ConsumerState { 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 { _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 { 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, + ), + ], ], - ); + ), + ); } /// 构建结算区域