详情面板增加演员

This commit is contained in:
mtvpls
2026-02-17 01:22:15 +08:00
parent 29a14bf8fa
commit bdb43450a6
3 changed files with 274 additions and 5 deletions
+161 -5
View File
@@ -42,8 +42,8 @@ interface DetailData {
};
intro?: string;
genres?: string[];
directors?: Array<{ name: string }>;
actors?: Array<{ name: string }>;
directors?: Array<{ name: string; profile_path?: string }>;
actors?: Array<{ name: string; character?: string; profile_path?: string }>;
countries?: string[];
languages?: string[];
duration?: string;
@@ -108,6 +108,11 @@ const DetailPanel: React.FC<DetailPanelProps> = ({
const [startX, setStartX] = useState(0);
const [scrollLeft, setScrollLeft] = useState(0);
const episodesScrollRef = React.useRef<HTMLDivElement>(null);
const actorsScrollRef = React.useRef<HTMLDivElement>(null);
const [isActorsDragging, setIsActorsDragging] = useState(false);
const [isActorsMouseDown, setIsActorsMouseDown] = useState(false);
const [actorsStartX, setActorsStartX] = useState(0);
const [actorsScrollLeft, setActorsScrollLeft] = useState(0);
// 图片点击处理
const handleImageClick = (imageUrl: string) => {
@@ -646,6 +651,51 @@ const DetailPanel: React.FC<DetailPanelProps> = ({
fetchSeasonData();
}, [detailData?.tmdbId, detailData?.mediaType, detailData?.seasonNumber, seasonsLoaded]);
// 异步获取演职人员信息(仅TMDB)
useEffect(() => {
if (!detailData?.tmdbId || !detailData?.mediaType || currentSource !== 'tmdb') {
return;
}
// 如果已经有演员信息,不重复获取
if (detailData.actors && detailData.actors.length > 0) {
return;
}
const fetchCredits = async () => {
try {
const creditsResponse = await fetch(
`/api/tmdb/credits?id=${detailData.tmdbId}&type=${detailData.mediaType}`
);
if (!creditsResponse.ok) return;
const creditsData = await creditsResponse.json();
// 更新演员和导演信息
setDetailData(prev => prev ? {
...prev,
directors: creditsData.crew
?.filter((person: any) => person.job === 'Director')
.slice(0, 5)
.map((person: any) => ({
name: person.name,
profile_path: person.profile_path,
})) || prev.directors,
actors: creditsData.cast
?.slice(0, 15)
.map((person: any) => ({
name: person.name,
character: person.character,
profile_path: person.profile_path,
})) || prev.actors,
} : null);
} catch (err) {
console.error('获取演职人员信息失败:', err);
}
};
fetchCredits();
}, [detailData?.tmdbId, detailData?.mediaType, currentSource, detailData?.actors]);
// 切换季度时获取集数
const handleSeasonChange = async (seasonNumber: number) => {
if (!detailData?.tmdbId || selectedSeason === seasonNumber) return;
@@ -734,6 +784,54 @@ const DetailPanel: React.FC<DetailPanelProps> = ({
}
};
// 演员列表拖动滚动处理函数
const handleActorsMouseDown = (e: React.MouseEvent) => {
if (!actorsScrollRef.current) return;
setIsActorsMouseDown(true);
setActorsStartX(e.pageX - actorsScrollRef.current.offsetLeft);
setActorsScrollLeft(actorsScrollRef.current.scrollLeft);
};
const handleActorsMouseMove = (e: React.MouseEvent) => {
if (!isActorsMouseDown || !actorsScrollRef.current) return;
const x = e.pageX - actorsScrollRef.current.offsetLeft;
const distance = Math.abs(x - actorsStartX);
// 只有移动超过5px才进入拖动模式
if (distance > 5 && !isActorsDragging) {
setIsActorsDragging(true);
actorsScrollRef.current.style.cursor = 'grabbing';
actorsScrollRef.current.style.userSelect = 'none';
}
if (isActorsDragging) {
e.preventDefault();
const walk = (x - actorsStartX) * 2; // 滚动速度倍数
actorsScrollRef.current.scrollLeft = actorsScrollLeft - walk;
}
};
const handleActorsMouseUp = () => {
setIsActorsMouseDown(false);
setIsActorsDragging(false);
if (actorsScrollRef.current) {
actorsScrollRef.current.style.cursor = 'grab';
actorsScrollRef.current.style.userSelect = 'auto';
}
};
const handleActorsMouseLeave = () => {
if (isActorsMouseDown || isActorsDragging) {
setIsActorsMouseDown(false);
setIsActorsDragging(false);
if (actorsScrollRef.current) {
actorsScrollRef.current.style.cursor = 'grab';
actorsScrollRef.current.style.userSelect = 'auto';
}
}
};
if (!isVisible || !mounted) return null;
const content = (
@@ -929,9 +1027,67 @@ const DetailPanel: React.FC<DetailPanelProps> = ({
<Users size={16} />
</h4>
<p className="text-gray-700 dark:text-gray-300">
{detailData.actors.slice(0, 10).map((a) => a.name).join(', ')}
</p>
{currentSource === 'tmdb' ? (
<div
ref={actorsScrollRef}
onMouseDown={handleActorsMouseDown}
onMouseMove={handleActorsMouseMove}
onMouseUp={handleActorsMouseUp}
onMouseLeave={handleActorsMouseLeave}
className="overflow-x-auto -mx-6 px-6 cursor-grab active:cursor-grabbing"
style={{
scrollbarWidth: 'thin',
scrollBehavior: isActorsDragging ? 'auto' : 'smooth'
}}
>
<div className="flex gap-4 pb-2">
{detailData.actors.map((actor, index) => (
<div
key={index}
className="flex flex-col items-center flex-shrink-0"
style={{ pointerEvents: isActorsDragging ? 'none' : 'auto' }}
>
{actor.profile_path ? (
<div
className="relative w-20 h-20 rounded-full overflow-hidden bg-gray-200 dark:bg-gray-700 mb-2 cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => handleImageClick(processImageUrl(getTMDBImageUrl(actor.profile_path, 'w185')))}
>
<Image
src={processImageUrl(getTMDBImageUrl(actor.profile_path, 'w185'))}
alt={actor.name}
fill
className="object-cover"
draggable={false}
/>
</div>
) : (
<div className="w-20 h-20 rounded-full bg-gray-200 dark:bg-gray-700 mb-2 flex items-center justify-center">
<Users size={28} className="text-gray-400" />
</div>
)}
<a
href={`https://baike.baidu.com/item/${encodeURIComponent(actor.name)}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs font-medium text-gray-900 dark:text-gray-100 text-center w-20 line-clamp-2 hover:text-green-600 dark:hover:text-green-400 transition-colors cursor-pointer"
onClick={(e) => e.stopPropagation()}
>
{actor.name}
</a>
{actor.character && (
<p className="text-xs text-gray-500 dark:text-gray-400 text-center w-20 line-clamp-2">
{actor.character}
</p>
)}
</div>
))}
</div>
</div>
) : (
<p className="text-gray-700 dark:text-gray-300">
{detailData.actors.slice(0, 10).map((a) => a.name).join(', ')}
</p>
)}
</div>
)}