refactor(splash): 简化开屏动画为纯帧播放,恢复监控Tab

- 重构 splash_page.dart: 移除 video_player 依赖,改用纯帧动画播放
- 帧数从53张调整为36张,添加预加载优化
- 移除 pubspec.yaml 中的 video_player 依赖
- 恢复底部导航栏"监控"Tab(原"矿机")
- 调整路由索引:0-龙虎榜, 1-监控, 2-兑换, 3-我
- 移除"我的"页面的"领取全部"按钮

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-15 03:28:53 -08:00
parent f3a475a6f4
commit 306f003679
59 changed files with 79 additions and 300 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 866 KiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 943 KiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 968 KiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 986 KiB

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 985 KiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 931 KiB

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 975 KiB

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 KiB

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 971 KiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 954 KiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 KiB

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 960 KiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 972 KiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 KiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 972 KiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 921 KiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 958 KiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 959 KiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 935 KiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 904 KiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 820 KiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 867 KiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 872 KiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 861 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 840 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 767 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 812 KiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 823 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 805 KiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 781 KiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 693 KiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 760 KiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 788 KiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 KiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 806 KiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 734 KiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 820 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 857 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 876 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 817 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 894 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 924 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 936 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 941 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 896 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 936 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 941 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 936 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 923 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 870 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 874 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 873 KiB

View File

