From d29ff0975b3ae19a787c4cc55144bff50edac764 Mon Sep 17 00:00:00 2001 From: hailin Date: Mon, 15 Dec 2025 20:50:27 -0800 Subject: [PATCH] =?UTF-8?q?feat(profile):=20=E6=B7=BB=E5=8A=A0=E6=87=92?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E3=80=81=E9=98=B2=E6=8A=96=E5=92=8C=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E9=87=8D=E8=AF=95=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 VisibilityDetector 实现懒加载,滚动到可见区域才加载数据 - 添加 300ms 防抖机制,防止快速滑动触发大量 API 请求 - 添加失败自动重试(最多3次,指数退避:1s→2s→4s) - 添加 60 秒定时刷新可见区域数据 - 添加下拉刷新功能 - 添加 Shimmer 骨架屏加载状态 - 添加错误状态 UI 和手动重试按钮 - 创建通用 LazyLoadSection 组件 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../lib/core/widgets/lazy_load_section.dart | 277 +++++++++++ .../presentation/pages/profile_page.dart | 451 ++++++++++++++++-- frontend/mobile-app/pubspec.lock | 8 + frontend/mobile-app/pubspec.yaml | 1 + 4 files changed, 693 insertions(+), 44 deletions(-) create mode 100644 frontend/mobile-app/lib/core/widgets/lazy_load_section.dart diff --git a/frontend/mobile-app/lib/core/widgets/lazy_load_section.dart b/frontend/mobile-app/lib/core/widgets/lazy_load_section.dart new file mode 100644 index 00000000..bc191c37 --- /dev/null +++ b/frontend/mobile-app/lib/core/widgets/lazy_load_section.dart @@ -0,0 +1,277 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:visibility_detector/visibility_detector.dart'; + +/// 数据加载状态 +enum LoadState { + /// 未开始加载 + idle, + /// 加载中 + loading, + /// 加载成功 + success, + /// 加载失败 + error, +} + +/// 懒加载区域组件 +/// +/// 当区域进入可视区域时自动触发数据加载,支持: +/// - 懒加载:滚动到可见时才加载 +/// - 失败重试:支持手动重试和自动重试 +/// - 骨架屏:加载中显示 Shimmer 效果 +/// - 错误状态:显示错误信息和重试按钮 +class LazyLoadSection extends StatefulWidget { + /// 唯一标识符,用于 VisibilityDetector + final String sectionKey; + + /// 数据加载函数,返回 Future + final Future Function() onLoad; + + /// 加载成功后显示的内容 + final Widget child; + + /// 骨架屏高度(可选,默认 100) + final double skeletonHeight; + + /// 自定义骨架屏(可选) + final Widget? skeleton; + + /// 自定义错误组件(可选) + final Widget Function(String error, VoidCallback retry)? errorBuilder; + + /// 最大重试次数(默认 3 次) + final int maxRetries; + + /// 重试延迟基数(毫秒,指数退避) + final int retryDelayMs; + + /// 可见度阈值(0-1,默认 0.1 表示 10% 可见时触发) + final double visibilityThreshold; + + /// 是否立即加载(不等待可见,用于首屏数据) + final bool loadImmediately; + + const LazyLoadSection({ + super.key, + required this.sectionKey, + required this.onLoad, + required this.child, + this.skeletonHeight = 100, + this.skeleton, + this.errorBuilder, + this.maxRetries = 3, + this.retryDelayMs = 1000, + this.visibilityThreshold = 0.1, + this.loadImmediately = false, + }); + + @override + State createState() => _LazyLoadSectionState(); +} + +class _LazyLoadSectionState extends State { + LoadState _state = LoadState.idle; + String _errorMessage = ''; + int _retryCount = 0; + bool _hasBeenVisible = false; + + @override + void initState() { + super.initState(); + if (widget.loadImmediately) { + _loadData(); + } + } + + /// 执行数据加载 + Future _loadData() async { + if (_state == LoadState.loading) return; + + setState(() { + _state = LoadState.loading; + _errorMessage = ''; + }); + + try { + await widget.onLoad(); + if (mounted) { + setState(() { + _state = LoadState.success; + _retryCount = 0; + }); + } + } catch (e) { + debugPrint('[LazyLoadSection] ${widget.sectionKey} 加载失败: $e'); + if (mounted) { + setState(() { + _state = LoadState.error; + _errorMessage = e.toString().replaceAll('Exception: ', ''); + }); + // 自动重试 + _scheduleAutoRetry(); + } + } + } + + /// 安排自动重试(指数退避) + void _scheduleAutoRetry() { + if (_retryCount >= widget.maxRetries) { + debugPrint('[LazyLoadSection] ${widget.sectionKey} 已达最大重试次数'); + return; + } + + final delay = widget.retryDelayMs * (1 << _retryCount); // 指数退避 + debugPrint('[LazyLoadSection] ${widget.sectionKey} 将在 ${delay}ms 后重试 (第 ${_retryCount + 1} 次)'); + + Future.delayed(Duration(milliseconds: delay), () { + if (mounted && _state == LoadState.error) { + _retryCount++; + _loadData(); + } + }); + } + + /// 手动重试 + void _manualRetry() { + _retryCount = 0; + _loadData(); + } + + /// 处理可见性变化 + void _onVisibilityChanged(VisibilityInfo info) { + if (_hasBeenVisible) return; + + if (info.visibleFraction >= widget.visibilityThreshold) { + _hasBeenVisible = true; + if (_state == LoadState.idle) { + _loadData(); + } + } + } + + @override + Widget build(BuildContext context) { + return VisibilityDetector( + key: Key('lazy_${widget.sectionKey}'), + onVisibilityChanged: _onVisibilityChanged, + child: _buildContent(), + ); + } + + Widget _buildContent() { + switch (_state) { + case LoadState.idle: + case LoadState.loading: + return _buildSkeleton(); + case LoadState.error: + return _buildError(); + case LoadState.success: + return widget.child; + } + } + + /// 构建骨架屏 + Widget _buildSkeleton() { + if (widget.skeleton != null) { + return widget.skeleton!; + } + + return Shimmer.fromColors( + baseColor: const Color(0xFFE0E0E0), + highlightColor: const Color(0xFFF5F5F5), + child: Container( + height: widget.skeletonHeight, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + ), + ); + } + + /// 构建错误状态 + Widget _buildError() { + if (widget.errorBuilder != null) { + return widget.errorBuilder!(_errorMessage, _manualRetry); + } + + return Container( + height: widget.skeletonHeight, + 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 >= widget.maxRetries + ? '加载失败,点击重试' + : '加载失败,正在重试...', + style: const TextStyle( + fontSize: 12, + color: Color(0xFFE65100), + ), + textAlign: TextAlign.center, + ), + if (_retryCount >= widget.maxRetries) ...[ + const SizedBox(height: 8), + GestureDetector( + onTap: _manualRetry, + 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, + ), + ), + ), + ), + ], + ], + ), + ); + } +} + +/// 带下拉刷新的懒加载列表包装器 +class LazyLoadRefreshWrapper extends StatelessWidget { + final Widget child; + final Future Function() onRefresh; + + const LazyLoadRefreshWrapper({ + super.key, + required this.child, + required this.onRefresh, + }); + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: onRefresh, + color: const Color(0xFFD4AF37), + backgroundColor: Colors.white, + child: child, + ); + } +} diff --git a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart index 1f601fe6..43a5b055 100644 --- a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart +++ b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart @@ -8,6 +8,8 @@ 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/services/referral_service.dart'; import '../../../../core/services/reward_service.dart'; @@ -125,20 +127,52 @@ class _ProfilePageState extends ConsumerState { // 通知未读数量 int _unreadNotificationCount = 0; + // ========== 懒加载 + 重试机制相关状态 ========== + // 各区域加载状态 + bool _isLoadingUserData = true; + bool _isLoadingReferral = true; + bool _isLoadingAuthorization = true; + 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 防抖延迟 + @override void initState() { super.initState(); - // 先同步检查本地头像,再异步加载其他数据 + // 首屏数据:用户基本信息(从本地存储优先) _checkLocalAvatarSync(); _loadUserData(); _loadAppInfo(); - // 加载推荐和授权数据 - _loadReferralData(); - _loadAuthorizationData(); - // 加载钱包和收益数据 - _loadWalletData(); - // 加载通知未读数量 + // 通知数量(轻量级请求) _loadUnreadNotificationCount(); + // 启动定时刷新(可见区域的数据) + _startAutoRefreshTimer(); } /// 加载应用信息 @@ -290,7 +324,15 @@ class _ProfilePageState extends ConsumerState { } /// 加载推荐数据 (from referral-service) - Future _loadReferralData() async { + /// [isRefresh] 是否为刷新操作(刷新时不显示加载状态) + Future _loadReferralData({bool isRefresh = false}) async { + if (!isRefresh) { + setState(() { + _isLoadingReferral = true; + _referralError = null; + }); + } + try { debugPrint('[ProfilePage] 开始加载推荐数据...'); final referralService = ref.read(referralServiceProvider); @@ -308,6 +350,9 @@ class _ProfilePageState extends ConsumerState { if (mounted) { setState(() { + _isLoadingReferral = false; + _referralError = null; + _referralRetryCount = 0; _directReferralCount = referralInfo.directReferralCount; _totalTeamCount = referralInfo.totalTeamCount; _personalPlantingCount = referralInfo.personalPlantingCount; @@ -326,12 +371,32 @@ class _ProfilePageState extends ConsumerState { } 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) - Future _loadAuthorizationData() async { + /// [isRefresh] 是否为刷新操作(刷新时不显示加载状态) + Future _loadAuthorizationData({bool isRefresh = false}) async { + if (!isRefresh) { + setState(() { + _isLoadingAuthorization = true; + _authorizationError = null; + }); + } + try { debugPrint('[ProfilePage] 开始加载授权数据...'); final authorizationService = ref.read(authorizationServiceProvider); @@ -363,6 +428,9 @@ class _ProfilePageState extends ConsumerState { if (mounted) { setState(() { + _isLoadingAuthorization = false; + _authorizationError = null; + _authorizationRetryCount = 0; _community = summary.communityName ?? '--'; _authCityCompany = summary.authCityCompanyName ?? '--'; _cityCompany = summary.cityCompanyName ?? '--'; @@ -439,7 +507,19 @@ class _ProfilePageState extends ConsumerState { } 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, + ); + } } } @@ -474,14 +554,17 @@ class _ProfilePageState extends ConsumerState { } /// 加载收益数据 (直接从 reward-service 获取) - Future _loadWalletData() async { + /// [isRefresh] 是否为刷新操作(刷新时不显示加载状态) + Future _loadWalletData({bool isRefresh = false}) async { try { debugPrint('[ProfilePage] ========== 加载收益数据 =========='); - debugPrint('[ProfilePage] mounted: $mounted'); - setState(() { - _isLoadingWallet = true; - _walletError = null; - }); + debugPrint('[ProfilePage] mounted: $mounted, isRefresh: $isRefresh'); + if (!isRefresh) { + setState(() { + _isLoadingWallet = true; + _walletError = null; + }); + } debugPrint('[ProfilePage] 获取 rewardServiceProvider...'); final rewardService = ref.read(rewardServiceProvider); @@ -519,6 +602,8 @@ class _ProfilePageState extends ConsumerState { _remainingSeconds = summary.pendingRemainingSeconds; _pendingRewards = pendingRewards; _isLoadingWallet = false; + _walletError = null; + _walletRetryCount = 0; }); debugPrint('[ProfilePage] UI 状态更新完成'); @@ -546,8 +631,15 @@ class _ProfilePageState extends ConsumerState { if (mounted) { setState(() { _isLoadingWallet = false; - _walletError = '加载失败,点击重试'; + _walletError = '加载失败'; }); + // 自动重试 + _scheduleRetry( + section: '收益数据', + retryCount: _walletRetryCount, + loadFunction: () => _loadWalletData(), + updateRetryCount: (count) => _walletRetryCount = count, + ); } } } @@ -566,9 +658,147 @@ class _ProfilePageState extends ConsumerState { @override void dispose() { _timer?.cancel(); + _refreshTimer?.cancel(); + // 取消防抖定时器 + _referralDebounceTimer?.cancel(); + _authorizationDebounceTimer?.cancel(); + _walletDebounceTimer?.cancel(); super.dispose(); } + /// 启动定时刷新(每60秒刷新一次可见区域的数据) + void _startAutoRefreshTimer() { + _refreshTimer = Timer.periodic( + const Duration(seconds: _autoRefreshIntervalSeconds), + (_) => _refreshVisibleSections(), + ); + } + + /// 刷新当前可见区域的数据 + Future _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 _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 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) { @@ -747,6 +977,85 @@ class _ProfilePageState extends ConsumerState { ); } + /// 构建骨架屏 + 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( @@ -765,31 +1074,49 @@ class _ProfilePageState extends ConsumerState { ), ), child: SafeArea( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 页面标题行(带通知图标) - _buildPageHeader(), - const SizedBox(height: 16), - // 用户头像和基本信息 - _buildUserHeader(), - const SizedBox(height: 16), - // 推荐人信息卡片 - _buildReferralInfoCard(), - const SizedBox(height: 16), - // 社区/省份标签 - _buildCommunityLabel(), - const SizedBox(height: 8), - // 认种按钮 - _buildPlantingButton(), - const SizedBox(height: 16), - // 主要内容卡片 - _buildMainContentCard(), - const SizedBox(height: 24), - ], + 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), + // 推荐人信息卡片(懒加载:推荐数据) + 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), + ], + ), ), ), ), @@ -1074,8 +1401,26 @@ class _ProfilePageState extends ConsumerState { ); } - /// 构建推荐人信息卡片 + /// 构建推荐人信息卡片(包含授权数据) Widget _buildReferralInfoCard() { + // 显示加载状态(骨架屏) + if (_isLoadingAuthorization && !_hasLoadedAuthorization) { + return _buildSkeleton(height: 180); + } + + // 显示错误状态(带重试) + if (_authorizationError != null && _authorizationRetryCount >= _maxRetries) { + return _buildErrorRetry( + error: _authorizationError!, + onRetry: () { + _authorizationRetryCount = 0; + _loadAuthorizationData(); + }, + retryCount: _authorizationRetryCount, + height: 180, + ); + } + return Container( width: double.infinity, padding: const EdgeInsets.all(16), @@ -1214,6 +1559,24 @@ class _ProfilePageState extends ConsumerState { /// 构建主要内容卡片 Widget _buildMainContentCard() { + // 显示加载状态(骨架屏)- 收益数据 + if (_isLoadingWallet && !_hasLoadedWallet) { + return _buildSkeleton(height: 400); + } + + // 显示错误状态(带重试)- 收益数据 + if (_walletError != null && _walletRetryCount >= _maxRetries) { + return _buildErrorRetry( + error: _walletError!, + onRetry: () { + _walletRetryCount = 0; + _loadWalletData(); + }, + retryCount: _walletRetryCount, + height: 400, + ); + } + return Container( width: double.infinity, padding: const EdgeInsets.all(16), diff --git a/frontend/mobile-app/pubspec.lock b/frontend/mobile-app/pubspec.lock index f3f93d46..08e6d7dd 100644 --- a/frontend/mobile-app/pubspec.lock +++ b/frontend/mobile-app/pubspec.lock @@ -1586,6 +1586,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + visibility_detector: + dependency: "direct main" + description: + name: visibility_detector + sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 + url: "https://pub.dev" + source: hosted + version: "0.4.0+2" vm_service: dependency: transitive description: diff --git a/frontend/mobile-app/pubspec.yaml b/frontend/mobile-app/pubspec.yaml index b4d0f938..f6878090 100644 --- a/frontend/mobile-app/pubspec.yaml +++ b/frontend/mobile-app/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: flutter_svg: ^2.0.10+1 cached_network_image: ^3.3.1 shimmer: ^3.0.0 + visibility_detector: ^0.4.0+2 lottie: ^3.1.0 qr_flutter: ^4.1.0 mobile_scanner: ^5.1.1