rwadurian/frontend/mobile-app/lib/features/auth/presentation/pages/splash_page.dart

289 lines
9.1 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 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../../routes/route_paths.dart';
import '../../../../bootstrap.dart';
import '../../../../core/providers/maintenance_provider.dart';
import '../../../../core/di/injection_container.dart';
import '../../../../core/services/pending_action_polling_service.dart';
import '../../../../routes/app_router.dart';
import '../providers/auth_provider.dart';
/// 开屏页面 - 应用启动时显示的第一个页面
/// 播放帧动画,动画结束后检查用户认证状态并跳转
class SplashPage extends ConsumerStatefulWidget {
const SplashPage({super.key});
@override
ConsumerState<SplashPage> createState() => _SplashPageState();
}
class _SplashPageState extends ConsumerState<SplashPage> {
/// 帧动画总帧数
static const int _totalFrames = 36;
/// 帧动画帧率 (fps)
static const int _frameRate = 15;
/// 当前显示的帧索引 (0-based)
int _currentFrameIndex = 0;
/// 是否显示跳过按钮
bool _showSkipButton = false;
/// 是否已经开始跳转(防止重复跳转)
bool _isNavigating = false;
/// 帧动画是否正在播放
bool _isPlaying = true;
/// 预加载的图片缓存
final List<ImageProvider> _frameProviders = [];
@override
void initState() {
super.initState();
_initializeFrames();
}
/// 初始化帧动画
void _initializeFrames() {
// 创建所有帧的 ImageProvider不会立即加载只是创建引用
for (int i = 1; i <= _totalFrames; i++) {
final frameNumber = i.toString().padLeft(3, '0');
final framePath = 'assets/images/splash_frames/frame_$frameNumber.png';
_frameProviders.add(AssetImage(framePath));
}
// 启动帧动画播放
_playAnimation();
// 1秒后显示跳过按钮
Future.delayed(const Duration(seconds: 1), () {
if (mounted) {
setState(() {
_showSkipButton = true;
});
}
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 预加载前几帧到内存,确保播放流畅
_precacheInitialFrames();
}
/// 预加载前几帧
void _precacheInitialFrames() {
// 预加载前5帧确保启动时不卡顿
final framesToPrecache = _totalFrames < 5 ? _totalFrames : 5;
for (int i = 0; i < framesToPrecache; i++) {
precacheImage(_frameProviders[i], context);
}
}
/// 播放帧动画
Future<void> _playAnimation() async {
final frameDuration = Duration(milliseconds: 1000 ~/ _frameRate);
while (_isPlaying && _currentFrameIndex < _totalFrames - 1 && mounted) {
await Future.delayed(frameDuration);
if (!mounted || !_isPlaying) break;
setState(() {
_currentFrameIndex++;
});
// 预加载后续帧提前2帧预加载
final nextFrameIndex = _currentFrameIndex + 2;
if (nextFrameIndex < _totalFrames) {
precacheImage(_frameProviders[nextFrameIndex], context);
}
}
// 动画播放完成
if (_currentFrameIndex >= _totalFrames - 1 && mounted && _isPlaying) {
_navigateToNextPage();
}
}
/// 跳过动画
void _skipAnimation() {
_isPlaying = false;
_navigateToNextPage();
}
/// 启动待办操作轮询
///
/// 仅在启动时没有检测到待办操作时调用。
/// 轮询检测到待办后会自动停止并跳转到待办页面。
void _startPendingActionPolling() {
final pollingService = ref.read(pendingActionPollingServiceProvider);
pollingService.start(
onPendingActionDetected: () {
debugPrint('[SplashPage] 轮询检测到待办操作 → 跳转到待办操作页面');
// 使用 navigatorKey 进行跳转,因为此时 SplashPage 可能已经不在树上
_navigateToPendingActions();
},
);
}
/// 跳转到待办操作页面
void _navigateToPendingActions() {
// 使用全局 rootNavigatorKey 进行跳转,因为此时 SplashPage 已不在 widget 树上
final navigatorContext = rootNavigatorKey.currentContext;
if (navigatorContext != null) {
GoRouter.of(navigatorContext).go(RoutePaths.pendingActions);
} else {
debugPrint('[SplashPage] 无法获取 Navigator Context跳转失败');
}
}
/// 导航到下一个页面
Future<void> _navigateToNextPage() async {
// 防止重复跳转
if (_isNavigating) return;
_isNavigating = true;
// 初始化遥测服务(需要 BuildContext
await initializeTelemetry(context);
if (!mounted) return;
// 检查系统维护状态(优先级最高)
final isUnderMaintenance = await ref
.read(maintenanceProvider.notifier)
.showMaintenanceDialogIfNeeded(context);
if (isUnderMaintenance) {
// 系统正在维护,弹窗已显示,不进行跳转
debugPrint('[SplashPage] 系统维护中 → 显示维护弹窗');
_isNavigating = false; // 允许维护恢复后重新跳转
return;
}
// 检查认证状态
await ref.read(authProvider.notifier).checkAuthStatus();
if (!mounted) return;
final authState = ref.read(authProvider);
// 根据认证状态决定跳转目标
// 优先级:
// 1. 账号已创建 → 检查待办操作 → 有待办则跳转待办页 → 无待办则跳转主页面
// 2. 首次打开 → 向导页
// 3. 已看过向导但账号未创建(退出登录后) → 登录页面
if (authState.isAccountCreated) {
// 账号已创建,检查是否有待办操作
debugPrint('[SplashPage] 账号已创建 → 检查待办操作');
try {
final pendingActionCheckService = ref.read(pendingActionCheckServiceProvider);
final hasPending = await pendingActionCheckService.hasPendingActions();
if (!mounted) return;
if (hasPending) {
// 有待办操作,跳转到待办操作页面(不启动轮询)
debugPrint('[SplashPage] 有待办操作 → 跳转到待办操作页面');
context.go(RoutePaths.pendingActions);
} else {
// 无待办操作,启动轮询检查并跳转到主页面
debugPrint('[SplashPage] 无待办操作 → 启动轮询并跳转到龙虎榜');
_startPendingActionPolling();
context.go(RoutePaths.ranking);
}
} catch (e) {
// 检查失败,不阻止用户使用,启动轮询并跳转到主页面
debugPrint('[SplashPage] 检查待办操作失败: $e → 启动轮询并跳转到龙虎榜');
_startPendingActionPolling();
context.go(RoutePaths.ranking);
}
} else if (authState.isFirstLaunch) {
// 首次打开,进入向导页
debugPrint('[SplashPage] 首次打开 → 跳转到向导页');
context.go(RoutePaths.guide);
} else {
// 已看过向导但账号未创建(退出登录后),跳转到登录页面
debugPrint('[SplashPage] 已看过向导,账号未创建 → 跳转到登录页面');
context.go(RoutePaths.phoneLogin);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
fit: StackFit.expand,
children: [
// 帧动画(全屏覆盖)
_buildFrameAnimation(),
// 跳过按钮
if (_showSkipButton)
Positioned(
top: MediaQuery.of(context).padding.top + 16,
right: 16,
child: _buildSkipButton(),
),
],
),
);
}
/// 构建帧动画视图
Widget _buildFrameAnimation() {
return SizedBox.expand(
child: Image(
image: _frameProviders[_currentFrameIndex],
fit: BoxFit.cover,
gaplessPlayback: true, // 关键:防止帧切换时闪烁
),
);
}
/// 构建跳过按钮
Widget _buildSkipButton() {
return GestureDetector(
onTap: _skipAnimation,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withValues(alpha: 0.3),
width: 1,
),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'跳过',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
SizedBox(width: 4),
Icon(
Icons.skip_next,
size: 18,
color: Colors.white,
),
],
),
),
);
}
}