feat: loading screen

This commit is contained in:
shinya
2025-07-08 23:50:20 +08:00
parent aa70f4c9f6
commit 7af32201ab

View File

@@ -39,6 +39,10 @@ function PlayPageClient() {
// 状态变量State
// -----------------------------------------------------------------------------
const [loading, setLoading] = useState(true);
const [loadingStage, setLoadingStage] = useState<
'searching' | 'fetching' | 'ready'
>('searching');
const [loadingMessage, setLoadingMessage] = useState('正在搜索播放源...');
const [error, setError] = useState<string | null>(null);
const [detail, setDetail] = useState<VideoDetail | null>(null);
@@ -55,9 +59,6 @@ function PlayPageClient() {
});
// 视频基本信息
const [videoDoubanId, setVideoDoubanId] = useState(
searchParams.get('douban_id') || ''
);
const [videoTitle, setVideoTitle] = useState(searchParams.get('title') || '');
const [videoYear, setVideoYear] = useState(searchParams.get('year') || '');
const [videoCover, setVideoCover] = useState('');
@@ -245,8 +246,10 @@ function PlayPageClient() {
if (!currentSource && !currentId) {
// 只包含视频标题,搜索视频
setLoading(true);
setLoadingStage('searching');
setLoadingMessage('🔍 正在搜索播放源...');
const searchResults = await handleSearchSources(videoTitle);
console.log('searchResults', searchResults);
if (searchResults.length == 0) {
setError('未找到匹配结果');
setLoading(false);
@@ -255,7 +258,6 @@ function PlayPageClient() {
setCurrentSource(searchResults[0].source);
setCurrentId(searchResults[0].id);
setVideoYear(searchResults[0].year);
setVideoDoubanId(''); // 清空豆瓣ID
// 替换URL参数
const newUrl = new URL(window.location.href);
newUrl.searchParams.set('source', searchResults[0].source);
@@ -268,6 +270,9 @@ function PlayPageClient() {
const fetchDetail = async () => {
try {
setLoadingStage('fetching');
setLoadingMessage('🎬 正在获取视频详情...');
const detailData = await fetchVideoDetail({
source: currentSource,
id: currentId,
@@ -282,7 +287,6 @@ function PlayPageClient() {
// 确保集数索引在有效范围内
if (currentEpisodeIndex >= detailData.episodes.length) {
console.log('currentEpisodeIndex', currentEpisodeIndex);
setCurrentEpisodeIndex(0);
}
@@ -293,9 +297,17 @@ function PlayPageClient() {
newUrl.searchParams.delete('position');
window.history.replaceState({}, '', newUrl.toString());
}
setLoadingStage('ready');
setLoadingMessage('✨ 准备就绪,即将开始播放...');
// 短暂延迟让用户看到完成状态
setTimeout(() => {
setLoading(false);
}, 1000);
} catch (err) {
console.error('获取视频详情失败:', err);
} finally {
setError(err instanceof Error ? err.message : '获取视频详情失败');
setLoading(false);
}
};
@@ -417,16 +429,12 @@ function PlayPageClient() {
? (detail.episodes.length === 1 &&
result.episodes.length === 1) ||
(detail.episodes.length > 1 && result.episodes.length > 1)
: true) &&
(videoDoubanId && result.douban_id
? result.douban_id.toString() === videoDoubanId
: true)
);
if (exactMatchs.length > 0) {
processedResults.push(...exactMatchs);
}
});
console.log('processedResults', processedResults);
setAvailableSources(processedResults);
return processedResults;
@@ -739,17 +747,27 @@ function PlayPageClient() {
// 切换收藏
const handleToggleFavorite = async () => {
if (!videoTitleRef.current || !detailRef.current || !currentSourceRef.current || !currentIdRef.current) return;
if (
!videoTitleRef.current ||
!detailRef.current ||
!currentSourceRef.current ||
!currentIdRef.current
)
return;
try {
const newState = await toggleFavorite(currentSourceRef.current, currentIdRef.current, {
title: videoTitleRef.current,
source_name: detailRef.current?.source_name || '',
year: detailRef.current?.year || '',
cover: detailRef.current?.poster || '',
total_episodes: detailRef.current?.episodes.length || 1,
save_time: Date.now(),
});
const newState = await toggleFavorite(
currentSourceRef.current,
currentIdRef.current,
{
title: videoTitleRef.current,
source_name: detailRef.current?.source_name || '',
year: detailRef.current?.year || '',
cover: detailRef.current?.poster || '',
total_episodes: detailRef.current?.episodes.length || 1,
save_time: Date.now(),
}
);
setFavorited(newState);
} catch (err) {
console.error('切换收藏失败:', err);
@@ -798,7 +816,6 @@ function PlayPageClient() {
}`;
artPlayerRef.current.poster = videoCover;
if (artPlayerRef.current?.video) {
console.log('attachVideoEventListeners');
attachVideoEventListeners(
artPlayerRef.current.video as HTMLVideoElement
);
@@ -1024,7 +1041,6 @@ function PlayPageClient() {
});
if (artPlayerRef.current?.video) {
console.log('attachVideoEventListeners');
attachVideoEventListeners(
artPlayerRef.current.video as HTMLVideoElement
);
@@ -1037,7 +1053,7 @@ function PlayPageClient() {
console.error('创建播放器失败:', err);
setError('播放器初始化失败');
}
}, [Artplayer, Hls, videoUrl]);
}, [Artplayer, Hls, videoUrl, loading]);
// ---------------------------------------------------------------------------
// 视频元素事件监听
@@ -1164,12 +1180,91 @@ function PlayPageClient() {
if (loading) {
return (
<PageLayout activePath='/play'>
<div className='flex items-center justify-center h-screen'>
<div className='text-center'>
<div className='animate-spin rounded-full h-32 w-32 border-b-2 border-green-500 mx-auto mb-4'></div>
<p className='text-lg text-gray-600 dark:text-gray-300'>
...
</p>
<div className='flex items-center justify-center min-h-screen bg-gradient-to-br from-green-50 via-white to-emerald-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900'>
<div className='text-center max-w-md mx-auto px-6'>
{/* 动画影院图标 */}
<div className='relative mb-8'>
<div className='relative mx-auto w-24 h-24 bg-gradient-to-r from-green-500 to-emerald-600 rounded-2xl shadow-2xl flex items-center justify-center transform hover:scale-105 transition-transform duration-300'>
<div className='text-white text-4xl'>
{loadingStage === 'searching' && '🔍'}
{loadingStage === 'fetching' && '🎬'}
{loadingStage === 'ready' && '✨'}
</div>
{/* 旋转光环 */}
<div className='absolute -inset-2 bg-gradient-to-r from-green-500 to-emerald-600 rounded-2xl opacity-20 animate-spin'></div>
</div>
{/* 浮动粒子效果 */}
<div className='absolute top-0 left-0 w-full h-full pointer-events-none'>
<div className='absolute top-2 left-2 w-2 h-2 bg-green-400 rounded-full animate-bounce'></div>
<div
className='absolute top-4 right-4 w-1.5 h-1.5 bg-emerald-400 rounded-full animate-bounce'
style={{ animationDelay: '0.5s' }}
></div>
<div
className='absolute bottom-3 left-6 w-1 h-1 bg-lime-400 rounded-full animate-bounce'
style={{ animationDelay: '1s' }}
></div>
</div>
</div>
{/* 进度指示器 */}
<div className='mb-6 w-80 mx-auto'>
<div className='flex justify-center space-x-2 mb-4'>
<div
className={`w-3 h-3 rounded-full transition-all duration-500 ${
loadingStage === 'searching'
? 'bg-green-500 scale-125'
: loadingStage === 'fetching' || loadingStage === 'ready'
? 'bg-green-500'
: 'bg-gray-300'
}`}
></div>
<div
className={`w-3 h-3 rounded-full transition-all duration-500 ${
loadingStage === 'fetching'
? 'bg-green-500 scale-125'
: loadingStage === 'ready'
? 'bg-green-500'
: 'bg-gray-300'
}`}
></div>
<div
className={`w-3 h-3 rounded-full transition-all duration-500 ${
loadingStage === 'ready'
? 'bg-green-500 scale-125'
: 'bg-gray-300'
}`}
></div>
</div>
{/* 进度条 */}
<div className='w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden'>
<div
className='h-full bg-gradient-to-r from-green-500 to-emerald-600 rounded-full transition-all duration-1000 ease-out'
style={{
width:
loadingStage === 'searching'
? '33%'
: loadingStage === 'fetching'
? '66%'
: '100%',
}}
></div>
</div>
</div>
{/* 加载消息 */}
<div className='space-y-2'>
<p className='text-xl font-semibold text-gray-800 dark:text-gray-200 animate-pulse'>
{loadingMessage}
</p>
<p className='text-sm text-gray-500 dark:text-gray-400'>
{loadingStage === 'searching' && '正在为您寻找可用播放源...'}
{loadingStage === 'fetching' && '正在解析视频信息和播放列表...'}
{loadingStage === 'ready' && '一切准备就绪,马上就能观看啦!'}
</p>
</div>
</div>
</div>
</PageLayout>
@@ -1179,24 +1274,67 @@ function PlayPageClient() {
if (error) {
return (
<PageLayout activePath='/play'>
<div className='flex items-center justify-center h-screen'>
<div className='text-center'>
<div className='text-red-500 text-6xl mb-4'></div>
<p className='text-lg text-red-600 dark:text-red-400 mb-4'>
{error}
</p>
<button
onClick={() =>
videoTitle
? (window.location.href = `/search?q=${encodeURIComponent(
videoTitle
)}`)
: window.history.back()
}
className='px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors'
>
{videoTitle ? '返回搜索' : '返回'}
</button>
<div className='flex items-center justify-center min-h-screen bg-gradient-to-br from-red-50 via-white to-orange-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900'>
<div className='text-center max-w-md mx-auto px-6'>
{/* 错误图标 */}
<div className='relative mb-8'>
<div className='relative mx-auto w-24 h-24 bg-gradient-to-r from-red-500 to-orange-500 rounded-2xl shadow-2xl flex items-center justify-center transform hover:scale-105 transition-transform duration-300'>
<div className='text-white text-4xl'>😵</div>
{/* 脉冲效果 */}
<div className='absolute -inset-2 bg-gradient-to-r from-red-500 to-orange-500 rounded-2xl opacity-20 animate-pulse'></div>
</div>
{/* 浮动错误粒子 */}
<div className='absolute top-0 left-0 w-full h-full pointer-events-none'>
<div className='absolute top-2 left-2 w-2 h-2 bg-red-400 rounded-full animate-bounce'></div>
<div
className='absolute top-4 right-4 w-1.5 h-1.5 bg-orange-400 rounded-full animate-bounce'
style={{ animationDelay: '0.5s' }}
></div>
<div
className='absolute bottom-3 left-6 w-1 h-1 bg-yellow-400 rounded-full animate-bounce'
style={{ animationDelay: '1s' }}
></div>
</div>
</div>
{/* 错误信息 */}
<div className='space-y-4 mb-8'>
<h2 className='text-2xl font-bold text-gray-800 dark:text-gray-200'>
</h2>
<div className='bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4'>
<p className='text-red-600 dark:text-red-400 font-medium'>
{error}
</p>
</div>
<p className='text-sm text-gray-500 dark:text-gray-400'>
</p>
</div>
{/* 操作按钮 */}
<div className='space-y-3'>
<button
onClick={() =>
videoTitle
? (window.location.href = `/search?q=${encodeURIComponent(
videoTitle
)}`)
: window.history.back()
}
className='w-full px-6 py-3 bg-gradient-to-r from-green-500 to-emerald-600 text-white rounded-xl font-medium hover:from-green-600 hover:to-emerald-700 transform hover:scale-105 transition-all duration-200 shadow-lg hover:shadow-xl'
>
{videoTitle ? '🔍 返回搜索' : '← 返回上页'}
</button>
<button
onClick={() => window.location.reload()}
className='w-full px-6 py-3 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-xl font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors duration-200'
>
🔄
</button>
</div>
</div>
</div>
</PageLayout>