rwadurian/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart

4333 lines
152 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 'dart:io';
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:shimmer/shimmer.dart';
import 'package:visibility_detector/visibility_detector.dart';
import '../../../../core/di/injection_container.dart';
import '../../../../core/storage/storage_keys.dart';
import '../../../../core/services/referral_service.dart';
import '../../../../core/services/reward_service.dart';
import '../../../../core/services/notification_service.dart';
import '../../../../core/providers/notification_badge_provider.dart';
import '../../../../core/utils/date_utils.dart';
import '../../../../routes/route_paths.dart';
import '../../../../routes/app_router.dart';
import '../../../auth/presentation/providers/auth_provider.dart';
import '../../../auth/presentation/providers/wallet_status_provider.dart';
import '../widgets/team_tree_widget.dart';
import '../widgets/stacked_cards_widget.dart';
import '../../../authorization/presentation/widgets/stickman_race_widget.dart';
import '../../../kyc/data/kyc_service.dart';
/// 个人中心页面 - 显示用户信息、社区数据、收益和设置
/// 包含用户资料、推荐信息、社区考核、收益领取等功能
class ProfilePage extends ConsumerStatefulWidget {
const ProfilePage({super.key});
@override
ConsumerState<ProfilePage> createState() => _ProfilePageState();
}
class _ProfilePageState extends ConsumerState<ProfilePage> {
// 用户数据(从存储/API加载
String _nickname = '加载中...';
String _serialNumber = '--';
String? _avatarSvg;
String? _avatarUrl;
String? _localAvatarPath; // 本地头像文件路径
String _referrerSerial = '--'; // 推荐人序列号从API获取
String _referralCode = '--'; // 我的推荐码
// 授权数据(从 authorization-service 获取)
String _community = '--';
String _parentCommunity = '--';
String _childCommunity = '--';
String _authCityCompany = '--'; // 市团队
String _cityCompany = '--'; // 市区域
String _authProvinceCompany = '--'; // 省团队
String _provinceCompany = '--'; // 省区域
String _province = '--';
// 团队数据(从 referral-service 获取)
int _directReferralCount = 0;
int _totalTeamCount = 0;
int _personalPlantingCount = 0;
int _teamPlantingCount = 0;
double _leaderboardScore = 0;
int? _leaderboardRank;
// 直推数据(从 referral-service 获取)
List<Map<String, dynamic>> _referrals = [];
// 团队树根节点(缓存以保持展开状态)
TeamTreeNode? _teamTreeRootNode;
// 社区考核数据(从 authorization-service 获取)
bool _hasCommunityAuth = false; // 是否有社区授权
bool _communityBenefitActive = false; // 社区权益是否激活
int _communityCurrentTreeCount = 0; // 当前团队认种数量
int _communityInitialTarget = 10; // 初始考核目标社区固定10
int _communityMonthlyTarget = 10; // 月度考核目标
int _communityMonthIndex = 0; // 当前考核月份
// 市团队考核数据
bool _hasAuthCityCompanyAuth = false;
bool _authCityCompanyBenefitActive = false;
int _authCityCompanyCurrentTreeCount = 0;
int _authCityCompanyInitialTarget = 100;
int _authCityCompanyMonthlyTarget = 100;
int _authCityCompanyMonthIndex = 0;
String _authCityCompanyRegionCode = ''; // 市团队区域代码
// 省团队考核数据
bool _hasAuthProvinceCompanyAuth = false;
bool _authProvinceCompanyBenefitActive = false;
int _authProvinceCompanyCurrentTreeCount = 0;
int _authProvinceCompanyInitialTarget = 500;
int _authProvinceCompanyMonthlyTarget = 500;
int _authProvinceCompanyMonthIndex = 0;
String _authProvinceCompanyRegionCode = ''; // 省团队区域代码
// 市区域考核数据
bool _hasCityCompanyAuth = false;
bool _cityCompanyBenefitActive = false;
int _cityCompanyCurrentTreeCount = 0;
int _cityCompanyInitialTarget = 100;
int _cityCompanyMonthlyTarget = 100;
int _cityCompanyMonthIndex = 0;
// 省区域考核数据
bool _hasProvinceCompanyAuth = false;
bool _provinceCompanyBenefitActive = false;
int _provinceCompanyCurrentTreeCount = 0;
int _provinceCompanyInitialTarget = 500;
int _provinceCompanyMonthlyTarget = 500;
int _provinceCompanyMonthIndex = 0;
// 火柴人排名数据(省公司/市公司排名赛跑)
List<StickmanRankingData> _provinceRankings = [];
List<StickmanRankingData> _cityRankings = [];
String _provinceRegionName = ''; // 省名称
String _cityRegionName = ''; // 市名称
bool _isLoadingStickmanRanking = false;
bool _hasLoadedStickmanRanking = false;
// 收益数据(从 reward-service 直接获取)
double _pendingUsdt = 0.0;
double _pendingPower = 0.0;
double _settleableUsdt = 0.0;
double _settledUsdt = 0.0;
double _expiredUsdt = 0.0;
double _expiredPower = 0.0;
bool _isLoadingWallet = false; // 懒加载,初始为 false
String? _walletError;
// 待领取奖励列表(每一笔单独显示)
List<PendingRewardItem> _pendingRewards = [];
// 可结算奖励列表(逐笔显示)
List<SettleableRewardItem> _settleableRewards = [];
// 已过期奖励列表(逐笔显示)
List<ExpiredRewardItem> _expiredRewards = [];
// 倒计时
Timer? _timer;
int _remainingSeconds = 0;
// 应用版本信息
String _appVersion = '--';
String _buildNumber = '--';
String _packageName = '--';
String _deviceModel = '--';
String _osVersion = '--';
String _platform = '--';
// ========== 懒加载 + 重试机制相关状态 ==========
// 各区域加载状态(懒加载区域初始为 false等待可见时触发
bool _isLoadingUserData = true;
bool _isLoadingReferral = false;
bool _isLoadingAuthorization = false;
String? _userDataError;
String? _referralError;
String? _authorizationError;
// 各区域重试计数
int _userDataRetryCount = 0;
int _referralRetryCount = 0;
int _authorizationRetryCount = 0;
int _walletRetryCount = 0;
static const int _maxRetries = 3;
static const int _retryDelayMs = 1000;
// 懒加载标记(是否已触发过加载)
bool _hasLoadedReferral = false;
bool _hasLoadedAuthorization = false;
bool _hasLoadedWallet = false;
// 定时刷新相关
Timer? _refreshTimer;
static const int _autoRefreshIntervalSeconds = 60; // 60秒自动刷新一次可见区域
bool _isReferralVisible = false;
bool _isAuthorizationVisible = false;
bool _isWalletVisible = false;
// 防抖相关(防止快速滑动导致大量请求)
Timer? _referralDebounceTimer;
Timer? _authorizationDebounceTimer;
Timer? _walletDebounceTimer;
static const int _debounceDelayMs = 300; // 300ms 防抖延迟
// 隐藏"我的团队"功能的秘密点击计数器
int _teamPlantingTapCount = 0;
DateTime? _lastTeamPlantingTapTime;
bool _showMyTeamTree = false;
@override
void initState() {
super.initState();
// 首屏数据:用户基本信息(从本地存储优先)
_checkLocalAvatarSync();
_loadUserData().then((_) {
// 用户数据加载完成后,检查钱包状态
_checkAndStartWalletPolling();
});
_loadAppInfo();
// 通知数量(轻量级请求)- 延迟到下一帧执行,避免在 build 期间修改 provider
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadUnreadNotificationCount();
});
// 启动定时刷新(可见区域的数据)
_startAutoRefreshTimer();
}
/// 加载应用信息
Future<void> _loadAppInfo() async {
try {
// 加载包信息
final packageInfo = await PackageInfo.fromPlatform();
// 加载设备信息
final deviceInfoPlugin = DeviceInfoPlugin();
String deviceModel = '';
String osVersion = '';
String platform = '';
if (Platform.isAndroid) {
final androidInfo = await deviceInfoPlugin.androidInfo;
deviceModel = '${androidInfo.brand} ${androidInfo.model}';
osVersion = 'Android ${androidInfo.version.release} (SDK ${androidInfo.version.sdkInt})';
platform = 'Android';
} else if (Platform.isIOS) {
final iosInfo = await deviceInfoPlugin.iosInfo;
deviceModel = iosInfo.model;
osVersion = '${iosInfo.systemName} ${iosInfo.systemVersion}';
platform = 'iOS';
}
if (mounted) {
setState(() {
_appVersion = packageInfo.version;
_buildNumber = packageInfo.buildNumber;
_packageName = packageInfo.packageName;
_deviceModel = deviceModel;
_osVersion = osVersion;
_platform = platform;
});
}
} catch (e) {
debugPrint('[ProfilePage] 加载应用信息失败: $e');
}
}
/// 同步检查本地头像文件(在 build 之前快速获取)
void _checkLocalAvatarSync() {
// 使用 WidgetsBinding 确保在第一帧渲染前执行
WidgetsBinding.instance.addPostFrameCallback((_) async {
final accountService = ref.read(accountServiceProvider);
final localPath = await accountService.getLocalAvatarPath();
if (mounted && localPath != null && _localAvatarPath == null) {
setState(() {
_localAvatarPath = localPath;
});
}
});
}
/// 加载用户数据
Future<void> _loadUserData() async {
debugPrint('[ProfilePage] _loadUserData() - 开始加载本地存储数据...');
final accountService = ref.read(accountServiceProvider);
// 并行加载本地存储数据
final results = await Future.wait([
accountService.getUsername(),
accountService.getUserSerialNum(),
accountService.getAvatarSvg(),
accountService.getAvatarUrl(),
accountService.getLocalAvatarPath(),
accountService.getReferralCode(),
]);
final username = results[0] as String?;
final serialNum = results[1] as String?;
final avatarSvg = results[2] as String?;
final avatarUrl = results[3] as String?;
final localAvatarPath = results[4] as String?;
final referralCode = results[5] as String?;
debugPrint('[ProfilePage] _loadUserData() - 本地存储数据:');
debugPrint('[ProfilePage] username: $username');
debugPrint('[ProfilePage] serialNum: $serialNum');
debugPrint('[ProfilePage] avatarSvg: ${avatarSvg != null ? "长度=${avatarSvg.length}" : "null"}');
debugPrint('[ProfilePage] avatarUrl: $avatarUrl');
debugPrint('[ProfilePage] localAvatarPath: $localAvatarPath');
if (mounted) {
setState(() {
_nickname = username ?? '未设置昵称';
_serialNumber = serialNum?.toString() ?? '--';
_avatarSvg = avatarSvg;
_avatarUrl = avatarUrl;
_localAvatarPath = localAvatarPath;
_referralCode = referralCode ?? '--';
});
// 如果有远程URL但没有本地缓存后台下载并缓存
if (avatarUrl != null && avatarUrl.isNotEmpty && localAvatarPath == null) {
_downloadAndCacheAvatar(avatarUrl);
}
}
// 从API获取完整用户信息包括推荐人
_loadMeData();
}
/// 从API加载完整用户信息
Future<void> _loadMeData() async {
try {
debugPrint('[ProfilePage] _loadMeData() - 开始加载用户信息...');
final accountService = ref.read(accountServiceProvider);
final meData = await accountService.getMe();
debugPrint('[ProfilePage] _loadMeData() - API返回数据:');
debugPrint('[ProfilePage] nickname: ${meData.nickname}');
debugPrint('[ProfilePage] avatarUrl: ${meData.avatarUrl != null ? "长度=${meData.avatarUrl!.length}" : "null"}');
debugPrint('[ProfilePage] avatarUrl内容前100字符: ${meData.avatarUrl?.substring(0, meData.avatarUrl!.length > 100 ? 100 : meData.avatarUrl!.length) ?? "null"}');
debugPrint('[ProfilePage] 当前_avatarSvg: ${_avatarSvg != null ? "长度=${_avatarSvg!.length}" : "null"}');
if (mounted) {
setState(() {
_referrerSerial = meData.inviterSequence?.toString() ?? '';
// 如果API返回了更新的数据也更新本地显示
if (meData.nickname.isNotEmpty) {
_nickname = meData.nickname;
}
if (meData.referralCode.isNotEmpty) {
_referralCode = meData.referralCode;
}
// 如果本地没有头像但API返回了头像更新显示
debugPrint('[ProfilePage] _loadMeData() - 检查头像同步条件:');
debugPrint('[ProfilePage] _avatarSvg == null: ${_avatarSvg == null}');
debugPrint('[ProfilePage] meData.avatarUrl != null: ${meData.avatarUrl != null}');
debugPrint('[ProfilePage] meData.avatarUrl.isNotEmpty: ${meData.avatarUrl?.isNotEmpty ?? false}');
if (_avatarSvg == null && meData.avatarUrl != null && meData.avatarUrl!.isNotEmpty) {
debugPrint('[ProfilePage] _loadMeData() - 同步头像到本地!');
_avatarSvg = meData.avatarUrl;
// 同时保存到本地存储避免下次还需要从API获取
accountService.updateLocalAvatarSvg(meData.avatarUrl!);
} else {
debugPrint('[ProfilePage] _loadMeData() - 不需要同步头像 (条件不满足)');
}
});
}
} catch (e, stackTrace) {
debugPrint('[ProfilePage] 加载用户信息失败: $e');
debugPrint('[ProfilePage] 堆栈: $stackTrace');
// 失败时保持现有数据,不影响页面显示
}
}
/// 加载推荐数据 (from referral-service)
/// [isRefresh] 是否为刷新操作(刷新时不显示加载状态)
Future<void> _loadReferralData({bool isRefresh = false}) async {
if (!isRefresh) {
setState(() {
_isLoadingReferral = true;
_referralError = null;
});
}
try {
debugPrint('[ProfilePage] 开始加载推荐数据...');
final referralService = ref.read(referralServiceProvider);
// 并行加载推荐信息和直推列表
final results = await Future.wait([
referralService.getMyReferralInfo(),
referralService.getDirectReferrals(limit: 10),
]);
final referralInfo = results[0] as ReferralInfoResponse;
final directReferrals = results[1] as DirectReferralsResponse;
debugPrint('[ProfilePage] 推荐数据加载成功: directReferralCount=${referralInfo.directReferralCount}, totalTeamCount=${referralInfo.totalTeamCount}');
if (mounted) {
setState(() {
_isLoadingReferral = false;
_referralError = null;
_referralRetryCount = 0;
_directReferralCount = referralInfo.directReferralCount;
_totalTeamCount = referralInfo.totalTeamCount;
_personalPlantingCount = referralInfo.personalPlantingCount;
_teamPlantingCount = referralInfo.teamPlantingCount;
_leaderboardScore = referralInfo.leaderboardScore;
_leaderboardRank = referralInfo.leaderboardRank;
// 转换直推列表格式
_referrals = directReferrals.referrals.map((r) => <String, dynamic>{
'serial': r.accountSequence, // 使用8位账户序列号显示
'personal': r.personalPlantingCount, // 个人认种量
'team': r.teamPlantingCount, // 团队认种量
}).toList();
});
}
} catch (e, stackTrace) {
debugPrint('[ProfilePage] 加载推荐数据失败: $e');
debugPrint('[ProfilePage] 堆栈: $stackTrace');
if (mounted) {
setState(() {
_isLoadingReferral = false;
_referralError = '加载失败';
});
// 自动重试
_scheduleRetry(
section: '推荐数据',
retryCount: _referralRetryCount,
loadFunction: () => _loadReferralData(),
updateRetryCount: (count) => _referralRetryCount = count,
);
}
}
}
/// 加载授权数据 (from authorization-service)
/// [isRefresh] 是否为刷新操作(刷新时不显示加载状态)
Future<void> _loadAuthorizationData({bool isRefresh = false}) async {
if (!isRefresh) {
setState(() {
_isLoadingAuthorization = true;
_authorizationError = null;
});
}
try {
debugPrint('[ProfilePage] 开始加载授权数据...');
final authorizationService = ref.read(authorizationServiceProvider);
final summary = await authorizationService.getMyAuthorizationSummary();
debugPrint('[ProfilePage] 授权数据加载成功:');
debugPrint('[ProfilePage] 社区授权: ${summary.community != null}');
debugPrint('[ProfilePage] 市团队授权: ${summary.authCityCompany != null}');
debugPrint('[ProfilePage] 市区域授权: ${summary.cityCompany != null}');
debugPrint('[ProfilePage] 省团队授权: ${summary.authProvinceCompany != null}');
debugPrint('[ProfilePage] 省区域授权: ${summary.provinceCompany != null}');
// 获取社区层级数据
try {
final hierarchy = await authorizationService.getMyCommunityHierarchy();
if (mounted) {
setState(() {
_parentCommunity = hierarchy.parentCommunity.communityName;
_childCommunity = hierarchy.childCommunityCount > 0
? '${hierarchy.childCommunities.first.communityName}${hierarchy.childCommunityCount > 1 ? "${hierarchy.childCommunityCount}个" : ""}'
: '--';
});
}
debugPrint('[ProfilePage] 社区层级: 上级=${hierarchy.parentCommunity.communityName}, 下级数量=${hierarchy.childCommunityCount}');
} catch (e) {
debugPrint('[ProfilePage] 获取社区层级失败: $e');
// 失败时保持默认值
}
if (mounted) {
setState(() {
_isLoadingAuthorization = false;
_authorizationError = null;
_authorizationRetryCount = 0;
_community = summary.communityName ?? '--';
_authCityCompany = summary.authCityCompanyName ?? '--';
_cityCompany = summary.cityCompanyName ?? '--';
_authProvinceCompany = summary.authProvinceCompanyName ?? '--';
_provinceCompany = summary.provinceCompanyName ?? '--';
// 更新社区考核数据
if (summary.community != null) {
_hasCommunityAuth = true;
_communityBenefitActive = summary.community!.benefitActive;
_communityCurrentTreeCount = summary.community!.currentTreeCount;
_communityInitialTarget = summary.community!.initialTargetTreeCount;
_communityMonthlyTarget = summary.community!.monthlyTargetTreeCount;
_communityMonthIndex = summary.community!.currentMonthIndex;
debugPrint('[ProfilePage] 社区考核数据:');
debugPrint('[ProfilePage] 权益激活: $_communityBenefitActive');
debugPrint('[ProfilePage] 当前数量: $_communityCurrentTreeCount');
debugPrint('[ProfilePage] 初始目标: $_communityInitialTarget');
debugPrint('[ProfilePage] 月度目标: $_communityMonthlyTarget');
debugPrint('[ProfilePage] 考核月份: $_communityMonthIndex');
} else {
_hasCommunityAuth = false;
}
// 更新市团队考核数据
if (summary.authCityCompany != null) {
_hasAuthCityCompanyAuth = true;
_authCityCompanyBenefitActive = summary.authCityCompany!.benefitActive;
_authCityCompanyCurrentTreeCount = summary.authCityCompany!.currentTreeCount;
_authCityCompanyInitialTarget = summary.authCityCompany!.initialTargetTreeCount;
_authCityCompanyMonthlyTarget = summary.authCityCompany!.monthlyTargetTreeCount;
_authCityCompanyMonthIndex = summary.authCityCompany!.currentMonthIndex;
_authCityCompanyRegionCode = summary.authCityCompany!.regionCode;
debugPrint('[ProfilePage] 市团队授权数据:');
debugPrint('[ProfilePage] regionCode: $_authCityCompanyRegionCode');
debugPrint('[ProfilePage] benefitActive: $_authCityCompanyBenefitActive');
debugPrint('[ProfilePage] currentTreeCount: $_authCityCompanyCurrentTreeCount');
debugPrint('[ProfilePage] monthIndex: $_authCityCompanyMonthIndex');
} else {
_hasAuthCityCompanyAuth = false;
_authCityCompanyRegionCode = '';
}
// 更新省团队考核数据
if (summary.authProvinceCompany != null) {
_hasAuthProvinceCompanyAuth = true;
_authProvinceCompanyBenefitActive = summary.authProvinceCompany!.benefitActive;
_authProvinceCompanyCurrentTreeCount = summary.authProvinceCompany!.currentTreeCount;
_authProvinceCompanyInitialTarget = summary.authProvinceCompany!.initialTargetTreeCount;
_authProvinceCompanyMonthlyTarget = summary.authProvinceCompany!.monthlyTargetTreeCount;
_authProvinceCompanyMonthIndex = summary.authProvinceCompany!.currentMonthIndex;
_authProvinceCompanyRegionCode = summary.authProvinceCompany!.regionCode;
debugPrint('[ProfilePage] 省团队授权数据:');
debugPrint('[ProfilePage] regionCode: $_authProvinceCompanyRegionCode');
debugPrint('[ProfilePage] benefitActive: $_authProvinceCompanyBenefitActive');
debugPrint('[ProfilePage] currentTreeCount: $_authProvinceCompanyCurrentTreeCount');
debugPrint('[ProfilePage] monthIndex: $_authProvinceCompanyMonthIndex');
} else {
_hasAuthProvinceCompanyAuth = false;
_authProvinceCompanyRegionCode = '';
}
// 更新市区域考核数据
if (summary.cityCompany != null) {
_hasCityCompanyAuth = true;
_cityCompanyBenefitActive = summary.cityCompany!.benefitActive;
_cityCompanyCurrentTreeCount = summary.cityCompany!.currentTreeCount;
_cityCompanyInitialTarget = summary.cityCompany!.initialTargetTreeCount;
_cityCompanyMonthlyTarget = summary.cityCompany!.monthlyTargetTreeCount;
_cityCompanyMonthIndex = summary.cityCompany!.currentMonthIndex;
} else {
_hasCityCompanyAuth = false;
}
// 更新省区域考核数据
if (summary.provinceCompany != null) {
_hasProvinceCompanyAuth = true;
_provinceCompanyBenefitActive = summary.provinceCompany!.benefitActive;
_provinceCompanyCurrentTreeCount = summary.provinceCompany!.currentTreeCount;
_provinceCompanyInitialTarget = summary.provinceCompany!.initialTargetTreeCount;
_provinceCompanyMonthlyTarget = summary.provinceCompany!.monthlyTargetTreeCount;
_provinceCompanyMonthIndex = summary.provinceCompany!.currentMonthIndex;
} else {
_hasProvinceCompanyAuth = false;
}
});
// 授权数据加载完成后,加载火柴人排名数据
_loadStickmanRankingData();
}
} catch (e, stackTrace) {
debugPrint('[ProfilePage] 加载授权数据失败: $e');
debugPrint('[ProfilePage] 堆栈: $stackTrace');
if (mounted) {
setState(() {
_isLoadingAuthorization = false;
_authorizationError = '加载失败';
});
// 自动重试
_scheduleRetry(
section: '授权数据',
retryCount: _authorizationRetryCount,
loadFunction: () => _loadAuthorizationData(),
updateRetryCount: (count) => _authorizationRetryCount = count,
);
}
}
}
/// 加载火柴人排名数据
Future<void> _loadStickmanRankingData() async {
// 只有当用户有省公司或市公司授权时才加载
if (!_hasAuthProvinceCompanyAuth && !_hasAuthCityCompanyAuth) {
return;
}
if (_isLoadingStickmanRanking) return;
setState(() {
_isLoadingStickmanRanking = true;
});
try {
debugPrint('[ProfilePage] 开始加载火柴人排名数据...');
final authorizationService = ref.read(authorizationServiceProvider);
// 获取当前月份
final now = DateTime.now();
final month = '${now.year}-${now.month.toString().padLeft(2, '0')}';
// 如果有省团队授权,加载省排名(全系统所有省团队的排名)
if (_hasAuthProvinceCompanyAuth) {
try {
_provinceRegionName = _authProvinceCompany;
debugPrint('[ProfilePage] 开始加载省团队排名: month=$month, regionCode=$_authProvinceCompanyRegionCode');
final rankings = await authorizationService.getStickmanRanking(
month: month,
roleType: 'AUTH_PROVINCE_COMPANY',
regionCode: _authProvinceCompanyRegionCode,
);
debugPrint('[ProfilePage] 省团队排名API返回: ${rankings.length}');
if (mounted) {
setState(() {
_provinceRankings = rankings.map((r) => StickmanRankingData(
id: r.id,
nickname: r.nickname,
avatarUrl: r.avatarUrl,
completedCount: r.completedCount,
targetCount: 50000, // 省公司目标固定5万
monthlyEarnings: r.monthlyEarnings,
isCurrentUser: r.isCurrentUser,
accountSequence: r.accountSequence,
)).toList();
});
}
debugPrint('[ProfilePage] 省团队排名加载成功: ${_provinceRankings.length}');
} catch (e) {
debugPrint('[ProfilePage] 加载省团队排名失败: $e');
}
}
// 如果有市团队授权,加载市排名(全系统所有市团队的排名)
if (_hasAuthCityCompanyAuth) {
try {
_cityRegionName = _authCityCompany;
debugPrint('[ProfilePage] 开始加载市团队排名: month=$month, regionCode=$_authCityCompanyRegionCode');
final rankings = await authorizationService.getStickmanRanking(
month: month,
roleType: 'AUTH_CITY_COMPANY',
regionCode: _authCityCompanyRegionCode,
);
debugPrint('[ProfilePage] 市团队排名API返回: ${rankings.length}');
if (mounted) {
setState(() {
_cityRankings = rankings.map((r) => StickmanRankingData(
id: r.id,
nickname: r.nickname,
avatarUrl: r.avatarUrl,
completedCount: r.completedCount,
targetCount: 10000, // 市公司目标固定1万
monthlyEarnings: r.monthlyEarnings,
isCurrentUser: r.isCurrentUser,
accountSequence: r.accountSequence,
)).toList();
});
}
debugPrint('[ProfilePage] 市团队排名加载成功: ${_cityRankings.length}');
} catch (e) {
debugPrint('[ProfilePage] 加载市团队排名失败: $e');
}
}
if (mounted) {
setState(() {
_isLoadingStickmanRanking = false;
_hasLoadedStickmanRanking = true;
});
}
} catch (e) {
debugPrint('[ProfilePage] 加载火柴人排名失败: $e');
if (mounted) {
setState(() {
_isLoadingStickmanRanking = false;
});
}
}
}
/// 加载通知未读数量(现在通过全局 Provider 管理)
Future<void> _loadUnreadNotificationCount() async {
// 触发全局 Provider 刷新
await ref.read(notificationBadgeProvider.notifier).refresh();
}
/// 跳转到通知中心
void _goToNotifications() {
context.push(RoutePaths.notifications).then((_) {
// 从通知页面返回后刷新未读数量
_loadUnreadNotificationCount();
});
}
/// 加载收益数据 (直接从 reward-service 获取)
/// [isRefresh] 是否为刷新操作(刷新时不显示加载状态)
Future<void> _loadWalletData({bool isRefresh = false}) async {
try {
debugPrint('[ProfilePage] ========== 加载收益数据 ==========');
debugPrint('[ProfilePage] mounted: $mounted, isRefresh: $isRefresh');
if (!isRefresh) {
setState(() {
_isLoadingWallet = true;
_walletError = null;
});
}
debugPrint('[ProfilePage] 获取 rewardServiceProvider...');
final rewardService = ref.read(rewardServiceProvider);
// 并行加载汇总数据、待领取列表、可结算列表、已过期列表
debugPrint('[ProfilePage] 调用 getMyRewardSummary()、getPendingRewards()、getSettleableRewards()、getExpiredRewards()...');
final results = await Future.wait([
rewardService.getMyRewardSummary(),
rewardService.getPendingRewards(),
rewardService.getSettleableRewards(),
rewardService.getExpiredRewards(),
]);
final summary = results[0] as RewardSummary;
final pendingRewards = results[1] as List<PendingRewardItem>;
final settleableRewards = results[2] as List<SettleableRewardItem>;
final expiredRewards = results[3] as List<ExpiredRewardItem>;
debugPrint('[ProfilePage] -------- 收益数据加载成功 --------');
debugPrint('[ProfilePage] 待领取 USDT: ${summary.pendingUsdt}');
debugPrint('[ProfilePage] 待领取 贡献值: ${summary.pendingHashpower}');
debugPrint('[ProfilePage] 可结算 USDT: ${summary.settleableUsdt}');
debugPrint('[ProfilePage] 已结算 USDT: ${summary.settledTotalUsdt}');
debugPrint('[ProfilePage] 已过期 USDT: ${summary.expiredTotalUsdt}');
debugPrint('[ProfilePage] 已过期 贡献值: ${summary.expiredTotalHashpower}');
debugPrint('[ProfilePage] 过期时间: ${summary.pendingExpireAt}');
debugPrint('[ProfilePage] 剩余秒数: ${summary.pendingRemainingSeconds}');
debugPrint('[ProfilePage] 待领取奖励条目数: ${pendingRewards.length}');
debugPrint('[ProfilePage] 可结算奖励条目数: ${settleableRewards.length}');
debugPrint('[ProfilePage] 已过期奖励条目数: ${expiredRewards.length}');
if (mounted) {
debugPrint('[ProfilePage] 更新 UI 状态...');
setState(() {
_pendingUsdt = summary.pendingUsdt;
_pendingPower = summary.pendingHashpower;
_settleableUsdt = summary.settleableUsdt;
_settledUsdt = summary.settledTotalUsdt;
_expiredUsdt = summary.expiredTotalUsdt;
_expiredPower = summary.expiredTotalHashpower;
_remainingSeconds = summary.pendingRemainingSeconds;
_pendingRewards = pendingRewards;
_settleableRewards = settleableRewards;
_expiredRewards = expiredRewards;
_isLoadingWallet = false;
_walletError = null;
_walletRetryCount = 0;
});
debugPrint('[ProfilePage] UI 状态更新完成');
debugPrint('[ProfilePage] _pendingUsdt: $_pendingUsdt');
debugPrint('[ProfilePage] _remainingSeconds: $_remainingSeconds');
debugPrint('[ProfilePage] _pendingRewards: ${_pendingRewards.length}');
debugPrint('[ProfilePage] _settleableRewards: ${_settleableRewards.length}');
debugPrint('[ProfilePage] _expiredRewards: ${_expiredRewards.length}');
// 启动倒计时(如果有待领取收益)
if (_remainingSeconds > 0) {
debugPrint('[ProfilePage] 启动倒计时: $_remainingSeconds');
_startCountdown();
} else {
debugPrint('[ProfilePage] 无需启动倒计时剩余时间为0');
}
} else {
debugPrint('[ProfilePage] 组件已卸载,跳过 UI 更新');
}
debugPrint('[ProfilePage] ================================');
} catch (e, stackTrace) {
debugPrint('[ProfilePage] !!!!!!!!!! 加载收益数据失败 !!!!!!!!!!');
debugPrint('[ProfilePage] 错误类型: ${e.runtimeType}');
debugPrint('[ProfilePage] 错误信息: $e');
debugPrint('[ProfilePage] 堆栈跟踪:');
debugPrint('$stackTrace');
if (mounted) {
setState(() {
_isLoadingWallet = false;
_walletError = '加载失败';
});
// 自动重试
_scheduleRetry(
section: '收益数据',
retryCount: _walletRetryCount,
loadFunction: () => _loadWalletData(),
updateRetryCount: (count) => _walletRetryCount = count,
);
}
}
}
/// 后台下载并缓存头像
Future<void> _downloadAndCacheAvatar(String url) async {
final accountService = ref.read(accountServiceProvider);
final localPath = await accountService.downloadAndCacheAvatar(url);
if (mounted && localPath != null) {
setState(() {
_localAvatarPath = localPath;
});
}
}
/// 检查并启动钱包轮询
/// 逻辑:
/// 1. 先检查本地标志 isWalletReady如果已 ready 则跳过
/// 2. 如果不 ready调用 API 检查钱包状态
/// 3. API 返回 ready 后,设置本地标志并刷新 UI
/// 4. API 返回未 ready启动轮询直到成功
Future<void> _checkAndStartWalletPolling() async {
final authState = ref.read(authProvider);
debugPrint('[ProfilePage] _checkAndStartWalletPolling() called');
debugPrint('[ProfilePage] authState.isWalletReady: ${authState.isWalletReady}');
debugPrint('[ProfilePage] _serialNumber: $_serialNumber');
// 1. 先检查本地标志,如果已 ready 则不需要检查 API
if (authState.isWalletReady) {
debugPrint('[ProfilePage] Wallet already ready (local flag), skip API check');
return;
}
// 2. 没有序列号则跳过
if (_serialNumber.isEmpty || _serialNumber == '--') {
debugPrint('[ProfilePage] Skip wallet check - no valid serialNumber');
return;
}
// 3. 调用 API 检查钱包状态
debugPrint('[ProfilePage] isWalletReady=false, checking wallet status from API...');
try {
final accountService = ref.read(accountServiceProvider);
final walletInfo = await accountService.getWalletInfo(_serialNumber);
debugPrint('[ProfilePage] Wallet status from API: isReady=${walletInfo.isReady}, isGenerating=${walletInfo.isGenerating}');
if (walletInfo.isReady) {
// 4. 钱包已就绪,更新本地存储
debugPrint('[ProfilePage] Wallet is ready, updating local storage');
final secureStorage = ref.read(secureStorageProvider);
await secureStorage.write(key: StorageKeys.isWalletReady, value: 'true');
await secureStorage.write(key: StorageKeys.isAccountCreated, value: 'true');
// 刷新 authProvider 状态
await ref.read(authProvider.notifier).checkAuthStatus();
// 停止轮询
ref.read(walletStatusProvider.notifier).stopPolling();
// 强制刷新UI显示序列号
if (mounted) {
setState(() {});
}
return;
}
// 5. 钱包未就绪,启动轮询直到成功
debugPrint('[ProfilePage] Wallet not ready, starting polling');
await ref.read(walletStatusProvider.notifier).startPolling();
} catch (e) {
debugPrint('[ProfilePage] Failed to check wallet status: $e');
// API 调用失败时,也尝试启动轮询
await ref.read(walletStatusProvider.notifier).startPolling();
}
}
@override
void dispose() {
_timer?.cancel();
_refreshTimer?.cancel();
// 取消防抖定时器
_referralDebounceTimer?.cancel();
_authorizationDebounceTimer?.cancel();
_walletDebounceTimer?.cancel();
// 取消秘密点击解锁定时器
_teamPlantingUnlockTimer?.cancel();
// 停止钱包轮询(在 super.dispose() 之前安全调用)
try {
ref.read(walletStatusProvider.notifier).stopPolling();
} catch (e) {
// 忽略 dispose 后的 ref 访问错误
debugPrint('[ProfilePage] dispose() - 停止轮询时出错(可忽略): $e');
}
super.dispose();
}
/// 启动定时刷新每60秒刷新一次可见区域的数据
void _startAutoRefreshTimer() {
_refreshTimer = Timer.periodic(
const Duration(seconds: _autoRefreshIntervalSeconds),
(_) => _refreshVisibleSections(),
);
}
/// 刷新当前可见区域的数据
Future<void> _refreshVisibleSections() async {
debugPrint('[ProfilePage] 定时刷新可见区域...');
if (_isWalletVisible && !_isLoadingWallet) {
debugPrint('[ProfilePage] 刷新收益数据');
_loadWalletData(isRefresh: true);
}
if (_isReferralVisible && !_isLoadingReferral) {
debugPrint('[ProfilePage] 刷新推荐数据');
_loadReferralData(isRefresh: true);
}
if (_isAuthorizationVisible && !_isLoadingAuthorization) {
debugPrint('[ProfilePage] 刷新授权数据');
_loadAuthorizationData(isRefresh: true);
}
}
/// 下拉刷新 - 刷新所有数据
Future<void> _onRefresh() async {
debugPrint('[ProfilePage] 下拉刷新 - 刷新所有数据');
// 重置重试计数
_userDataRetryCount = 0;
_referralRetryCount = 0;
_authorizationRetryCount = 0;
_walletRetryCount = 0;
// 并行刷新所有已加载的数据
await Future.wait([
_loadUserData(),
if (_hasLoadedReferral) _loadReferralData(isRefresh: true),
if (_hasLoadedAuthorization) _loadAuthorizationData(isRefresh: true),
if (_hasLoadedWallet) _loadWalletData(isRefresh: true),
_loadUnreadNotificationCount(),
]);
}
/// 处理推荐数据区域可见性变化(带防抖)
void _onReferralVisibilityChanged(VisibilityInfo info) {
final isVisible = info.visibleFraction > 0.1;
_isReferralVisible = isVisible;
// 取消之前的防抖定时器
_referralDebounceTimer?.cancel();
// 首次可见时触发加载(带防抖)
if (isVisible && !_hasLoadedReferral && !_isLoadingReferral) {
_referralDebounceTimer = Timer(
const Duration(milliseconds: _debounceDelayMs),
() {
if (mounted && _isReferralVisible && !_hasLoadedReferral) {
_hasLoadedReferral = true;
_loadReferralData();
}
},
);
}
}
/// 处理授权数据区域可见性变化(带防抖)
void _onAuthorizationVisibilityChanged(VisibilityInfo info) {
final isVisible = info.visibleFraction > 0.1;
_isAuthorizationVisible = isVisible;
// 取消之前的防抖定时器
_authorizationDebounceTimer?.cancel();
// 首次可见时触发加载(带防抖)
if (isVisible && !_hasLoadedAuthorization && !_isLoadingAuthorization) {
_authorizationDebounceTimer = Timer(
const Duration(milliseconds: _debounceDelayMs),
() {
if (mounted && _isAuthorizationVisible && !_hasLoadedAuthorization) {
_hasLoadedAuthorization = true;
_loadAuthorizationData();
}
},
);
}
}
/// 处理收益数据区域可见性变化(带防抖)
void _onWalletVisibilityChanged(VisibilityInfo info) {
final isVisible = info.visibleFraction > 0.1;
_isWalletVisible = isVisible;
// 取消之前的防抖定时器
_walletDebounceTimer?.cancel();
// 首次可见时触发加载(带防抖)
if (isVisible && !_hasLoadedWallet && !_isLoadingWallet) {
_walletDebounceTimer = Timer(
const Duration(milliseconds: _debounceDelayMs),
() {
if (mounted && _isWalletVisible && !_hasLoadedWallet) {
_hasLoadedWallet = true;
_loadWalletData();
}
},
);
}
}
/// 自动重试调度(指数退避)
void _scheduleRetry({
required String section,
required int retryCount,
required Future<void> Function() loadFunction,
required void Function(int) updateRetryCount,
}) {
if (retryCount >= _maxRetries) {
debugPrint('[ProfilePage] $section 已达最大重试次数');
return;
}
final delay = _retryDelayMs * (1 << retryCount); // 指数退避
debugPrint('[ProfilePage] $section 将在 ${delay}ms 后重试 (第 ${retryCount + 1} 次)');
Future.delayed(Duration(milliseconds: delay), () {
if (mounted) {
updateRetryCount(retryCount + 1);
loadFunction();
}
});
}
/// 开始倒计时
void _startCountdown() {
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_remainingSeconds > 0) {
setState(() {
_remainingSeconds--;
});
}
});
}
/// 格式化倒计时
String _formatCountdown() {
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');
return '$hours : $minutes : $seconds';
}
/// 复制序列号
void _copySerialNumber() {
Clipboard.setData(ClipboardData(text: _serialNumber));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('序列号已复制'),
backgroundColor: Color(0xFFD4AF37),
),
);
}
/// 跳转到分享页面
void _goToSharePage() {
context.push(
RoutePaths.share,
extra: SharePageParams(
shareLink: 'https://s3.szaiai.com/rwadurian/app-release.apk',
referralCode: _referralCode,
),
);
}
/// 领取全部收益
void _claimAllEarnings() {
// 如果没有待领取收益,提示用户
if (_pendingUsdt <= 0 && _pendingPower <= 0) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('暂无可领取的收益'),
backgroundColor: Color(0xFF8B5A2B),
),
);
return;
}
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('确认领取'),
content: Text(
'确定领取全部收益吗?\n绿积分: ${_formatNumber(_pendingUsdt)}',
// TODO: 暂时隐藏贡献值显示
// '确定领取全部收益吗?\n绿积分: ${_formatNumber(_pendingUsdt)}\n贡献值: ${_formatNumber(_pendingPower)}',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('取消'),
),
TextButton(
onPressed: () async {
Navigator.pop(dialogContext);
await _doClaimRewards();
},
child: const Text('确认'),
),
],
),
);
}
/// 执行领取奖励
Future<void> _doClaimRewards() async {
try {
debugPrint('[ProfilePage] ========== 执行领取奖励 ==========');
debugPrint('[ProfilePage] 当前待领取: USDT=$_pendingUsdt, 贡献值=$_pendingPower');
final walletService = ref.read(walletServiceProvider);
debugPrint('[ProfilePage] 调用 claimRewards()...');
await walletService.claimRewards();
debugPrint('[ProfilePage] 领取成功,准备刷新数据');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('领取成功'),
backgroundColor: Color(0xFFD4AF37),
),
);
// 刷新钱包数据
debugPrint('[ProfilePage] 刷新钱包数据...');
_loadWalletData();
}
debugPrint('[ProfilePage] ================================');
} catch (e, stackTrace) {
debugPrint('[ProfilePage] !!!!!!!!!! 领取奖励失败 !!!!!!!!!!');
debugPrint('[ProfilePage] 错误: $e');
debugPrint('[ProfilePage] 堆栈: $stackTrace');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('领取失败: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
/// 结算
void _onSettlement() {
context.go(RoutePaths.trading);
}
/// 充值USDT
void _onDeposit() {
context.push(RoutePaths.depositUsdt);
}
/// 进入交易
void _goToTrading() {
context.go(RoutePaths.trading);
}
/// 进入认种
void _goToPlanting() {
context.push(RoutePaths.plantingQuantity);
}
/// 谷歌验证器
void _goToGoogleAuth() {
context.push(RoutePaths.googleAuth);
}
/// 修改密码
void _goToChangePassword() {
context.push(RoutePaths.changePassword);
}
/// 绑定邮箱
void _goToBindEmail() {
context.push(RoutePaths.bindEmail);
}
/// 实名认证
void _goToKyc() {
context.push(RoutePaths.kycEntry);
}
/// 自助申请授权
void _goToAuthorizationApply() {
context.push(RoutePaths.authorizationApply);
}
/// 编辑资料
Future<void> _goToEditProfile() async {
final result = await context.push<bool>(RoutePaths.editProfile);
// 如果编辑页面返回 true说明有更新刷新用户数据
if (result == true && mounted) {
_loadUserData();
}
}
/// 格式化数字
String _formatNumber(double number) {
final parts = number.toStringAsFixed(2).split('.');
final intPart = parts[0].replaceAllMapped(
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]},',
);
return '$intPart.${parts[1]}';
}
/// 格式化整数
String _formatInt(int number) {
return number.toString().replaceAllMapped(
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]},',
);
}
/// 构建骨架屏
Widget _buildSkeleton({double height = 100, double? width}) {
return Shimmer.fromColors(
baseColor: const Color(0xFFE0E0E0),
highlightColor: const Color(0xFFF5F5F5),
child: Container(
height: height,
width: width,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
),
);
}
/// 构建错误重试组件
Widget _buildErrorRetry({
required String error,
required VoidCallback onRetry,
required int retryCount,
double height = 100,
}) {
return Container(
height: height,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFFF3E0),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0xFFFFCC80),
width: 1,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: Color(0xFFE65100),
size: 24,
),
const SizedBox(height: 8),
Text(
retryCount >= _maxRetries
? '加载失败,点击重试'
: '加载失败,正在重试...',
style: const TextStyle(
fontSize: 12,
color: Color(0xFFE65100),
),
textAlign: TextAlign.center,
),
if (retryCount >= _maxRetries) ...[
const SizedBox(height: 8),
GestureDetector(
onTap: onRetry,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFFD4AF37),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'重试',
style: TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
),
],
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFFFF5E6),
Color(0xFFFFE4B5),
],
),
),
child: SafeArea(
child: RefreshIndicator(
onRefresh: _onRefresh,
color: const Color(0xFFD4AF37),
backgroundColor: Colors.white,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 页面标题行(带通知图标)
_buildPageHeader(),
const SizedBox(height: 16),
// 用户头像和基本信息
_buildUserHeader(),
const SizedBox(height: 16),
// 火柴人排名赛跑(省公司/市公司)
_buildStickmanRaceSection(),
// 推荐人信息卡片(懒加载:推荐数据)
VisibilityDetector(
key: const Key('referral_section'),
onVisibilityChanged: _onReferralVisibilityChanged,
child: _buildReferralInfoCard(),
),
const SizedBox(height: 16),
// 社区/省份标签(懒加载:授权数据)
VisibilityDetector(
key: const Key('authorization_section'),
onVisibilityChanged: _onAuthorizationVisibilityChanged,
child: _buildCommunityLabel(),
),
const SizedBox(height: 8),
// 认种按钮
_buildPlantingButton(),
const SizedBox(height: 16),
// 主要内容卡片(懒加载:收益数据)
VisibilityDetector(
key: const Key('wallet_section'),
onVisibilityChanged: _onWalletVisibilityChanged,
child: _buildMainContentCard(),
),
const SizedBox(height: 24),
],
),
),
),
),
),
),
);
}
/// 构建页面标题行(含通知图标)
Widget _buildPageHeader() {
// 使用 watch 确保 UI 响应式更新
final unreadCount = ref.watch(unreadNotificationCountProvider);
return Container(
height: 48,
padding: const EdgeInsets.only(top: 8),
child: Stack(
alignment: Alignment.center,
children: [
// 页面标题(居中显示)
const Center(
child: Text(
'我的',
style: TextStyle(
fontSize: 20,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
color: Color(0xFF5D4037),
),
),
),
// 通知图标(右侧,带未读角标)
Positioned(
right: 0,
child: GestureDetector(
onTap: _goToNotifications,
child: Container(
width: 40,
height: 40,
alignment: Alignment.center,
child: Stack(
clipBehavior: Clip.none,
children: [
const Icon(
Icons.notifications_outlined,
size: 26,
color: Color(0xFF5D4037),
),
// 未读角标
if (unreadCount > 0)
Positioned(
top: -4,
right: -4,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 5,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color(0xFFD4AF37),
borderRadius: BorderRadius.circular(10),
),
constraints: const BoxConstraints(
minWidth: 18,
minHeight: 18,
),
child: Text(
unreadCount > 99
? '99+'
: unreadCount.toString(),
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: Colors.white,
),
textAlign: TextAlign.center,
),
),
),
],
),
),
),
),
],
),
);
}
/// 构建用户头像和基本信息
Widget _buildUserHeader() {
return Row(
children: [
// 头像(点击跳转到编辑资料页面)
GestureDetector(
onTap: _goToEditProfile,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(40),
color: const Color(0xFFFFF5E6),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(40),
child: _buildAvatarContent(),
),
),
),
const SizedBox(width: 16),
// 用户信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_nickname,
style: const TextStyle(
fontSize: 20,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.25,
letterSpacing: -0.3,
color: Color(0xFF5D4037),
),
),
const SizedBox(height: 3),
_buildSerialNumberOrStatus(),
],
),
),
],
);
}
/// 构建火柴人赛跑排名区域
Widget _buildStickmanRaceSection() {
// 如果没有省公司或市公司授权,不显示
if (!_hasAuthProvinceCompanyAuth && !_hasAuthCityCompanyAuth) {
return const SizedBox.shrink();
}
// 如果正在加载,显示加载提示
if (_isLoadingStickmanRanking && !_hasLoadedStickmanRanking) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0x80FFFFFF),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
),
child: const Center(
child: Column(
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
),
SizedBox(height: 8),
Text(
'加载排名数据...',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
color: Color(0xFF8B5A2B),
),
),
],
),
),
);
}
debugPrint('[ProfilePage] _buildStickmanRaceSection() - 开始构建火柴人区域');
debugPrint('[ProfilePage] _provinceRankings.length=${_provinceRankings.length}, _cityRankings.length=${_cityRankings.length}');
for (var r in _provinceRankings) {
debugPrint('[ProfilePage] Province ranking: nickname=${r.nickname}, completedCount=${r.completedCount}, targetCount=${r.targetCount}, progress=${r.progress}');
}
for (var r in _cityRankings) {
debugPrint('[ProfilePage] City ranking: nickname=${r.nickname}, completedCount=${r.completedCount}, targetCount=${r.targetCount}, progress=${r.progress}');
}
return Column(
children: [
// 省公司排名赛跑
if (_hasAuthProvinceCompanyAuth && _provinceRankings.isNotEmpty) ...[
StickmanRaceWidget(
rankings: _provinceRankings,
roleType: AuthorizationRoleType.province,
regionName: _provinceRegionName,
),
const SizedBox(height: 16),
],
// 市公司排名赛跑
if (_hasAuthCityCompanyAuth && _cityRankings.isNotEmpty) ...[
StickmanRaceWidget(
rankings: _cityRankings,
roleType: AuthorizationRoleType.city,
regionName: _cityRegionName,
),
const SizedBox(height: 16),
],
],
);
}
/// 构建头像内容优先本地文件其次网络URL最后SVG
Widget _buildAvatarContent() {
debugPrint('[ProfilePage] _buildAvatarContent() - 开始构建头像');
debugPrint('[ProfilePage] _localAvatarPath: $_localAvatarPath');
debugPrint('[ProfilePage] _avatarUrl: $_avatarUrl');
debugPrint('[ProfilePage] _avatarSvg: ${_avatarSvg != null ? "长度=${_avatarSvg!.length}" : "null"}');
// 1. 优先显示本地缓存的头像文件
if (_localAvatarPath != null && _localAvatarPath!.isNotEmpty) {
final file = File(_localAvatarPath!);
// 同步检查文件是否存在
if (file.existsSync()) {
debugPrint('[ProfilePage] _buildAvatarContent() - 使用本地文件: $_localAvatarPath');
return Image.file(
file,
width: 80,
height: 80,
fit: BoxFit.cover,
gaplessPlayback: true, // 防止图片切换时闪烁
errorBuilder: (context, error, stackTrace) {
debugPrint('[ProfilePage] _buildAvatarContent() - 本地文件加载失败: $error');
// 本地文件加载失败尝试网络URL
return _buildNetworkOrSvgAvatar();
},
);
} else {
debugPrint('[ProfilePage] _buildAvatarContent() - 本地文件不存在');
}
}
// 2. 没有本地缓存尝试网络URL或SVG
return _buildNetworkOrSvgAvatar();
}
/// 构建网络URL或SVG头像
Widget _buildNetworkOrSvgAvatar() {
debugPrint('[ProfilePage] _buildNetworkOrSvgAvatar() - 尝试网络URL或SVG');
// 尝试显示网络图片URL
if (_avatarUrl != null && _avatarUrl!.isNotEmpty) {
debugPrint('[ProfilePage] _buildNetworkOrSvgAvatar() - 使用网络URL: $_avatarUrl');
return Image.network(
_avatarUrl!,
width: 80,
height: 80,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
),
),
);
},
errorBuilder: (context, error, stackTrace) {
debugPrint('[ProfilePage] _buildNetworkOrSvgAvatar() - 网络图片加载失败: $error');
// 加载失败时显示SVG或默认头像
return _buildSvgOrDefaultAvatar();
},
);
}
// 显示SVG或默认头像
debugPrint('[ProfilePage] _buildNetworkOrSvgAvatar() - 无网络URL尝试SVG');
return _buildSvgOrDefaultAvatar();
}
/// 构建SVG或默认头像
/// 注意_avatarSvg 可能存储的是 URL用户上传的图片或 SVG 字符串(随机生成的头像)
Widget _buildSvgOrDefaultAvatar() {
if (_avatarSvg != null && _avatarSvg!.isNotEmpty) {
debugPrint('[ProfilePage] _buildSvgOrDefaultAvatar() - 检查头像内容,长度=${_avatarSvg!.length}');
debugPrint('[ProfilePage] 内容前50字符: ${_avatarSvg!.substring(0, _avatarSvg!.length > 50 ? 50 : _avatarSvg!.length)}');
// 检测是否是 URL用户上传的头像图片
if (_avatarSvg!.startsWith('http://') || _avatarSvg!.startsWith('https://')) {
debugPrint('[ProfilePage] _buildSvgOrDefaultAvatar() - 检测到是URL使用网络图片');
return Image.network(
_avatarSvg!,
width: 80,
height: 80,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
),
),
);
},
errorBuilder: (context, error, stackTrace) {
debugPrint('[ProfilePage] _buildSvgOrDefaultAvatar() - 网络图片加载失败: $error');
return const Icon(
Icons.person,
size: 40,
color: Color(0xFF8B5A2B),
);
},
);
}
// 检测是否是 SVG 字符串(随机生成的 SVG 头像)
if (_avatarSvg!.contains('<svg') || _avatarSvg!.startsWith('<?xml')) {
debugPrint('[ProfilePage] _buildSvgOrDefaultAvatar() - 检测到是SVG字符串使用SvgPicture');
// 使用 SizedBox + RepaintBoundary 包装确保固定尺寸并隔离渲染,避免布局问题
return RepaintBoundary(
child: SizedBox(
width: 80,
height: 80,
child: SvgPicture.string(
_avatarSvg!,
width: 80,
height: 80,
fit: BoxFit.cover,
placeholderBuilder: (context) => const Icon(
Icons.person,
size: 40,
color: Color(0xFF8B5A2B),
),
),
),
);
}
debugPrint('[ProfilePage] _buildSvgOrDefaultAvatar() - 未知格式,使用默认图标');
}
debugPrint('[ProfilePage] _buildSvgOrDefaultAvatar() - 无头像数据,使用默认图标');
return const Icon(
Icons.person,
size: 40,
color: Color(0xFF8B5A2B),
);
}
/// 构建推荐人信息卡片(包含授权数据)
Widget _buildReferralInfoCard() {
// Widget 结构始终保持,数据值根据状态显示 "--" 或实际值
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0x80FFFFFF),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 左列:推荐人序列号、所属社区、上级社区、下级社区
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoItem('推荐人的序列号', _referrerSerial),
const SizedBox(height: 12),
_buildInfoItem('所属社区', _community),
const SizedBox(height: 12),
_buildInfoItem('上级社区', _parentCommunity),
const SizedBox(height: 12),
_buildInfoItem('下级社区', _childCommunity),
],
),
),
// 右列:市团队、省团队、市区域、省区域
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoItem('市团队', _authCityCompany),
const SizedBox(height: 12),
_buildInfoItem('省团队', _authProvinceCompany),
const SizedBox(height: 12),
_buildInfoItem('市区域', _cityCompany),
const SizedBox(height: 12),
_buildInfoItem('省区域', _provinceCompany),
],
),
),
],
),
);
}
/// 构建信息项(标签 + 值)
Widget _buildInfoItem(String label, String value) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$label:',
style: const TextStyle(
fontSize: 12,
fontFamily: 'Inter',
height: 1.5,
color: Color(0x995D4037),
),
),
const SizedBox(height: 2),
Text(
value,
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
height: 1.5,
color: Color(0xFF5D4037),
),
),
],
);
}
/// 构建社区/省份标签
Widget _buildCommunityLabel() {
return const Text(
'社区 / 省份',
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',
height: 1.5,
color: Color(0xFF5D4037),
),
);
}
/// 构建认种按钮
Widget _buildPlantingButton() {
return GestureDetector(
onTap: _goToPlanting,
child: Container(
width: double.infinity,
height: 48,
decoration: BoxDecoration(
color: const Color(0xFFD4AF37),
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Text(
'认种 / Planting',
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
letterSpacing: 0.24,
color: Colors.white,
),
),
),
),
);
}
/// 构建主要内容卡片
Widget _buildMainContentCard() {
// Widget 结构始终保持,数据值根据状态显示 "0" 或实际值
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0x80FFFFFF),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
),
child: Column(
children: [
// 收益区域
_buildEarningsSection(),
const SizedBox(height: 16),
// 结算区域(待领取)
_buildSettlementSection(),
const SizedBox(height: 16),
// 已过期区域(放在待领取下面)
_buildExpiredSection(),
const SizedBox(height: 16),
// 操作按钮
_buildActionButtons(),
const SizedBox(height: 16),
// 团队统计
_buildTeamStats(),
const SizedBox(height: 16),
// 直推列表
_buildReferralList(),
const SizedBox(height: 16),
// 我的团队仅在解锁后显示团队种植数区域连续点击19次
if (_showMyTeamTree) ...[
_buildMyTeamTree(),
const SizedBox(height: 16),
],
// 分享邀请按钮
_buildShareButton(),
// 社区权益考核(仅在有社区授权时显示)
if (_hasCommunityAuth) ...[
const SizedBox(height: 16),
_buildCommunityAssessment(),
],
// 市团队权益考核
if (_hasAuthCityCompanyAuth) ...[
const SizedBox(height: 16),
_buildRoleAssessment(
title: '市团队权益考核',
hasAuth: _hasAuthCityCompanyAuth,
benefitActive: _authCityCompanyBenefitActive,
currentTreeCount: _authCityCompanyCurrentTreeCount,
initialTarget: _authCityCompanyInitialTarget,
monthlyTarget: _authCityCompanyMonthlyTarget,
monthIndex: _authCityCompanyMonthIndex,
rewardDescription: '每新增认种 1 棵可获得 288 绿积分',
),
],
// 省团队权益考核
if (_hasAuthProvinceCompanyAuth) ...[
const SizedBox(height: 16),
_buildRoleAssessment(
title: '省团队权益考核',
hasAuth: _hasAuthProvinceCompanyAuth,
benefitActive: _authProvinceCompanyBenefitActive,
currentTreeCount: _authProvinceCompanyCurrentTreeCount,
initialTarget: _authProvinceCompanyInitialTarget,
monthlyTarget: _authProvinceCompanyMonthlyTarget,
monthIndex: _authProvinceCompanyMonthIndex,
rewardDescription: '每新增认种 1 棵可获得 144 绿积分',
),
],
// 市区域权益考核
if (_hasCityCompanyAuth) ...[
const SizedBox(height: 16),
_buildRoleAssessment(
title: '市区域权益考核',
hasAuth: _hasCityCompanyAuth,
benefitActive: _cityCompanyBenefitActive,
currentTreeCount: _cityCompanyCurrentTreeCount,
initialTarget: _cityCompanyInitialTarget,
monthlyTarget: _cityCompanyMonthlyTarget,
monthIndex: _cityCompanyMonthIndex,
rewardDescription: '每新增认种 1 棵可获得 252 绿积分',
),
],
// 省区域权益考核
if (_hasProvinceCompanyAuth) ...[
const SizedBox(height: 16),
_buildRoleAssessment(
title: '省区域权益考核',
hasAuth: _hasProvinceCompanyAuth,
benefitActive: _provinceCompanyBenefitActive,
currentTreeCount: _provinceCompanyCurrentTreeCount,
initialTarget: _provinceCompanyInitialTarget,
monthlyTarget: _provinceCompanyMonthlyTarget,
monthIndex: _provinceCompanyMonthIndex,
rewardDescription: '每新增认种 1 棵可获得 108 绿积分',
),
],
const SizedBox(height: 16),
// 设置菜单
_buildSettingsMenu(),
const SizedBox(height: 16),
// 应用版本信息
_buildAppVersionInfo(),
const SizedBox(height: 16),
// 账号操作按钮
_buildAccountActionButtons(),
],
),
);
}
/// 构建收益区域
Widget _buildEarningsSection() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFFF5E6),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
),
child: _isLoadingWallet
? _buildLoadingState()
: _walletError != null
? _buildErrorState()
: _buildEarningsContent(),
);
}
/// 构建加载状态
Widget _buildLoadingState() {
return const SizedBox(
height: 200,
child: Center(
child: CircularProgressIndicator(
color: Color(0xFFD4AF37),
strokeWidth: 2,
),
),
);
}
/// 构建错误状态
Widget _buildErrorState() {
return GestureDetector(
onTap: _loadWalletData,
child: SizedBox(
height: 200,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: Color(0xFFE65100),
size: 40,
),
const SizedBox(height: 8),
Text(
_walletError!,
style: const TextStyle(
fontSize: 14,
color: Color(0xFFE65100),
),
),
],
),
),
),
);
}
/// 构建收益内容
Widget _buildEarningsContent() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 汇总信息区域 - 竖排布局
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 待领取倒计时
Row(
children: [
const Text(
'待领取',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
color: Color(0xFF5D4037),
),
),
const SizedBox(width: 8),
Text(
_formatCountdown(),
style: const TextStyle(
fontSize: 14,
fontFamily: 'Consolas',
fontWeight: FontWeight.w700,
height: 1.5,
letterSpacing: 0.7,
color: Color(0xFFD4AF37),
),
),
],
),
const SizedBox(height: 8),
// 绿积分
Row(
children: [
const Text(
'绿积分:',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.5,
color: Color(0xCC5D4037),
),
),
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
_formatNumber(_pendingUsdt),
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.25,
color: Color(0xFF5D4037),
),
),
),
),
],
),
// TODO: 暂时隐藏贡献值显示
// const SizedBox(height: 4),
// // 贡献值
// Row(
// 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: 16,
// fontFamily: 'Inter',
// fontWeight: FontWeight.w500,
// height: 1.25,
// color: Color(0xFF5D4037),
// ),
// ),
// ],
// ),
],
),
// 待领取奖励列表 - 使用堆叠卡片展示
if (_pendingRewards.isNotEmpty) ...[
const SizedBox(height: 16),
const Divider(color: Color(0x33D4AF37), height: 1),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'待领取明细',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFF5D4037),
),
),
Text(
'${_pendingRewards.length}',
style: const TextStyle(
fontSize: 12,
fontFamily: 'Inter',
color: Color(0xFF8B5A2B),
),
),
],
),
const SizedBox(height: 8),
// 堆叠卡片展示
StackedCardsView<PendingRewardItem>(
items: _pendingRewards,
peekHeight: 28,
expandedCardHeight: 110,
enableSound: true,
itemBuilder: (item, isSelected, index) => _buildStackedPendingRewardCard(item, isSelected),
),
],
],
);
}
/// 构建堆叠卡片样式的待领取奖励项
Widget _buildStackedPendingRewardCard(PendingRewardItem item, bool isSelected) {
// 确保剩余时间不为负数
final remainingSeconds = item.remainingSeconds > 0 ? item.remainingSeconds : 0;
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';
// 构建金额显示文本
final List<String> amountParts = [];
if (item.usdtAmount > 0) {
amountParts.add('${_formatNumber(item.usdtAmount)} 绿积分');
}
// TODO: 暂时隐藏贡献值显示
// if (item.hashpowerAmount > 0) {
// amountParts.add('${_formatNumber(item.hashpowerAmount)} 贡献值');
// }
final amountText = amountParts.isNotEmpty ? amountParts.join(' ') : '0 绿积分';
return Container(
height: isSelected ? 110 : 48,
decoration: BoxDecoration(
color: isSelected ? Colors.white : const Color(0xFFFFFDF8),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? const Color(0x44D4AF37) : const Color(0x22D4AF37),
width: isSelected ? 1.5 : 1,
),
),
child: isSelected
? Padding(
padding: const EdgeInsets.all(12),
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),
// 第二行:金额信息
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
amountText,
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
color: Color(0xFF5D4037),
),
),
),
// 第三行:来源信息(从 memo 中提取,去掉权益类型前缀)
if (item.memo.isNotEmpty) ...[
const SizedBox(height: 4),
Builder(
builder: (context) {
// memo 格式如 "分享权益来自用户D25122400000的认种"
// 去掉前缀,只显示 "来自用户xxx的认种" 部分
String displayMemo = item.memo;
final colonIndex = displayMemo.indexOf('');
if (colonIndex != -1 && colonIndex < displayMemo.length - 1) {
displayMemo = displayMemo.substring(colonIndex + 1);
}
return Text(
displayMemo,
style: const TextStyle(
fontSize: 11,
fontFamily: 'Inter',
color: Color(0x995D4037),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
);
},
),
],
],
),
)
: Padding(
// 未选中状态:只显示卡片头部预览
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item.rightTypeName,
style: const TextStyle(
fontSize: 13,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
color: Color(0xFF5D4037),
),
),
Text(
amountText,
style: const TextStyle(
fontSize: 13,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFFD4AF37),
),
),
],
),
),
);
}
/// 构建单条待领取奖励项
Widget _buildPendingRewardItem(PendingRewardItem item) {
// 确保剩余时间不为负数
final remainingSeconds = item.remainingSeconds > 0 ? item.remainingSeconds : 0;
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';
// 构建金额显示文本
final List<String> amountParts = [];
if (item.usdtAmount > 0) {
amountParts.add('${_formatNumber(item.usdtAmount)} 绿积分');
}
// TODO: 暂时隐藏贡献值显示
// if (item.hashpowerAmount > 0) {
// amountParts.add('${_formatNumber(item.hashpowerAmount)} 贡献值');
// }
// 如果都为0显示默认文本
final amountText = amountParts.isNotEmpty ? amountParts.join(' ') : '0 绿积分';
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),
// 第二行:金额信息
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
amountText,
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,
),
],
],
),
);
}
/// 构建结算区域
Widget _buildSettlementSection() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFFF5E6),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 可结算 USDT
Row(
children: [
const Text(
'可结算 (绿积分)',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.5,
color: Color(0xCC5D4037),
),
),
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
_formatNumber(_settleableUsdt),
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
color: Color(0xFF5D4037),
),
),
),
),
],
),
// 可结算奖励明细列表
if (_settleableRewards.isNotEmpty) ...[
const SizedBox(height: 16),
const Divider(color: Color(0x33D4AF37), height: 1),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'可结算明细',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFF5D4037),
),
),
Text(
'${_settleableRewards.length}',
style: const TextStyle(
fontSize: 12,
fontFamily: 'Inter',
color: Color(0xFF8B5A2B),
),
),
],
),
const SizedBox(height: 8),
// 堆叠卡片展示
StackedCardsView<SettleableRewardItem>(
items: _settleableRewards,
peekHeight: 28,
expandedCardHeight: 110,
enableSound: true,
itemBuilder: (item, isSelected, index) => _buildStackedSettleableRewardCard(item, isSelected),
),
],
const SizedBox(height: 11),
// 已结算 USDT
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
const Text(
'已结算 (绿积分)',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.5,
color: Color(0xCC5D4037),
),
),
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
_formatNumber(_settledUsdt),
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
color: Color(0xFF5D4037),
),
),
),
),
],
),
),
GestureDetector(
onTap: _onSettlement,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
decoration: BoxDecoration(
color: const Color(0xFFD4AF37),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'结算',
style: TextStyle(
fontSize: 12,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
letterSpacing: 0.18,
color: Colors.white,
),
),
),
),
],
),
],
),
);
}
/// 构建单条可结算奖励项
Widget _buildSettleableRewardItem(SettleableRewardItem item) {
// 格式化时间(优先使用 claimedAt否则使用 createdAt- UTC -> 本地时间
final displayDate = item.claimedAt ?? item.createdAt;
final localDate = displayDate.toLocal();
final settledDate = '${localDate.month}/${localDate.day} ${localDate.hour.toString().padLeft(2, '0')}:${localDate.minute.toString().padLeft(2, '0')}';
// 构建金额显示文本
final List<String> amountParts = [];
if (item.usdtAmount > 0) {
amountParts.add('${_formatNumber(item.usdtAmount)} 绿积分');
}
// TODO: 暂时隐藏贡献值显示
// if (item.hashpowerAmount > 0) {
// amountParts.add('${_formatNumber(item.hashpowerAmount)} 贡献值');
// }
final amountText = amountParts.isNotEmpty ? amountParts.join(' ') : '0 绿积分';
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.check_circle_outline,
size: 14,
color: Color(0xFF4CAF50),
),
const SizedBox(width: 4),
Text(
settledDate,
style: const TextStyle(
fontSize: 12,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
color: Color(0xFF4CAF50),
),
),
],
),
],
),
const SizedBox(height: 8),
// 第二行:金额信息
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
amountText,
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
color: Color(0xFF5D4037),
),
),
),
],
),
);
}
/// 构建堆叠卡片样式的可结算奖励项
Widget _buildStackedSettleableRewardCard(SettleableRewardItem item, bool isSelected) {
// 格式化时间(优先使用 claimedAt否则使用 createdAt- UTC -> 本地时间
final displayDate = item.claimedAt ?? item.createdAt;
final localDate = displayDate.toLocal();
final settledDate = '${localDate.month}/${localDate.day} ${localDate.hour.toString().padLeft(2, '0')}:${localDate.minute.toString().padLeft(2, '0')}';
// 构建金额显示文本
final List<String> amountParts = [];
if (item.usdtAmount > 0) {
amountParts.add('${_formatNumber(item.usdtAmount)} 绿积分');
}
// TODO: 暂时隐藏贡献值显示
// if (item.hashpowerAmount > 0) {
// amountParts.add('${_formatNumber(item.hashpowerAmount)} 贡献值');
// }
final amountText = amountParts.isNotEmpty ? amountParts.join(' ') : '0 绿积分';
return Container(
height: isSelected ? 110 : 48,
decoration: BoxDecoration(
color: isSelected ? Colors.white : const Color(0xFFFFFDF8),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? const Color(0x44D4AF37) : const Color(0x22D4AF37),
width: isSelected ? 1.5 : 1,
),
),
child: isSelected
? Padding(
padding: const EdgeInsets.all(12),
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.check_circle_outline,
size: 14,
color: Color(0xFF4CAF50),
),
const SizedBox(width: 4),
Text(
settledDate,
style: const TextStyle(
fontSize: 12,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
color: Color(0xFF4CAF50),
),
),
],
),
],
),
const SizedBox(height: 6),
// 第二行:金额信息
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
amountText,
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
color: Color(0xFF5D4037),
),
),
),
// 第三行:来源信息(从 memo 中提取,去掉权益类型前缀)
if (item.memo.isNotEmpty) ...[
const SizedBox(height: 4),
Builder(
builder: (context) {
// memo 格式如 "分享权益来自用户D25122400000的认种"
// 去掉前缀,只显示 "来自用户xxx的认种" 部分
String displayMemo = item.memo;
final colonIndex = displayMemo.indexOf('');
if (colonIndex != -1 && colonIndex < displayMemo.length - 1) {
displayMemo = displayMemo.substring(colonIndex + 1);
}
return Text(
displayMemo,
style: const TextStyle(
fontSize: 11,
fontFamily: 'Inter',
color: Color(0x995D4037),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
);
},
),
],
],
),
)
: Padding(
// 未选中状态:只显示卡片头部预览
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item.rightTypeName,
style: const TextStyle(
fontSize: 13,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
color: Color(0xFF5D4037),
),
),
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerRight,
child: Text(
amountText,
style: const TextStyle(
fontSize: 13,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFF4CAF50),
),
),
),
),
],
),
),
);
}
/// 构建已过期区域
Widget _buildExpiredSection() {
return SizedBox(
width: double.infinity,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFFF5E6),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'已过期',
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
color: Color(0xFF5D4037),
),
),
const SizedBox(height: 7),
// 已过期 USDT
Row(
children: [
const Text(
'绿积分:',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.5,
color: Color(0xCC5D4037),
),
),
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
_formatNumber(_expiredUsdt),
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
color: Color(0xFF5D4037),
),
),
),
),
],
),
// TODO: 暂时隐藏贡献值显示
// const SizedBox(height: 8),
// // 已过期 贡献值
// Row(
// children: [
// const Text(
// '贡献值:',
// style: TextStyle(
// fontSize: 14,
// fontFamily: 'Inter',
// fontWeight: FontWeight.w500,
// height: 1.5,
// color: Color(0xCC5D4037),
// ),
// ),
// Text(
// _formatNumber(_expiredPower),
// style: const TextStyle(
// fontSize: 14,
// fontFamily: 'Inter',
// fontWeight: FontWeight.w700,
// height: 1.5,
// color: Color(0xFF5D4037),
// ),
// ),
// ],
// ),
// 已过期奖励明细列表
if (_expiredRewards.isNotEmpty) ...[
const SizedBox(height: 16),
const Divider(color: Color(0x33D4AF37), height: 1),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'已过期明细',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFF5D4037),
),
),
Text(
'${_expiredRewards.length}',
style: const TextStyle(
fontSize: 12,
fontFamily: 'Inter',
color: Color(0xFF8B5A2B),
),
),
],
),
const SizedBox(height: 8),
// 堆叠卡片展示
StackedCardsView<ExpiredRewardItem>(
items: _expiredRewards,
peekHeight: 28,
expandedCardHeight: 90,
enableSound: true,
itemBuilder: (item, isSelected, index) => _buildStackedExpiredRewardCard(item, isSelected),
),
],
],
),
),
);
}
/// 构建单条已过期奖励项
Widget _buildExpiredRewardItem(ExpiredRewardItem item) {
// 格式化过期时间
final expiredDate = '${item.expiredAt.month}/${item.expiredAt.day} ${item.expiredAt.hour.toString().padLeft(2, '0')}:${item.expiredAt.minute.toString().padLeft(2, '0')}';
// 构建金额显示文本
final List<String> amountParts = [];
if (item.usdtAmount > 0) {
amountParts.add('${_formatNumber(item.usdtAmount)} 绿积分');
}
// TODO: 暂时隐藏贡献值显示
// if (item.hashpowerAmount > 0) {
// amountParts.add('${_formatNumber(item.hashpowerAmount)} 贡献值');
// }
final amountText = amountParts.isNotEmpty ? amountParts.join(' ') : '0 绿积分';
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5), // 灰色背景表示已过期
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0x22999999),
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(0xFF999999), // 灰色文字
),
),
Row(
children: [
const Icon(
Icons.cancel_outlined,
size: 14,
color: Color(0xFFE57373), // 红色图标
),
const SizedBox(width: 4),
Text(
expiredDate,
style: const TextStyle(
fontSize: 12,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
color: Color(0xFFE57373), // 红色文字
),
),
],
),
],
),
const SizedBox(height: 8),
// 第二行:金额信息
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
amountText,
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
color: Color(0xFF999999), // 灰色文字
),
),
),
],
),
);
}
/// 构建堆叠卡片样式的已过期奖励项
Widget _buildStackedExpiredRewardCard(ExpiredRewardItem item, bool isSelected) {
// 格式化过期时间
final expiredDate = '${item.expiredAt.month}/${item.expiredAt.day} ${item.expiredAt.hour.toString().padLeft(2, '0')}:${item.expiredAt.minute.toString().padLeft(2, '0')}';
// 构建金额显示文本
final List<String> amountParts = [];
if (item.usdtAmount > 0) {
amountParts.add('${_formatNumber(item.usdtAmount)} 绿积分');
}
// TODO: 暂时隐藏贡献值显示
// if (item.hashpowerAmount > 0) {
// amountParts.add('${_formatNumber(item.hashpowerAmount)} 贡献值');
// }
final amountText = amountParts.isNotEmpty ? amountParts.join(' ') : '0 绿积分';
return Container(
height: isSelected ? 90 : 48,
decoration: BoxDecoration(
color: isSelected ? const Color(0xFFF5F5F5) : const Color(0xFFFAFAFA),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? const Color(0x44999999) : const Color(0x22999999),
width: isSelected ? 1.5 : 1,
),
),
child: isSelected
? Padding(
padding: const EdgeInsets.all(12),
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(0xFF999999),
),
),
Row(
children: [
const Icon(
Icons.cancel_outlined,
size: 14,
color: Color(0xFFE57373),
),
const SizedBox(width: 4),
Text(
expiredDate,
style: const TextStyle(
fontSize: 12,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
color: Color(0xFFE57373),
),
),
],
),
],
),
const SizedBox(height: 8),
// 第二行:金额信息
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
amountText,
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
color: Color(0xFF999999),
),
),
),
],
),
)
: Padding(
// 未选中状态:只显示卡片头部预览
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item.rightTypeName,
style: const TextStyle(
fontSize: 13,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
color: Color(0xFF999999),
),
),
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerRight,
child: Text(
amountText,
style: const TextStyle(
fontSize: 13,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFFE57373),
),
),
),
),
],
),
),
);
}
/// 构建操作按钮
Widget _buildActionButtons() {
return Column(
children: [
// 充值 USDT 按钮
GestureDetector(
onTap: _onDeposit,
child: Container(
width: double.infinity,
height: 48,
decoration: BoxDecoration(
color: const Color(0xFFD4AF37),
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Text(
'充值绿积分',
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
letterSpacing: 0.24,
color: Colors.white,
),
),
),
),
),
// 进入交易按钮(暂时隐藏)
// const SizedBox(height: 12),
// GestureDetector(
// onTap: _goToTrading,
// child: Container(
// width: double.infinity,
// height: 48,
// decoration: BoxDecoration(
// color: const Color(0xFF8B5A2B),
// borderRadius: BorderRadius.circular(8),
// ),
// child: const Center(
// child: Text(
// '进入交易 (卖出 DST → 绿积分)',
// style: TextStyle(
// fontSize: 16,
// fontFamily: 'Inter',
// fontWeight: FontWeight.w700,
// height: 1.5,
// letterSpacing: 0.24,
// color: Colors.white,
// ),
// ),
// ),
// ),
// ),
],
);
}
/// 处理团队种植数区域的秘密点击连续点击19次后等待2秒显示"我的同僚"
Timer? _teamPlantingUnlockTimer;
void _onTeamPlantingTap() {
final now = DateTime.now();
// 取消之前的解锁定时器
_teamPlantingUnlockTimer?.cancel();
_teamPlantingUnlockTimer = null;
// 如果距离上次点击超过1秒重置计数
if (_lastTeamPlantingTapTime != null &&
now.difference(_lastTeamPlantingTapTime!).inMilliseconds > 1000) {
_teamPlantingTapCount = 0;
}
_lastTeamPlantingTapTime = now;
_teamPlantingTapCount++;
// 达到19次时启动2秒定时器
if (_teamPlantingTapCount == 19) {
_teamPlantingUnlockTimer = Timer(const Duration(seconds: 2), () {
if (mounted) {
setState(() {
_showMyTeamTree = true;
});
}
});
}
// 超过19次重置防止累积
else if (_teamPlantingTapCount > 19) {
_teamPlantingTapCount = 0;
}
}
/// 构建团队统计
Widget _buildTeamStats() {
return Row(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFFFF5E6),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
),
child: Column(
children: [
const Text(
'引荐',
style: TextStyle(
fontSize: 12,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.5,
color: Color(0xCC5D4037),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 6),
Text(
_formatInt(_directReferralCount),
style: const TextStyle(
fontSize: 20,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.25,
color: Color(0xFF5D4037),
),
textAlign: TextAlign.center,
),
],
),
),
),
const SizedBox(width: 8),
Expanded(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFFFF5E6),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
),
child: Column(
children: [
const Text(
'本人种植树',
style: TextStyle(
fontSize: 12,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.5,
color: Color(0xCC5D4037),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 6),
Text(
_formatInt(_personalPlantingCount),
style: const TextStyle(
fontSize: 20,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.25,
color: Color(0xFF5D4037),
),
textAlign: TextAlign.center,
),
],
),
),
),
const SizedBox(width: 8),
Expanded(
child: GestureDetector(
onTap: _onTeamPlantingTap,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFFFF5E6),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
),
child: Column(
children: [
const Text(
'同僚种植树',
style: TextStyle(
fontSize: 12,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.5,
color: Color(0xCC5D4037),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 6),
Text(
_formatInt(_teamPlantingCount),
style: const TextStyle(
fontSize: 20,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.25,
color: Color(0xFF5D4037),
),
textAlign: TextAlign.center,
),
],
),
),
),
),
],
);
}
/// 构建引荐列表
Widget _buildReferralList() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(top: 8),
child: Text(
'引荐列表',
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
color: Color(0xFF5D4037),
),
),
),
const SizedBox(height: 8),
..._referrals.map((referral) => Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xCCFFF5E6),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'序列号: ${referral['serial']}',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
height: 1.43,
color: Color(0xFF5D4037),
),
),
Text(
'本人/同僚: ${referral['personal']} / ${referral['team']}',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
height: 1.43,
color: Color(0xCC5D4037),
),
),
],
),
),
)),
],
);
}
/// 构建我的同僚树
Widget _buildMyTeamTree() {
// 创建或更新根节点(缓存以保持展开状态)
// 只有在数据变化时才创建新的根节点
if (_teamTreeRootNode == null ||
_teamTreeRootNode!.accountSequence != _serialNumber ||
_teamTreeRootNode!.personalPlantingCount != _personalPlantingCount ||
_teamTreeRootNode!.teamPlantingCount != _teamPlantingCount ||
_teamTreeRootNode!.directReferralCount != _directReferralCount) {
_teamTreeRootNode = TeamTreeNode.createRoot(
accountSequence: _serialNumber,
personalPlantingCount: _personalPlantingCount,
teamPlantingCount: _teamPlantingCount,
directReferralCount: _directReferralCount,
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(top: 8),
child: Text(
'我的同僚',
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
color: Color(0xFF5D4037),
),
),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xCCFFF5E6),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
),
child: _directReferralCount == 0
? const Center(
child: Padding(
padding: EdgeInsets.all(20),
child: Text(
'暂无同僚',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
color: Color(0x995D4037),
),
),
),
)
: TeamTreeWidget(
rootNode: _teamTreeRootNode!,
referralService: ref.read(referralServiceProvider),
),
),
],
);
}
/// 构建分享按钮
Widget _buildShareButton() {
return GestureDetector(
onTap: _goToSharePage,
child: Container(
width: double.infinity,
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: const Color(0x33D4AF37),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(
Icons.share,
size: 24,
color: Color(0xFFD4AF37),
),
SizedBox(width: 8),
Text(
'分享邀请',
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
letterSpacing: 0.24,
color: Color(0xFFD4AF37),
),
),
],
),
),
);
}
/// 构建社区权益考核
Widget _buildCommunityAssessment() {
// 如果没有社区授权,显示红色提示信息
if (!_hasCommunityAuth) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0x15F44336), // 浅红色背景
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0x33F44336), // 红色边框
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'社区权益考核',
style: TextStyle(
fontSize: 18,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.56,
color: Color(0xFF5D4037),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0x20F44336),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'未授权',
style: TextStyle(
fontSize: 12,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFFF44336), // 红色
),
),
),
],
),
const SizedBox(height: 15),
const Text(
'暂无社区授权',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.43,
color: Color(0xFFF44336), // 红色
),
),
const SizedBox(height: 8),
const Text(
'请联系管理员申请社区授权',
style: TextStyle(
fontSize: 12,
fontFamily: 'Inter',
height: 1.5,
color: Color(0x99F44336), // 浅红色
),
),
],
),
);
}
// 计算进度百分比(用于进度条)
final progressRatio = _communityInitialTarget > 0
? (_communityCurrentTreeCount / _communityInitialTarget).clamp(0.0, 1.0)
: 0.0;
// 确定状态文字和颜色
final String statusText;
final Color statusColor;
if (_communityBenefitActive) {
statusText = '已激活';
statusColor = const Color(0xFF4CAF50); // 绿色
} else {
statusText = '待激活';
statusColor = const Color(0xFFFF9800); // 橙色
}
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0x80FFFFFF),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题和状态
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'社区权益考核',
style: TextStyle(
fontSize: 18,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.56,
color: Color(0xFF5D4037),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.15),
borderRadius: BorderRadius.circular(4),
),
child: Text(
statusText,
style: TextStyle(
fontSize: 12,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: statusColor,
),
),
),
],
),
const SizedBox(height: 15),
// 考核月份(仅在权益激活后显示)
if (_communityBenefitActive && _communityMonthIndex > 0) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'当前考核月份',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.43,
color: Color(0xCC5D4037),
),
),
Text(
'$_communityMonthIndex',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.43,
color: Color(0xFF5D4037),
),
),
],
),
const SizedBox(height: 16),
],
// 团队认种数量 / 目标
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_communityBenefitActive ? '团队认种 / 月度目标' : '团队认种 / 激活目标',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.43,
color: Color(0xCC5D4037),
),
),
Text(
'$_communityCurrentTreeCount / ${_communityBenefitActive ? _communityMonthlyTarget : _communityInitialTarget}',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.43,
color: Color(0xFF5D4037),
),
),
],
),
const SizedBox(height: 4),
// 进度条
Container(
height: 6,
decoration: BoxDecoration(
color: const Color(0x33D4AF37),
borderRadius: BorderRadius.circular(3),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: progressRatio,
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFD4AF37),
borderRadius: BorderRadius.circular(3),
),
),
),
),
const SizedBox(height: 16),
// 社区贡献奖励
Row(
children: [
const Text(
'社区贡献奖励',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.43,
color: Color(0xCC5D4037),
),
),
const SizedBox(width: 39),
Expanded(
child: Text(
_communityBenefitActive
? '每新增认种 1 棵可获得 576 绿积分'
: '需团队认种达到 $_communityInitialTarget 棵激活',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.43,
color: _communityBenefitActive
? const Color(0xFFD4AF37)
: const Color(0x995D4037),
),
),
),
],
),
],
),
);
}
/// 构建通用权益考核组件
/// [title] 标题,如"市团队权益考核"
/// [hasAuth] 是否有该授权
/// [benefitActive] 权益是否激活
/// [currentTreeCount] 当前团队认种数量
/// [initialTarget] 初始考核目标
/// [monthlyTarget] 月度考核目标
/// [monthIndex] 当前考核月份
/// [rewardDescription] 奖励描述
Widget _buildRoleAssessment({
required String title,
required bool hasAuth,
required bool benefitActive,
required int currentTreeCount,
required int initialTarget,
required int monthlyTarget,
required int monthIndex,
required String rewardDescription,
}) {
// 如果没有该授权,不显示
if (!hasAuth) {
return const SizedBox.shrink();
}
// 计算进度百分比(用于进度条)
final target = benefitActive ? monthlyTarget : initialTarget;
final progressRatio = target > 0
? (currentTreeCount / target).clamp(0.0, 1.0)
: 0.0;
// 确定状态文字和颜色
final String statusText;
final Color statusColor;
if (benefitActive) {
statusText = '已激活';
statusColor = const Color(0xFF4CAF50); // 绿色
} else {
statusText = '待激活';
statusColor = const Color(0xFFFF9800); // 橙色
}
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0x80FFFFFF),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题和状态
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: const TextStyle(
fontSize: 18,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.56,
color: Color(0xFF5D4037),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.15),
borderRadius: BorderRadius.circular(4),
),
child: Text(
statusText,
style: TextStyle(
fontSize: 12,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: statusColor,
),
),
),
],
),
const SizedBox(height: 15),
// 考核月份(仅在权益激活后显示)
if (benefitActive && monthIndex > 0) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'当前考核月份',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.43,
color: Color(0xCC5D4037),
),
),
Text(
'$monthIndex',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.43,
color: Color(0xFF5D4037),
),
),
],
),
const SizedBox(height: 16),
],
// 团队认种数量 / 目标
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
benefitActive ? '团队认种 / 月度目标' : '团队认种 / 激活目标',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.43,
color: Color(0xCC5D4037),
),
),
Text(
'$currentTreeCount / $target',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.43,
color: Color(0xFF5D4037),
),
),
],
),
const SizedBox(height: 4),
// 进度条
Container(
height: 6,
decoration: BoxDecoration(
color: const Color(0x33D4AF37),
borderRadius: BorderRadius.circular(3),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: progressRatio,
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFD4AF37),
borderRadius: BorderRadius.circular(3),
),
),
),
),
const SizedBox(height: 16),
// 贡献奖励
Row(
children: [
const Text(
'贡献奖励',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.43,
color: Color(0xCC5D4037),
),
),
const SizedBox(width: 39),
Expanded(
child: Text(
benefitActive
? rewardDescription
: '需团队认种达到 $initialTarget 棵激活',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.43,
color: benefitActive
? const Color(0xFFD4AF37)
: const Color(0x995D4037),
),
),
),
],
),
],
),
);
}
/// 构建设置菜单
Widget _buildSettingsMenu() {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0x80FFFFFF),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
),
child: Column(
children: [
// 谷歌验证器功能暂时隐藏(功能保留)
// _buildSettingItem(
// icon: Icons.verified_user,
// title: '谷歌验证器',
// onTap: _goToGoogleAuth,
// ),
_buildSettingItem(
icon: Icons.lock,
title: '修改登录密码',
onTap: _goToChangePassword,
),
_buildSettingItem(
icon: Icons.email,
title: '绑定邮箱',
onTap: _goToBindEmail,
),
_buildSettingItem(
icon: Icons.verified_user,
title: '实名认证',
onTap: _goToKyc,
),
_buildSettingItem(
icon: Icons.verified,
title: '自助申请授权',
onTap: _goToAuthorizationApply,
),
],
),
);
}
/// 构建设置项
Widget _buildSettingItem({
required IconData icon,
required String title,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Row(
children: [
Icon(
icon,
size: 24,
color: const Color(0xFF8B5A2B),
),
const SizedBox(width: 16),
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.5,
color: Color(0xFF5D4037),
),
),
),
const Icon(
Icons.chevron_right,
size: 24,
color: Color(0xFF8B5A2B),
),
],
),
),
);
}
/// 构建应用版本信息
Widget _buildAppVersionInfo() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFFF5E6),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
const Row(
children: [
Icon(
Icons.info_outline,
size: 20,
color: Color(0xFF8B5A2B),
),
SizedBox(width: 8),
Text(
'应用信息',
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
height: 1.5,
color: Color(0xFF5D4037),
),
),
],
),
const SizedBox(height: 12),
// 版本信息列表
_buildVersionInfoRow('应用版本', 'v$_appVersion'),
const SizedBox(height: 8),
_buildVersionInfoRow('构建号', _buildNumber),
const SizedBox(height: 8),
_buildVersionInfoRow('构建模式', kReleaseMode ? 'Release' : (kDebugMode ? 'Debug' : 'Profile')),
const SizedBox(height: 8),
_buildVersionInfoRow('包名', _packageName),
const Divider(
height: 24,
color: Color(0x33D4AF37),
),
// 设备信息
_buildVersionInfoRow('设备型号', _deviceModel),
const SizedBox(height: 8),
_buildVersionInfoRow('系统版本', _osVersion),
const SizedBox(height: 8),
_buildVersionInfoRow('平台', _platform),
const SizedBox(height: 12),
// 版权信息
Center(
child: Text(
'© ${DateTime.now().year} RWADurian. All rights reserved.',
style: const TextStyle(
fontSize: 11,
fontFamily: 'Inter',
height: 1.5,
color: Color(0x995D4037),
),
),
),
],
),
);
}
/// 构建版本信息行
Widget _buildVersionInfoRow(String label, String value) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
height: 1.5,
color: Color(0xCC5D4037),
),
),
Flexible(
child: Text(
value,
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.5,
color: Color(0xFF5D4037),
),
textAlign: TextAlign.right,
overflow: TextOverflow.ellipsis,
),
),
],
);
}
/// 构建账号操作按钮(切换账号、退出登录)
Widget _buildAccountActionButtons() {
return Column(
children: [
// 切换账号按钮
GestureDetector(
onTap: _onSwitchAccount,
child: Container(
width: double.infinity,
height: 48,
decoration: BoxDecoration(
color: const Color(0xFFFFF5E6),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.swap_horiz,
size: 20,
color: Color(0xFF8B5A2B),
),
SizedBox(width: 8),
Text(
'切换账号',
style: TextStyle(
fontSize: 15,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.5,
color: Color(0xFF5D4037),
),
),
],
),
),
),
const SizedBox(height: 12),
// 退出登录按钮
GestureDetector(
onTap: _onLogout,
child: Container(
width: double.infinity,
height: 48,
decoration: BoxDecoration(
color: const Color(0xFFFFF5E6),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0x33E57373),
width: 1,
),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.logout,
size: 20,
color: Color(0xFFE57373),
),
SizedBox(width: 8),
Text(
'退出登录',
style: TextStyle(
fontSize: 15,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.5,
color: Color(0xFFE57373),
),
),
],
),
),
),
const SizedBox(height: 32), // 底部留白
],
);
}
/// 切换账号 - 跳转到账号切换页面
void _onSwitchAccount() {
context.push(RoutePaths.accountSwitch);
}
/// 退出登录
void _onLogout() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('退出登录'),
content: const Text('确定要退出当前账号吗?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
_performLogout();
},
style: TextButton.styleFrom(
foregroundColor: const Color(0xFFE57373),
),
child: const Text('退出'),
),
],
),
);
}
/// 执行退出登录
Future<void> _performLogout() async {
try {
final multiAccountService = ref.read(multiAccountServiceProvider);
// 退出当前账号(保留账号数据)
await multiAccountService.logoutCurrentAccount();
// 导航到向导页
if (mounted) {
context.go(RoutePaths.guide);
}
} catch (e) {
debugPrint('[ProfilePage] 退出登录失败: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('退出登录失败: $e')),
);
}
}
}
/// 构建序列号或钱包生成状态显示
/// 逻辑:先检查 isWalletReady 标志,如果 ready 就显示序列号
/// 如果不 ready_checkAndStartWalletPolling 会去 API 检查并轮询
Widget _buildSerialNumberOrStatus() {
final walletStatus = ref.watch(walletStatusProvider);
final authState = ref.watch(authProvider);
// 1. 钱包已就绪(本地标志 或 API状态显示序列号
if (authState.isWalletReady || walletStatus.isReady) {
if (_serialNumber.isNotEmpty && _serialNumber != '--') {
return Row(
children: [
Text(
'序列号: $_serialNumber',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
height: 1.5,
color: Color(0xCC5D4037),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: _copySerialNumber,
child: const Icon(
Icons.copy,
size: 16,
color: Color(0xCC5D4037),
),
),
],
);
}
}
// 2. 钱包未就绪,显示审核中状态
return Row(
children: [
const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
),
),
const SizedBox(width: 8),
Text(
'创建账号审核中...',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
height: 1.5,
color: Color(0xFFD4AF37),
fontStyle: FontStyle.italic,
),
),
],
);
}
}