From bdb43450a6041134cc99f8207681968b1ae608f2 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Tue, 17 Feb 2026 01:22:15 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=A6=E6=83=85=E9=9D=A2=E6=9D=BF=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=BC=94=E5=91=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/tmdb/credits/route.ts | 69 +++++++++++++ src/components/DetailPanel.tsx | 166 +++++++++++++++++++++++++++++- src/lib/tmdb.client.ts | 44 ++++++++ 3 files changed, 274 insertions(+), 5 deletions(-) create mode 100644 src/app/api/tmdb/credits/route.ts diff --git a/src/app/api/tmdb/credits/route.ts b/src/app/api/tmdb/credits/route.ts new file mode 100644 index 0000000..5f02d90 --- /dev/null +++ b/src/app/api/tmdb/credits/route.ts @@ -0,0 +1,69 @@ +/* eslint-disable @typescript-eslint/no-explicit-any, no-console */ + +import { NextRequest, NextResponse } from 'next/server'; + +import { getAuthInfoFromCookie } from '@/lib/auth'; +import { getConfig } from '@/lib/config'; +import { getTMDBCredits } from '@/lib/tmdb.client'; + +export const runtime = 'nodejs'; + +/** + * GET /api/tmdb/credits?id=xxx&type=movie|tv + * 获取TMDB演职人员信息 + */ +export async function GET(request: NextRequest) { + try { + const authInfo = getAuthInfoFromCookie(request); + if (!authInfo || !authInfo.username) { + return NextResponse.json({ error: '未授权' }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); + const type = searchParams.get('type') || 'movie'; + + if (!id) { + return NextResponse.json({ error: '缺少ID参数' }, { status: 400 }); + } + + if (type !== 'movie' && type !== 'tv') { + return NextResponse.json({ error: '类型参数必须是movie或tv' }, { status: 400 }); + } + + const config = await getConfig(); + const tmdbApiKey = config.SiteConfig.TMDBApiKey; + const tmdbProxy = config.SiteConfig.TMDBProxy; + const tmdbReverseProxy = config.SiteConfig.TMDBReverseProxy; + + if (!tmdbApiKey) { + return NextResponse.json( + { error: 'TMDB API Key 未配置' }, + { status: 400 } + ); + } + + const response = await getTMDBCredits( + tmdbApiKey, + parseInt(id), + type as 'movie' | 'tv', + tmdbProxy, + tmdbReverseProxy + ); + + if (response.code !== 200 || !response.credits) { + return NextResponse.json( + { error: 'TMDB 演职人员信息获取失败', code: response.code }, + { status: response.code } + ); + } + + return NextResponse.json(response.credits); + } catch (error) { + console.error('TMDB演职人员信息获取失败:', error); + return NextResponse.json( + { error: '获取演职人员信息失败', details: (error as Error).message }, + { status: 500 } + ); + } +} diff --git a/src/components/DetailPanel.tsx b/src/components/DetailPanel.tsx index d38f7fa..723e680 100644 --- a/src/components/DetailPanel.tsx +++ b/src/components/DetailPanel.tsx @@ -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 = ({ const [startX, setStartX] = useState(0); const [scrollLeft, setScrollLeft] = useState(0); const episodesScrollRef = React.useRef(null); + const actorsScrollRef = React.useRef(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 = ({ 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 = ({ } }; + // 演员列表拖动滚动处理函数 + 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 = ({ 演员 -

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

+ {currentSource === 'tmdb' ? ( +
+
+ {detailData.actors.map((actor, index) => ( +
+ {actor.profile_path ? ( +
handleImageClick(processImageUrl(getTMDBImageUrl(actor.profile_path, 'w185')))} + > + {actor.name} +
+ ) : ( +
+ +
+ )} + e.stopPropagation()} + > + {actor.name} + + {actor.character && ( +

+ {actor.character} +

+ )} +
+ ))} +
+
+ ) : ( +

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

+ )} )} diff --git a/src/lib/tmdb.client.ts b/src/lib/tmdb.client.ts index 53d1448..b01620f 100644 --- a/src/lib/tmdb.client.ts +++ b/src/lib/tmdb.client.ts @@ -659,3 +659,47 @@ export async function getTMDBTVDetails( return { code: 500, details: null }; } } + +/** + * 获取 TMDB 演职人员信息 + * @param apiKey - TMDB API Key + * @param mediaId - 媒体ID + * @param mediaType - 媒体类型 (movie 或 tv) + * @param proxy - 代理服务器地址 + * @param reverseProxyBaseUrl - 反代 Base URL + * @returns 演职人员信息 + */ +export async function getTMDBCredits( + apiKey: string, + mediaId: number, + mediaType: 'movie' | 'tv', + proxy?: string, + reverseProxyBaseUrl?: string +): Promise<{ code: number; credits: any }> { + try { + const actualKey = getNextApiKey(apiKey); + if (!actualKey) { + return { code: 400, credits: null }; + } + + const baseUrl = reverseProxyBaseUrl || DEFAULT_TMDB_BASE_URL; + const url = `${baseUrl}/3/${mediaType}/${mediaId}/credits?api_key=${actualKey}&language=zh-CN`; + + const response = await universalFetch(url, proxy); + + if (!response.ok) { + console.error('TMDB Credits API 请求失败:', response.status, response.statusText); + return { code: response.status, credits: null }; + } + + const data: any = await response.json(); + + return { + code: 200, + credits: data, + }; + } catch (error) { + console.error('获取 TMDB 演职人员信息失败:', error); + return { code: 500, credits: null }; + } +}