feat(profile): 添加懒加载、防抖和失败重试机制
- 添加 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 <noreply@anthropic.com>
This commit is contained in:
parent
e75b968aeb
commit
d29ff0975b
|
|
@ -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<void> 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<LazyLoadSection> createState() => _LazyLoadSectionState();
|
||||
}
|
||||
|
||||
class _LazyLoadSectionState extends State<LazyLoadSection> {
|
||||
LoadState _state = LoadState.idle;
|
||||
String _errorMessage = '';
|
||||
int _retryCount = 0;
|
||||
bool _hasBeenVisible = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.loadImmediately) {
|
||||
_loadData();
|
||||
}
|
||||
}
|
||||
|
||||
/// 执行数据加载
|
||||
Future<void> _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<void> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ProfilePage> {
|
|||
// 通知未读数量
|
||||
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<ProfilePage> {
|
|||
}
|
||||
|
||||
/// 加载推荐数据 (from referral-service)
|
||||
Future<void> _loadReferralData() async {
|
||||
/// [isRefresh] 是否为刷新操作(刷新时不显示加载状态)
|
||||
Future<void> _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<ProfilePage> {
|
|||
|
||||
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<ProfilePage> {
|
|||
} 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<void> _loadAuthorizationData() async {
|
||||
/// [isRefresh] 是否为刷新操作(刷新时不显示加载状态)
|
||||
Future<void> _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<ProfilePage> {
|
|||
|
||||
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<ProfilePage> {
|
|||
} 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<ProfilePage> {
|
|||
}
|
||||
|
||||
/// 加载收益数据 (直接从 reward-service 获取)
|
||||
Future<void> _loadWalletData() async {
|
||||
/// [isRefresh] 是否为刷新操作(刷新时不显示加载状态)
|
||||
Future<void> _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<ProfilePage> {
|
|||
_remainingSeconds = summary.pendingRemainingSeconds;
|
||||
_pendingRewards = pendingRewards;
|
||||
_isLoadingWallet = false;
|
||||
_walletError = null;
|
||||
_walletRetryCount = 0;
|
||||
});
|
||||
|
||||
debugPrint('[ProfilePage] UI 状态更新完成');
|
||||
|
|
@ -546,8 +631,15 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
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<ProfilePage> {
|
|||
@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<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) {
|
||||
|
|
@ -747,6 +977,85 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
);
|
||||
}
|
||||
|
||||
/// 构建骨架屏
|
||||
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<ProfilePage> {
|
|||
),
|
||||
),
|
||||
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<ProfilePage> {
|
|||
);
|
||||
}
|
||||
|
||||
/// 构建推荐人信息卡片
|
||||
/// 构建推荐人信息卡片(包含授权数据)
|
||||
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<ProfilePage> {
|
|||
|
||||
/// 构建主要内容卡片
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue