Files
LunaTV/src/app/detail/page.tsx
2025-06-19 23:03:37 +08:00

272 lines
11 KiB
TypeScript
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.
'use client';
import Image from 'next/image';
import { useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import type { PlayRecord } from '@/lib/db.client';
import { generateStorageKey, getAllPlayRecords } from '@/lib/db.client';
import { VideoDetail } from '@/lib/video';
import PageLayout from '@/components/PageLayout';
export default function DetailPage() {
const searchParams = useSearchParams();
const [detail, setDetail] = useState<VideoDetail | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [playRecord, setPlayRecord] = useState<PlayRecord | null>(null);
// 格式化剩余时间(如 1h 50m
const formatDuration = (seconds: number) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const parts: string[] = [];
if (h) parts.push(`${h}h`);
if (m) parts.push(`${m}m`);
if (parts.length === 0) parts.push('0m');
return parts.join(' ');
};
useEffect(() => {
const source = searchParams.get('source');
const id = searchParams.get('id');
if (!source || !id) {
setError('缺少必要参数');
setLoading(false);
return;
}
const fetchData = async () => {
try {
const response = await fetch(`/api/detail?source=${source}&id=${id}`);
if (!response.ok) {
throw new Error('获取详情失败');
}
const data = await response.json();
setDetail(data);
// 获取播放记录
const allRecords = await getAllPlayRecords();
const key = generateStorageKey(source, id);
setPlayRecord(allRecords[key] || null);
} catch (err) {
setError(err instanceof Error ? err.message : '获取详情失败');
} finally {
setLoading(false);
}
};
fetchData();
}, [searchParams]);
return (
<PageLayout activePath='/detail'>
<div className='px-10 py-8 overflow-visible'>
{/* 顶部返回按钮已移入右侧信息容器 */}
{loading ? (
<div className='flex items-center justify-center min-h-[60vh]'>
<div className='animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500'></div>
</div>
) : error ? (
<div className='flex items-center justify-center min-h-[60vh]'>
<div className='text-red-500 text-center'>
<div className='text-lg font-semibold mb-2'></div>
<div className='text-sm'>{error}</div>
</div>
</div>
) : !detail ? (
<div className='flex items-center justify-center min-h-[60vh]'>
<div className='text-gray-500 text-center'>
<div className='text-lg font-semibold mb-2'></div>
</div>
</div>
) : (
<div className='max-w-[95%] mx-auto'>
{/* 主信息区:左图右文 */}
<div className='relative flex flex-col md:flex-row gap-8 mb-8 bg-transparent rounded-xl p-6 md:items-start'>
{/* 返回按钮放置在主信息区左上角 */}
<button
onClick={() => {
const from = searchParams.get('from');
if (from === 'search') {
window.history.back();
} else {
window.location.href = '/';
}
}}
className='absolute top-0 left-0 -translate-x-[180%] -translate-y-1/2 p-2 rounded transition-colors'
>
<svg
className='h-5 w-5 text-gray-500 hover:text-green-600 transition-colors'
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M15 19l-7-7 7-7'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
</button>
{/* 封面 */}
<div className='flex-shrink-0 w-full md:w-72'>
<Image
src={detail.videoInfo.cover || '/images/placeholder.png'}
alt={detail.videoInfo.title}
width={288}
height={432}
className='w-full rounded-xl object-cover'
style={{ aspectRatio: '2/3' }}
priority
/>
</div>
{/* 右侧信息 */}
<div
className='flex-1 flex flex-col min-h-0'
style={{ height: '430px' }}
>
<h1 className='text-3xl font-bold mb-2 tracking-wide flex items-center flex-shrink-0'>
{detail.videoInfo.title}
</h1>
<div className='flex flex-wrap items-center gap-3 text-base mb-4 opacity-80 flex-shrink-0'>
{detail.videoInfo.remarks && (
<span className='text-green-600 font-semibold'>
{detail.videoInfo.remarks}
</span>
)}
{detail.videoInfo.year && (
<span>{detail.videoInfo.year}</span>
)}
{detail.videoInfo.source_name && (
<span>{detail.videoInfo.source_name}</span>
)}
{detail.videoInfo.type && (
<span>{detail.videoInfo.type}</span>
)}
</div>
{/* 按钮区域 */}
<div className='flex items-center gap-4 mb-4 flex-shrink-0'>
{playRecord ? (
<>
{/* 恢复播放 */}
<a
href={`/play?source=${searchParams.get(
'source'
)}&id=${searchParams.get('id')}`}
className='flex items-center justify-center gap-2 px-6 py-2 bg-green-500 hover:bg-green-600 rounded-lg transition-colors text-white'
>
<div className='w-0 h-0 border-t-[6px] border-t-transparent border-l-[10px] border-l-white border-b-[6px] border-b-transparent'></div>
<span></span>
</a>
{/* 从头开始 */}
<a
href={`/play?source=${searchParams.get(
'source'
)}&id=${searchParams.get('id')}&index=1&position=0`}
className='flex items-center justify-center gap-2 px-6 py-2 bg-gray-500 hover:bg-gray-600 rounded-lg transition-colors text-white'
>
<div className='w-0 h-0 border-t-[6px] border-t-transparent border-l-[10px] border-l-white border-b-[6px] border-b-transparent'></div>
<span></span>
</a>
</>
) : (
<>
{/* 播放 */}
<a
href={`/play?source=${searchParams.get(
'source'
)}&id=${searchParams.get('id')}&index=1&position=0`}
className='flex items-center justify-center gap-2 px-6 py-2 bg-green-500 hover:bg-green-600 rounded-lg transition-colors text-white'
>
<div className='w-0 h-0 border-t-[6px] border-t-transparent border-l-[10px] border-l-white border-b-[6px] border-b-transparent'></div>
<span></span>
</a>
</>
)}
{/* 爱心按钮 */}
<button className='flex items-center justify-center w-10 h-10 bg-pink-400 hover:bg-pink-500 rounded-full transition-colors'>
<svg
xmlns='http://www.w3.org/2000/svg'
className='h-5 w-5 text-white'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z'
/>
</svg>
</button>
</div>
{/* 播放记录进度条 */}
{playRecord && (
<div className='mb-4 flex items-center gap-3 w-full max-w-sm'>
{/* 进度条 */}
<div className='flex-1 h-1 bg-gray-600 rounded-sm overflow-hidden'>
<div
className='h-full bg-green-500'
style={{
width: `${
(playRecord.play_time / playRecord.total_time) * 100
}%`,
}}
></div>
</div>
{/* 剩余时间 */}
<span className='text-gray-600/60 text-xs whitespace-nowrap'>
{' '}
{formatDuration(
playRecord.total_time - playRecord.play_time
)}
</span>
</div>
)}
{detail.videoInfo.desc && (
<div
className='mt-0 text-base leading-relaxed opacity-90 overflow-y-auto pr-2 flex-1 min-h-0 scrollbar-hide'
style={{ whiteSpace: 'pre-line' }}
>
{detail.videoInfo.desc}
</div>
)}
</div>
</div>
{/* 选集按钮区 */}
{detail.episodes.length > 0 && (
<div className='mt-8 bg-transparent rounded-xl p-6'>
<div className='flex items-center gap-2 mb-4'>
<div className='text-xl font-semibold'></div>
<div className='text-gray-400 ml-2'>
{detail.episodes.length}
</div>
</div>
<div className='flex flex-wrap gap-4'>
{detail.episodes.map((episode, idx) => (
<a
key={idx}
href={`/play?source=${searchParams.get(
'source'
)}&id=${searchParams.get('id')}&index=${idx + 1}`}
className='bg-gray-500/80 hover:bg-green-500 text-white px-5 py-2 rounded-lg transition-colors text-base font-medium w-24 text-center'
>
{idx + 1}
</a>
))}
</div>
</div>
)}
</div>
)}
</div>
</PageLayout>
);
}