From c4198e0954c6f6c0827de6c9ab29d62b8f46999c Mon Sep 17 00:00:00 2001 From: mtvpls Date: Fri, 30 Jan 2026 10:18:17 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B1=86=E7=93=A3=E9=A2=84=E5=91=8A=E7=89=87?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E8=8E=B7=E5=8F=96+=E5=8F=AF=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E9=9D=99=E9=9F=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/tmdb/trending/route.ts | 7 -- src/components/BannerCarousel.tsx | 111 ++++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 10 deletions(-) diff --git a/src/app/api/tmdb/trending/route.ts b/src/app/api/tmdb/trending/route.ts index 3fd835f..25dcd14 100644 --- a/src/app/api/tmdb/trending/route.ts +++ b/src/app/api/tmdb/trending/route.ts @@ -309,11 +309,6 @@ async function getDoubanBannerContent(): Promise<{ code: number; list: any[] }> const detail = await fetchDoubanData(detailUrl); - // 获取预告片链接(取第一个)- 豆瓣是直链视频URL - const trailerUrl = detail.trailers && detail.trailers.length > 0 - ? detail.trailers[0].video_url - : null; - // 获取横屏图片 const backdropPath = detail.cover_url || movie.pic?.large || movie.pic?.normal || ''; @@ -345,7 +340,6 @@ async function getDoubanBannerContent(): Promise<{ code: number; list: any[] }> media_type: 'movie', genre_ids: [], genres: tags, // 使用从card_subtitle提取的标签 - trailer_url: trailerUrl, // 豆瓣预告片直链 video_key: null, // 豆瓣不使用YouTube key }; } catch (error) { @@ -377,7 +371,6 @@ async function getDoubanBannerContent(): Promise<{ code: number; list: any[] }> media_type: 'movie', genre_ids: [], genres: tags, // 使用从card_subtitle提取的标签 - trailer_url: null, video_key: null, }; } diff --git a/src/components/BannerCarousel.tsx b/src/components/BannerCarousel.tsx index c807de4..5389268 100644 --- a/src/components/BannerCarousel.tsx +++ b/src/components/BannerCarousel.tsx @@ -1,11 +1,12 @@ 'use client'; -import { ChevronLeft, ChevronRight, Play } from 'lucide-react'; +import { ChevronLeft, ChevronRight, Play, Volume2, VolumeX } from 'lucide-react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; import { useCallback, useEffect, useRef,useState } from 'react'; import { type TMDBItem,getGenreNames, getTMDBImageUrl } from '@/lib/tmdb.client'; +import { getDoubanDetail } from '@/lib/douban.client'; import { processImageUrl } from '@/lib/utils'; interface BannerCarouselProps { @@ -32,6 +33,10 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa const [isYouTubeAccessible, setIsYouTubeAccessible] = useState(false); // YouTube连通性(默认false,检查后再决定) const [enableTrailers, setEnableTrailers] = useState(false); // 是否启用预告片(默认关闭) const [dataSource, setDataSource] = useState(''); // 当前数据源 + const [trailersLoaded, setTrailersLoaded] = useState(false); // 预告片是否已加载 + const [isMuted, setIsMuted] = useState(true); // 视频是否静音(默认静音) + const videoRef = useRef(null); // 视频元素引用 + const videoRefs = useRef>(new Map()); // 所有视频元素的引用 const touchStartX = useRef(0); const touchEndX = useRef(0); const isManualChange = useRef(false); // 标记是否为手动切换 @@ -49,6 +54,19 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa router.push(`/play?title=${encodeURIComponent(title)}`); }; + // 切换音量 + const toggleMute = (e: React.MouseEvent) => { + e.stopPropagation(); + const newMutedState = !isMuted; + setIsMuted(newMutedState); + + // 直接更新当前视频元素的静音状态 + const currentVideo = videoRefs.current.get(currentIndex); + if (currentVideo) { + currentVideo.muted = newMutedState; + } + }; + // 获取图片URL(处理TX完整URL和TMDB路径) const getImageUrl = (path: string | null) => { if (!path) return ''; @@ -167,6 +185,7 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa setItems(cachedData); setDataSource(validSource || ''); // 设置数据源 setIsLoading(false); + setTrailersLoaded(false); // 重置预告片加载状态 } // 如果缓存过期或没有缓存,后台更新数据 @@ -180,6 +199,7 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa setItems(result.list); setDataSource(newDataSource); // 设置数据源 + setTrailersLoaded(false); // 重置预告片加载状态 // 保存到 localStorage(使用数据源特定的key) try { @@ -203,6 +223,70 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa fetchTrending(); }, [shouldLoad]); + // 前端获取豆瓣预告片 + useEffect(() => { + // 只有在启用预告片、数据源是豆瓣、有数据且未加载预告片时才执行 + if (!enableTrailers || dataSource !== 'Douban' || items.length === 0 || trailersLoaded) { + return; + } + + const fetchDoubanTrailers = async () => { + try { + // 为每个项目获取预告片 + const itemsWithTrailers = await Promise.all( + items.map(async (item) => { + try { + // 使用统一的豆瓣详情获取函数(会根据用户配置的代理设置自动选择请求方式) + const detail = await getDoubanDetail(item.id.toString()); + + // 获取预告片链接(取第一个) + const trailerUrl = detail.trailers && detail.trailers.length > 0 + ? detail.trailers[0].video_url + : null; + + return { + ...item, + trailer_url: trailerUrl, + }; + } catch (error) { + console.error(`获取豆瓣电影 ${item.id} 预告片失败:`, error); + return item; + } + }) + ); + + setItems(itemsWithTrailers); + setTrailersLoaded(true); + } catch (error) { + console.error('获取豆瓣预告片失败:', error); + } + }; + + fetchDoubanTrailers(); + }, [enableTrailers, dataSource, items.length, trailersLoaded]); + + // 切换轮播图时重置静音状态 + useEffect(() => { + setIsMuted(true); + }, [currentIndex]); + + // 控制视频播放/暂停和静音状态 + useEffect(() => { + // 遍历所有视频元素 + videoRefs.current.forEach((video, index) => { + if (index === currentIndex) { + // 当前显示的视频:播放并设置静音状态 + video.muted = isMuted; + video.play().catch(() => { + // 忽略自动播放失败的错误 + }); + } else { + // 非当前显示的视频:暂停 + video.pause(); + } + }); + }, [currentIndex, isMuted]); + // 自动播放 useEffect(() => { if (!items.length || isPaused) return; @@ -336,10 +420,16 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa /* 显示豆瓣直链视频 */