mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-22 10:34:42 +08:00
feat: loading screen
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user