'use client'; import { X, Calendar, Star, Clock, Tag, Users, Globe, Film } from 'lucide-react'; import Image from 'next/image'; import React, { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; interface DetailPanelProps { isOpen: boolean; onClose: () => void; title: string; poster?: string; doubanId?: number; bangumiId?: number; isBangumi?: boolean; tmdbId?: number; type?: 'movie' | 'tv'; seasonNumber?: number; cmsData?: { desc?: string; episodes?: string[]; episodes_titles?: string[]; }; // 用于调用 source-detail API sourceId?: string; source?: string; } interface DetailData { title: string; originalTitle?: string; year?: string; poster?: string; rating?: { value: number; count: number; }; intro?: string; genres?: string[]; directors?: Array<{ name: string }>; actors?: Array<{ name: string }>; countries?: string[]; languages?: string[]; duration?: string; episodesCount?: number; releaseDate?: string; status?: string; tagline?: string; seasons?: number; overview?: string; } const DetailPanel: React.FC = ({ isOpen, onClose, title, poster, doubanId, bangumiId, isBangumi, tmdbId, type = 'movie', seasonNumber, cmsData, sourceId, source, }) => { const [isVisible, setIsVisible] = useState(false); const [isAnimating, setIsAnimating] = useState(false); const [mounted, setMounted] = useState(false); const [detailData, setDetailData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // 确保组件在客户端挂载后才渲染 Portal useEffect(() => { setMounted(true); }, []); // 控制动画状态 useEffect(() => { let animationId: number; let timer: NodeJS.Timeout; if (isOpen) { setIsVisible(true); animationId = requestAnimationFrame(() => { animationId = requestAnimationFrame(() => { setIsAnimating(true); }); }); } else { setIsAnimating(false); timer = setTimeout(() => { setIsVisible(false); }, 200); } return () => { if (animationId) { cancelAnimationFrame(animationId); } if (timer) { clearTimeout(timer); } }; }, [isOpen]); // 阻止背景滚动 useEffect(() => { if (isVisible) { // 保存当前滚动位置 const scrollY = window.scrollY; const scrollX = window.scrollX; const body = document.body; const html = document.documentElement; // 获取滚动条宽度 const scrollBarWidth = window.innerWidth - html.clientWidth; // 保存原始样式 const originalBodyStyle = { position: body.style.position, top: body.style.top, left: body.style.left, right: body.style.right, width: body.style.width, paddingRight: body.style.paddingRight, overflow: body.style.overflow, }; // 设置body样式来阻止滚动,但保持原位置 body.style.position = 'fixed'; body.style.top = `-${scrollY}px`; body.style.left = `-${scrollX}px`; body.style.right = '0'; body.style.width = '100%'; body.style.overflow = 'hidden'; body.style.paddingRight = `${scrollBarWidth}px`; return () => { // 恢复所有原始样式 body.style.position = originalBodyStyle.position; body.style.top = originalBodyStyle.top; body.style.left = originalBodyStyle.left; body.style.right = originalBodyStyle.right; body.style.width = originalBodyStyle.width; body.style.paddingRight = originalBodyStyle.paddingRight; body.style.overflow = originalBodyStyle.overflow; // 使用 requestAnimationFrame 确保样式恢复后再滚动 requestAnimationFrame(() => { window.scrollTo(scrollX, scrollY); }); }; } }, [isVisible]); // ESC键关闭 useEffect(() => { const handleEsc = (e: KeyboardEvent) => { if (e.key === 'Escape') { onClose(); } }; if (isVisible) { document.addEventListener('keydown', handleEsc); return () => document.removeEventListener('keydown', handleEsc); } }, [isVisible, onClose]); // 获取详情数据 useEffect(() => { if (!isOpen) { return; } const fetchDetail = async () => { setLoading(true); setError(null); try { // 优先使用苹果CMS数据(短剧等) // 如果 cmsData 存在但 desc 为空,尝试通过 source-detail API 获取 if (cmsData) { if (cmsData.desc) { // 有 desc,直接使用 setDetailData({ title: title, intro: cmsData.desc, episodesCount: cmsData.episodes?.length, poster: poster, }); setLoading(false); return; } // cmsData 存在但 desc 为空,尝试通过 API 获取详情 if (sourceId && source) { try { const response = await fetch( `/api/source-detail?id=${encodeURIComponent(sourceId)}&source=${encodeURIComponent(source)}&title=${encodeURIComponent(title)}` ); if (response.ok) { const data = await response.json(); setDetailData({ title: data.title || title, intro: data.desc || '', episodesCount: data.episodes?.length || cmsData.episodes?.length, poster: data.poster || poster, year: data.year, }); setLoading(false); return; } } catch (err) { console.error('获取source-detail失败:', err); // 继续执行后续逻辑 } } } // 优先使用 Bangumi ID(因为 isBangumi 为 true 时,doubanId 实际上是 bangumiId) if (bangumiId || (isBangumi && doubanId)) { const actualBangumiId = bangumiId || doubanId; const response = await fetch(`https://api.bgm.tv/v0/subjects/${actualBangumiId}`); if (!response.ok) { throw new Error('获取Bangumi详情失败'); } const data = await response.json(); setDetailData({ title: data.name_cn || data.name, originalTitle: data.name, year: data.date ? data.date.substring(0, 4) : undefined, poster: data.images?.large || poster, rating: data.rating ? { value: data.rating.score, count: data.rating.total, } : undefined, intro: data.summary, genres: data.tags?.map((tag: any) => tag.name).slice(0, 5), episodesCount: data.eps, releaseDate: data.date, }); return; } // 使用豆瓣ID if (doubanId && !isBangumi) { const response = await fetch(`/api/douban/detail?id=${doubanId}`); if (!response.ok) { throw new Error('获取豆瓣详情失败'); } const data = await response.json(); setDetailData({ title: data.title, originalTitle: data.original_title, year: data.year, poster: data.pic?.large || data.pic?.normal || poster, rating: data.rating ? { value: data.rating.value, count: data.rating.count, } : undefined, intro: data.intro, genres: data.genres, directors: data.directors, actors: data.actors, countries: data.countries, languages: data.languages, duration: data.durations?.[0], episodesCount: data.episodes_count, }); return; } // 使用 TMDB 搜索 if (title) { // 移除季度信息进行搜索 let searchTitle = title; let extractedSeasonNumber = seasonNumber; // 匹配各种季度格式: 第一季、第1季、第一部、Season 1、S1等 const seasonPatterns = [ /第([一二三四五六七八九十\d]+)[季部]/, /Season\s*(\d+)/i, /S(\d+)/i, ]; for (const pattern of seasonPatterns) { const match = title.match(pattern); if (match) { searchTitle = title.replace(pattern, '').trim(); // 如果没有传入seasonNumber,尝试从标题中提取 if (!extractedSeasonNumber) { const seasonStr = match[1]; // 中文数字转数字 const chineseNumbers: Record = { '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9, '十': 10, }; extractedSeasonNumber = chineseNumbers[seasonStr] || parseInt(seasonStr) || undefined; } break; } } const searchResponse = await fetch( `/api/tmdb/search?query=${encodeURIComponent(searchTitle)}` ); if (!searchResponse.ok) { throw new Error('搜索失败'); } const searchData = await searchResponse.json(); if (searchData.results && searchData.results.length > 0) { const result = searchData.results[0]; const detailId = result.id; const mediaType = result.media_type || type; // 获取详情 const detailResponse = await fetch(`/api/tmdb/detail?id=${detailId}&type=${mediaType}`); if (!detailResponse.ok) { throw new Error('获取TMDB详情失败'); } const detailResult = await detailResponse.json(); // 如果有季度信息,尝试获取季度详情 let seasonData = null; if (extractedSeasonNumber && mediaType === 'tv') { try { const seasonResponse = await fetch( `/api/tmdb/seasons?id=${detailId}&season=${extractedSeasonNumber}` ); if (seasonResponse.ok) { seasonData = await seasonResponse.json(); } } catch (err) { console.error('获取季度信息失败', err); } } setDetailData({ title: mediaType === 'movie' ? detailResult.title : detailResult.name, originalTitle: mediaType === 'movie' ? detailResult.original_title : detailResult.original_name, year: mediaType === 'movie' ? detailResult.release_date?.substring(0, 4) : detailResult.first_air_date?.substring(0, 4), poster: detailResult.poster_path ? `https://image.tmdb.org/t/p/w500${detailResult.poster_path}` : poster, rating: detailResult.vote_average ? { value: detailResult.vote_average, count: detailResult.vote_count, } : undefined, intro: seasonData?.overview || detailResult.overview, genres: detailResult.genres?.map((g: any) => g.name), countries: detailResult.production_countries?.map((c: any) => c.name), languages: detailResult.spoken_languages?.map((l: any) => l.name), duration: detailResult.runtime ? `${detailResult.runtime}分钟` : undefined, episodesCount: seasonData?.episodes?.length || detailResult.number_of_episodes, releaseDate: mediaType === 'movie' ? detailResult.release_date : detailResult.first_air_date, status: detailResult.status, tagline: detailResult.tagline, seasons: detailResult.number_of_seasons, overview: detailResult.overview, }); return; } throw new Error('未找到相关内容'); } throw new Error('缺少必要的查询参数'); } catch (err) { console.error('获取详情失败:', err); setError(err instanceof Error ? err.message : '获取详情失败'); } finally { setLoading(false); } }; fetchDetail(); }, [isOpen, doubanId, bangumiId, isBangumi, tmdbId, title, type, seasonNumber, poster, cmsData, sourceId, source]); if (!isVisible || !mounted) return null; const content = (
{/* 背景遮罩 */}
{/* 详情面板 */}
{/* 头部 */}

详情

{/* 内容区域 */}
{loading && (
)} {error && (

{error}

)} {!loading && !error && detailData && (
{/* 海报和基本信息 */}
{detailData.poster && (
{detailData.title}
)}

{detailData.title}

{detailData.originalTitle && detailData.originalTitle !== detailData.title && (

{detailData.originalTitle}

)} {/* 评分 */} {detailData.rating && (
{detailData.rating.value.toFixed(1)} {detailData.rating.count > 0 && ( ({detailData.rating.count} 评价) )}
)} {/* 类型标签 */} {detailData.genres && detailData.genres.length > 0 && (
{detailData.genres.map((genre, index) => ( {genre} ))}
)} {/* 年份和时长 */}
{detailData.year && (
{detailData.year}
)} {detailData.duration && (
{detailData.duration}
)} {detailData.episodesCount && (
{detailData.episodesCount} 集
)}
{/* 简介 */} {(detailData.intro || detailData.overview) && (

简介

{detailData.intro || detailData.overview}

)} {/* 导演和演员 */} {detailData.directors && detailData.directors.length > 0 && (

导演

{detailData.directors.map((d) => d.name).join(', ')}

)} {detailData.actors && detailData.actors.length > 0 && (

演员

{detailData.actors.slice(0, 10).map((a) => a.name).join(', ')}

)} {/* 制作信息 */}
{detailData.countries && detailData.countries.length > 0 && (

国家/地区

{detailData.countries.join(', ')}

)} {detailData.languages && detailData.languages.length > 0 && (

语言

{detailData.languages.join(', ')}

)} {detailData.releaseDate && (

上映日期

{detailData.releaseDate}

)} {detailData.status && (

状态

{detailData.status}

)}
)}
); return createPortal(content, document.body); }; export default DetailPanel;