@ -1,14 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:video_player/video_player.dart';
import '../../../../routes/route_paths.dart';
import '../../../../routes/app_router.dart';
import '../../../../bootstrap.dart';
import '../providers/auth_provider.dart';
/// -
///
///
class SplashPage extends ConsumerStatefulWidget {
const SplashPage({super.key});
@ -17,11 +16,14 @@ class SplashPage extends ConsumerStatefulWidget {
}
class _SplashPageState extends ConsumerState<SplashPage> {
///
VideoPlayerController? _videoController;
///
static const int _totalFrames = 36;
///
bool _isVideoInitialized = false;
/// (fps)
static const int _frameRate = 15;
/// (0-based)
int _currentFrameIndex = 0;
///
bool _showSkipButton = false;
@ -29,117 +31,29 @@ class _SplashPageState extends ConsumerState<SplashPage> {
///
bool _isNavigating = false;
/// 使 fallback
bool _videoFailed = false;
///
static const int _totalFrames = 53;
/// (fps)
static const int _frameRate = 15;
///
int _currentFrameIndex = 0;
///
bool _isFrameAnimationPlaying = false;
bool _isPlaying = true;
///
final List<ImageProvider> _frameProviders = [];
@override
void initState() {
super.initState();
_initializeVideo();
_initializeFrames();
}
@override
void dispose() {
_videoController?.removeListener(_onVideoStateChanged);
_videoController?.dispose();
super.dispose();
}
///
Future<void> _initializeVideo() async {
debugPrint('[SplashPage] ========== 开始初始化视频 ==========');
debugPrint('[SplashPage] 视频路径: assets/videos/splash.mp4');
try {
// assets
debugPrint('[SplashPage] 创建 VideoPlayerController...');
_videoController = VideoPlayerController.asset('assets/videos/splash.mp4');
debugPrint('[SplashPage] 开始 initialize()...');
final stopwatch = Stopwatch()..start();
await _videoController!.initialize();
stopwatch.stop();
debugPrint('[SplashPage] initialize() 完成,耗时: ${stopwatch.elapsedMilliseconds}ms');
//
final value = _videoController!.value;
debugPrint('[SplashPage] 视频信息:');
debugPrint('[SplashPage] - 尺寸: ${value.size.width} x ${value.size.height}');
debugPrint('[SplashPage] - 时长: ${value.duration.inMilliseconds}ms');
debugPrint('[SplashPage] - 是否已初始化: ${value.isInitialized}');
debugPrint('[SplashPage] - 是否有错误: ${value.hasError}');
if (value.hasError) {
debugPrint('[SplashPage] - 错误信息: ${value.errorDescription}');
}
if (mounted) {
setState(() {
_isVideoInitialized = true;
});
//
_videoController!.addListener(_onVideoStateChanged);
//
debugPrint('[SplashPage] 开始播放视频...');
await _videoController!.play();
debugPrint('[SplashPage] play() 调用完成isPlaying: ${_videoController!.value.isPlaying}');
// 1
Future.delayed(const Duration(seconds: 1), () {
if (mounted) {
setState(() {
_showSkipButton = true;
});
}
});
}
} catch (e, stackTrace) {
debugPrint('[SplashPage] ========== 视频初始化失败 ==========');
debugPrint('[SplashPage] 错误类型: ${e.runtimeType}');
debugPrint('[SplashPage] 错误信息: $e');
debugPrint('[SplashPage] 堆栈跟踪:\n$stackTrace');
// controller
if (_videoController != null) {
final value = _videoController!.value;
debugPrint('[SplashPage] Controller 状态:');
debugPrint('[SplashPage] - 是否已初始化: ${value.isInitialized}');
debugPrint('[SplashPage] - 是否有错误: ${value.hasError}');
if (value.hasError) {
debugPrint('[SplashPage] - 错误描述: ${value.errorDescription}');
}
}
// fallback
_startFallbackAnimation();
///
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));
}
}
/// fallback 使
void _startFallbackAnimation() {
debugPrint('[SplashPage] 启动 fallback 帧动画,共 $_totalFrames');
setState(() {
_videoFailed = true;
_isFrameAnimationPlaying = true;
_currentFrameIndex = 0;
});
//
_playFrameAnimation();
_playAnimation();
// 1
Future.delayed(const Duration(seconds: 1), () {
@ -151,46 +65,51 @@ class _SplashPageState extends ConsumerState<SplashPage> {
});
}
@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> _playFrameAnimation() async {
Future<void> _playAnimation() async {
final frameDuration = Duration(milliseconds: 1000 ~/ _frameRate);
while (_isFrameAnimationPlaying && _currentFrameIndex < _totalFrames && mounted) {
while (_isPlaying && _currentFrameIndex < _totalFrames - 1 && mounted) {
await Future.delayed(frameDuration);
if (!mounted || !_isFrameAnimationPlaying) break;
if (!mounted || !_isPlaying) break;
setState(() {
_currentFrameIndex++;
});
// 2
final nextFrameIndex = _currentFrameIndex + 2;
if (nextFrameIndex < _totalFrames) {
precacheImage(_frameProviders[nextFrameIndex], context);
}
}
//
if (_currentFrameIndex >= _totalFrames && mounted && _isFrameAnimationPlaying) {
if (_currentFrameIndex >= _totalFrames - 1 && mounted && _isPlaying) {
_navigateToNextPage();
}
}
/// fallback
void _skipFallbackAnimation() {
_isFrameAnimationPlaying = false;
_navigateToNextPage();
}
///
void _onVideoStateChanged() {
if (_videoController == null) return;
//
if (_videoController!.value.position >= _videoController!.value.duration &&
_videoController!.value.duration > Duration.zero) {
_navigateToNextPage();
}
}
///
void _skipVideo() {
_videoController?.pause();
///
void _skipAnimation() {
_isPlaying = false;
_navigateToNextPage();
}
@ -249,24 +168,8 @@ class _SplashPageState extends ConsumerState<SplashPage> {
body: Stack(
fit: StackFit.expand,
children: [
//
if (_isVideoInitialized && _videoController != null)
SizedBox.expand(
child: FittedBox(
fit: BoxFit.cover,
child: SizedBox(
width: _videoController!.value.size.width,
height: _videoController!.value.size.height,
child: VideoPlayer(_videoController!),
),
),
)
else if (_videoFailed)
// fallback
_buildFallbackAnimationView()
else
// Logo
_buildLoadingView(),
//
_buildFrameAnimation(),
//
if (_showSkipButton)
@ -280,108 +183,13 @@ class _SplashPageState extends ConsumerState<SplashPage> {
);
}
///
Widget _buildLoadingView() {
return Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFFFF5E6),
Color(0xFFFFE4B5),
],
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo
Image.asset(
'assets/images/logo/app_icon.png',
width: 128,
height: 128,
),
const SizedBox(height: 24),
const Text(
'榴莲皇后',
style: TextStyle(
fontSize: 32,
fontFamily: 'Noto Sans SC',
fontWeight: FontWeight.w700,
height: 1.25,
letterSpacing: 1.6,
color: Color(0xFF4A3F35),
),
),
const SizedBox(height: 24),
//
const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
),
),
],
),
);
}
/// fallback 使
Widget _buildFallbackAnimationView() {
// : frame_001.png, frame_002.png, ... frame_053.png
final frameNumber = (_currentFrameIndex + 1).toString().padLeft(3, '0');
final framePath = 'assets/images/splash_frames/frame_$frameNumber.png';
///
Widget _buildFrameAnimation() {
return SizedBox.expand(
child: Image.asset(
framePath,
child: Image(
image: _frameProviders[_currentFrameIndex],
fit: BoxFit.cover,
gaplessPlayback: true, //
errorBuilder: (context, error, stackTrace) {
// fallback
debugPrint('[SplashPage] 帧加载失败: $framePath, 错误: $error');
return Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFFFF5E6),
Color(0xFFFFE4B5),
],
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/logo/app_icon.png',
width: 128,
height: 128,
),
const SizedBox(height: 24),
const Text(
'榴莲皇后',
style: TextStyle(
fontSize: 32,
fontFamily: 'Noto Sans SC',
fontWeight: FontWeight.w700,
height: 1.25,
letterSpacing: 1.6,
color: Color(0xFF4A3F35),
),
),
],
),
);
},
),
);
}
@ -389,7 +197,7 @@ class _SplashPageState extends ConsumerState<SplashPage> {
///
Widget _buildSkipButton() {
return GestureDetector(
onTap: _videoFailed ? _skipFallbackAnimation : _skipVideo,
onTap: _skipAnimation,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(

View File

@ -60,27 +60,27 @@ class _HomeShellPageState extends ConsumerState<HomeShellPage>
int _getCurrentIndex(BuildContext context) {
final location = GoRouterState.of(context).uri.path;
// Tab暂时隐藏: 0-, 1-, 2-
// : 0-, 1-, 2-, 3-
if (location.startsWith(RoutePaths.ranking)) return 0;
// if (location.startsWith(RoutePaths.mining)) return 1; //
if (location.startsWith(RoutePaths.trading)) return 1;
if (location.startsWith(RoutePaths.profile)) return 2;
if (location.startsWith(RoutePaths.mining)) return 1;
if (location.startsWith(RoutePaths.trading)) return 2;
if (location.startsWith(RoutePaths.profile)) return 3;
return 0;
}
void _onTabTapped(BuildContext context, int index) {
// Tab暂时隐藏: 0-, 1-, 2-
// : 0-, 1-, 2-, 3-
switch (index) {
case 0:
context.go(RoutePaths.ranking);
break;
// case 1: //
// context.go(RoutePaths.mining);
// break;
case 1:
context.go(RoutePaths.trading);
context.go(RoutePaths.mining);
break;
case 2:
context.go(RoutePaths.trading);
break;
case 3:
context.go(RoutePaths.profile);
break;
}

View File

@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
///
/// Tab
/// Tab暂时隐藏便
/// Tab
/// 使 LayoutBuilder
class BottomNavBar extends StatelessWidget {
final int currentIndex;
@ -42,21 +41,20 @@ class BottomNavBar extends StatelessWidget {
activeIcon: Icons.leaderboard,
label: '龙虎榜',
),
// Tab暂时隐藏便
// _buildNavItem(
// index: 1,
// icon: Icons.memory_outlined,
// activeIcon: Icons.memory,
// label: '矿机',
// ),
_buildNavItem(
index: 1, // index: 2
index: 1,
icon: Icons.monitor_heart_outlined,
activeIcon: Icons.monitor_heart,
label: '监控',
),
_buildNavItem(
index: 2,
icon: Icons.swap_horiz_outlined,
activeIcon: Icons.swap_horiz,
label: '兑换',
),
_buildNavItem(
index: 2, // index: 3
index: 3,
icon: Icons.person_outline,
activeIcon: Icons.person,
label: '',

View File

@ -11,7 +11,7 @@ enum MiningStatus {
paused, //
}
/// -
/// -
///
class MiningPage extends ConsumerStatefulWidget {
const MiningPage({super.key});
@ -130,8 +130,8 @@ class _MiningPageState extends ConsumerState<MiningPage> {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('矿机说明'),
content: const Text('矿机是您参与挖矿的核心工具。\n\n'
title: const Text('监控说明'),
content: const Text('监控是您查看挖矿状态的工具。\n\n'
'开启挖矿后,您将开始获得收益。\n\n'
'收益与您的算力和团队规模相关。'),
actions: [
@ -217,7 +217,7 @@ class _MiningPageState extends ConsumerState<MiningPage> {
//
const Expanded(
child: Text(
'矿机',
'监控',
style: TextStyle(
fontSize: 18,
fontFamily: 'Inter',

View File

@ -1500,32 +1500,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
//
...(_pendingRewards.map((item) => _buildPendingRewardItem(item))),
],
const SizedBox(height: 15),
//
GestureDetector(
onTap: _claimAllEarnings,
child: Container(
width: double.infinity,
height: 40,
decoration: BoxDecoration(
color: const Color(0xFFD4AF37),
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Text(
'领取全部',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
letterSpacing: 0.21,
color: Colors.white,
),
),
),
),
),
],
);
}

View File

@ -44,7 +44,6 @@ dependencies:
mobile_scanner: ^5.1.1
flutter_screenutil: ^5.9.0
city_pickers: ^1.3.0
video_player: ^2.8.6
# 工具
intl: ^0.20.2