/* eslint-disable @typescript-eslint/no-explicit-any, react-hooks/exhaustive-deps, no-console */ 'use client'; import { BookMarked, BookOpen, Bot, ChevronRight, Link as LinkIcon, ListVideo, Music, } from 'lucide-react'; import Link from 'next/link'; import { Suspense, useEffect, useState } from 'react'; import { BangumiCalendarData, GetBangumiCalendarData, } from '@/lib/bangumi.client'; import { getDoubanCategories } from '@/lib/douban.client'; import { getTMDBImageUrl, TMDBItem } from '@/lib/tmdb.client'; import { DoubanItem } from '@/lib/types'; import { base58Encode, processImageUrl } from '@/lib/utils'; import AIChatPanel from '@/components/AIChatPanel'; import BannerCarousel from '@/components/BannerCarousel'; import ContinueWatching from '@/components/ContinueWatching'; import FireworksCanvas from '@/components/FireworksCanvas'; import HttpWarningDialog from '@/components/HttpWarningDialog'; import PageLayout from '@/components/PageLayout'; import ScrollableRow from '@/components/ScrollableRow'; import { useSite } from '@/components/SiteProvider'; import Toast, { ToastProps } from '@/components/Toast'; import VideoCard from '@/components/VideoCard'; // 首页模块配置接口 interface HomeModule { id: string; name: string; enabled: boolean; order: number; } function HomeClient() { // 移除了 activeTab 状态,收藏夹功能已移到 UserMenu const [hotMovies, setHotMovies] = useState([]); const [hotTvShows, setHotTvShows] = useState([]); const [hotVarietyShows, setHotVarietyShows] = useState([]); const [hotDuanju, setHotDuanju] = useState([]); const [upcomingContent, setUpcomingContent] = useState([]); const [bangumiCalendarData, setBangumiCalendarData] = useState< BangumiCalendarData[] >([]); const [loading, setLoading] = useState(true); const { announcement } = useSite(); // 首页模块配置状态 const [homeModules, setHomeModules] = useState([ { id: 'hotMovies', name: '热门电影', enabled: true, order: 0 }, { id: 'hotDuanju', name: '热播短剧', enabled: true, order: 1 }, { id: 'bangumiCalendar', name: '新番放送', enabled: true, order: 2 }, { id: 'hotTvShows', name: '热门剧集', enabled: true, order: 3 }, { id: 'hotVarietyShows', name: '热门综艺', enabled: true, order: 4 }, { id: 'upcomingContent', name: '即将上映', enabled: true, order: 5 }, ]); const [homeBannerEnabled, setHomeBannerEnabled] = useState(true); const [homeContinueWatchingEnabled, setHomeContinueWatchingEnabled] = useState(true); const [showAnnouncement, setShowAnnouncement] = useState(false); const [showHttpWarning, setShowHttpWarning] = useState(true); const [showAIChat, setShowAIChat] = useState(false); const [aiEnabled, setAiEnabled] = useState(false); const [aiDefaultMessageNoVideo, setAiDefaultMessageNoVideo] = useState( '你好!我是MoonTVPlus的AI影视助手。想看什么电影或剧集?需要推荐吗?' ); const [sourceSearchEnabled, setSourceSearchEnabled] = useState(true); const [musicEnabled, setMusicEnabled] = useState(false); const [mangaEnabled, setMangaEnabled] = useState(false); const [booksEnabled, setBooksEnabled] = useState(false); const [netdiskTempPlayEnabled, setNetdiskTempPlayEnabled] = useState(false); const [showDirectPlayDialog, setShowDirectPlayDialog] = useState(false); const [directPlayUrl, setDirectPlayUrl] = useState(''); const [directPlaySubmitting, setDirectPlaySubmitting] = useState(false); const [toast, setToast] = useState(null); const detectNetdiskLink = ( url: string ): { provider: 'quark' | 'mobile' | 'baidu' | 'tianyi' | '123' | 'uc' | '115'; shareUrl: string; passcode?: string; } | null => { const trimmed = url.trim(); const pickPasscode = (...values: Array) => values.map((item) => item?.trim()).find(Boolean); const inlinePasscode = (text: string) => pickPasscode( text.match( /(?:提取码|访问码|密码)\s*[::=]?\s*([a-zA-Z0-9]{4,8})/i )?.[1], text.match(/[?&](?:pwd|passcode|accessCode)=([^&\s]+)/i)?.[1] ); if ( /https:\/\/(?:www\.)?123(?:684|865|912|pan)\.(?:com|cn)\/s\//i.test( trimmed ) ) { return { provider: '123', shareUrl: trimmed, passcode: pickPasscode( trimmed.match(/[?&]pwd=([^&]+)/i)?.[1], inlinePasscode(trimmed) ), }; } if ( /https:\/\/cloud\.189\.cn\/(web\/share\?code=|t\/)/i.test(trimmed) || /https:\/\/h5\.cloud\.189\.cn\/share\.html#\/t\//i.test(trimmed) ) { return { provider: 'tianyi', shareUrl: trimmed, passcode: pickPasscode( trimmed.match(/[?&]pwd=([^&]+)/i)?.[1], inlinePasscode(trimmed) ), }; } if (/pan\.baidu\.com\/(s\/|wap\/init\?surl=)/i.test(trimmed)) { return { provider: 'baidu', shareUrl: trimmed, passcode: pickPasscode( trimmed.match(/[?&](?:pwd|accessCode)=([^&]+)/i)?.[1], inlinePasscode(trimmed) ), }; } if (/https:\/\/pan\.quark\.cn\/s\//i.test(trimmed)) { return { provider: 'quark', shareUrl: trimmed, passcode: pickPasscode( trimmed.match(/[?&](?:pwd|passcode)=([^&]+)/i)?.[1], inlinePasscode(trimmed) ), }; } if (/https:\/\/drive\.uc\.cn\/s\//i.test(trimmed)) { return { provider: 'uc', shareUrl: trimmed, passcode: pickPasscode( trimmed.match(/[?&](?:pwd|passcode)=([^&]+)/i)?.[1], inlinePasscode(trimmed) ), }; } if (/https:\/\/(?:yun|caiyun)\.139\.com\//i.test(trimmed)) { return { provider: 'mobile', shareUrl: trimmed }; } if (/https:\/\/(?:115|anxia|115cdn)\.com\/s\//i.test(trimmed)) { return { provider: '115', shareUrl: trimmed, passcode: pickPasscode( trimmed.match(/[?&](?:password|pwd|passcode)=([^&]+)/i)?.[1], inlinePasscode(trimmed) ), }; } return null; }; const handleDirectPlay = () => { setDirectPlayUrl(''); setShowDirectPlayDialog(true); }; const submitDirectPlay = async () => { const trimmed = directPlayUrl.trim(); if (!trimmed) return; setDirectPlaySubmitting(true); try { const netdisk = detectNetdiskLink(trimmed); if (netdisk && !netdiskTempPlayEnabled) { throw new Error('无权限使用临时播放'); } if (netdisk) { const source = netdisk.provider === 'mobile' ? 'netdisk-mobile' : netdisk.provider === 'baidu' ? 'netdisk-baidu' : netdisk.provider === 'tianyi' ? 'netdisk-tianyi' : netdisk.provider === '115' ? 'netdisk-115' : netdisk.provider === 'uc' ? 'netdisk-uc' : netdisk.provider === '123' ? 'netdisk-123' : 'netdisk-quark'; const id = base58Encode( JSON.stringify({ shareUrl: netdisk.shareUrl, passcode: netdisk.passcode || '', }) ); if (!id) { throw new Error('网盘链接编码失败'); } const targetUrl = `/play?source=${encodeURIComponent( source )}&id=${encodeURIComponent(id)}&title=${encodeURIComponent( '网盘直链播放' )}`; setShowDirectPlayDialog(false); setDirectPlayUrl(''); window.location.assign(targetUrl); return; } const encoded = base58Encode(trimmed); if (!encoded) return; const targetUrl = `/play?source=directplay&id=${encodeURIComponent( encoded )}`; setShowDirectPlayDialog(false); setDirectPlayUrl(''); window.location.assign(targetUrl); } catch (error) { setToast({ message: error instanceof Error ? error.message : '播放失败', type: 'error', onClose: () => setToast(null), }); } finally { setDirectPlaySubmitting(false); } }; const loadHomeLayoutSettings = () => { if (typeof window === 'undefined') return; const savedHomeModules = localStorage.getItem('homeModules'); if (savedHomeModules) { try { setHomeModules(JSON.parse(savedHomeModules)); } catch (error) { console.error('解析首页模块配置失败:', error); } } const savedHomeBannerEnabled = localStorage.getItem('homeBannerEnabled'); if (savedHomeBannerEnabled !== null) { setHomeBannerEnabled(savedHomeBannerEnabled === 'true'); } const savedHomeContinueWatchingEnabled = localStorage.getItem( 'homeContinueWatchingEnabled' ); if (savedHomeContinueWatchingEnabled !== null) { setHomeContinueWatchingEnabled( savedHomeContinueWatchingEnabled === 'true' ); } }; // 加载首页模块配置 useEffect(() => { loadHomeLayoutSettings(); }, []); // 监听首页模块配置更新事件 useEffect(() => { const handleHomeModulesUpdated = () => { loadHomeLayoutSettings(); }; window.addEventListener('homeModulesUpdated', handleHomeModulesUpdated); return () => { window.removeEventListener( 'homeModulesUpdated', handleHomeModulesUpdated ); }; }, []); // 检查AI功能是否启用 useEffect(() => { if (typeof window !== 'undefined') { const enabled = (window as any).RUNTIME_CONFIG?.AI_ENABLED && (window as any).RUNTIME_CONFIG?.AI_ENABLE_HOMEPAGE_ENTRY; setAiEnabled(enabled); // 加载AI默认消息配置 const defaultMsg = (window as any).RUNTIME_CONFIG ?.AI_DEFAULT_MESSAGE_NO_VIDEO; if (defaultMsg) { setAiDefaultMessageNoVideo(defaultMsg); } } }, []); // 检查源站寻片功能是否启用 useEffect(() => { if (typeof window !== 'undefined') { const enabled = (window as any).RUNTIME_CONFIG?.ENABLE_SOURCE_SEARCH !== false; setSourceSearchEnabled(enabled); } }, []); // 检查音乐功能是否启用 useEffect(() => { if (typeof window !== 'undefined') { const enabled = !!(window as any).RUNTIME_CONFIG?.MUSIC_ENABLED; setMusicEnabled(enabled); } }, []); // 检查漫画功能是否启用 useEffect(() => { if (typeof window !== 'undefined') { const enabled = !!(window as any).RUNTIME_CONFIG?.SUWAYOMI_ENABLED; setMangaEnabled(enabled); } }, []); // 检查电子书功能是否启用 useEffect(() => { if (typeof window !== 'undefined') { const enabled = !!(window as any).RUNTIME_CONFIG?.BOOKS_ENABLED; setBooksEnabled(enabled); } }, []); // 检查网盘临时播放权限,仅有权限时在直链播放弹窗展示网盘在线播放提示 useEffect(() => { if (typeof window !== 'undefined') { const enabled = !!(window as any).RUNTIME_CONFIG ?.NETDISK_TEMP_PLAY_ENABLED; setNetdiskTempPlayEnabled(enabled); } }, []); // 检查公告弹窗状态 useEffect(() => { if (typeof window !== 'undefined' && announcement) { const hasSeenAnnouncement = localStorage.getItem('hasSeenAnnouncement'); if (hasSeenAnnouncement !== announcement) { setShowAnnouncement(true); } else { setShowAnnouncement(Boolean(!hasSeenAnnouncement && announcement)); } } }, [announcement]); useEffect(() => { const CACHE_DURATION = 60 * 60 * 1000; // 1小时 const getCache = (key: string) => { try { const cached = localStorage.getItem(key); if (!cached) return null; const { data, timestamp } = JSON.parse(cached); return { data, expired: Date.now() - timestamp > CACHE_DURATION }; } catch { return null; } }; const setCache = (key: string, data: any) => { try { localStorage.setItem( key, JSON.stringify({ data, timestamp: Date.now() }) ); } catch { // Ignore localStorage errors } }; const moviesCache = getCache('homepage_movies'); const tvShowsCache = getCache('homepage_tvshows'); const varietyCache = getCache('homepage_variety'); const bangumiCache = getCache('homepage_bangumi'); const duanjuCache = getCache('homepage_duanju'); const upcomingCache = getCache('homepage_upcoming'); if (moviesCache?.data) setHotMovies(moviesCache.data); if (tvShowsCache?.data) setHotTvShows(tvShowsCache.data); if (varietyCache?.data) setHotVarietyShows(varietyCache.data); if (bangumiCache?.data) setBangumiCalendarData(bangumiCache.data); if (duanjuCache?.data) setHotDuanju(duanjuCache.data); if (upcomingCache?.data) setUpcomingContent(upcomingCache.data); const hasCache = moviesCache || tvShowsCache || varietyCache || bangumiCache || duanjuCache || upcomingCache; if (hasCache) setLoading(false); const needsRefresh = !moviesCache || moviesCache.expired || !tvShowsCache || tvShowsCache.expired || !varietyCache || varietyCache.expired || !bangumiCache || bangumiCache.expired || !duanjuCache || duanjuCache.expired || !upcomingCache || upcomingCache.expired; if (needsRefresh) { (async () => { try { const [ moviesData, tvShowsData, varietyShowsData, bangumiCalendarData, ] = await Promise.all([ getDoubanCategories({ kind: 'movie', category: '热门', type: '全部', }).catch((error) => { console.error('获取热门电影数据失败:', error); return null; }), getDoubanCategories({ kind: 'tv', category: 'tv', type: 'tv', }).catch((error) => { console.error('获取热门剧集数据失败:', error); return null; }), getDoubanCategories({ kind: 'tv', category: 'show', type: 'show', }).catch((error) => { console.error('获取热门综艺数据失败:', error); return null; }), GetBangumiCalendarData().catch((error) => { console.error('获取新番放送数据失败:', error); return []; }), ]); if (moviesData?.code === 200) { setHotMovies(moviesData.list); if (moviesData.list && moviesData.list.length > 0) { setCache('homepage_movies', moviesData.list); } } if (tvShowsData?.code === 200) { setHotTvShows(tvShowsData.list); if (tvShowsData.list && tvShowsData.list.length > 0) { setCache('homepage_tvshows', tvShowsData.list); } } if (varietyShowsData?.code === 200) { setHotVarietyShows(varietyShowsData.list); if (varietyShowsData.list && varietyShowsData.list.length > 0) { setCache('homepage_variety', varietyShowsData.list); } } setBangumiCalendarData(bangumiCalendarData); if (bangumiCalendarData && bangumiCalendarData.length > 0) { setCache('homepage_bangumi', bangumiCalendarData); } try { const duanjuResponse = await fetch('/api/duanju/recommends'); if (duanjuResponse.ok) { const duanjuResult = await duanjuResponse.json(); if ( duanjuResult.code === 200 && duanjuResult.data && duanjuResult.data.length > 0 ) { setHotDuanju(duanjuResult.data); setCache('homepage_duanju', duanjuResult.data); } } } catch (error) { console.error('获取热播短剧数据失败:', error); } try { const response = await fetch('/api/tmdb/upcoming'); if (response.ok) { const result = await response.json(); if ( result.code === 200 && result.data && result.data.length > 0 ) { const sorted = [...result.data].sort((a, b) => { const dateA = new Date( a.release_date || '9999-12-31' ).getTime(); const dateB = new Date( b.release_date || '9999-12-31' ).getTime(); return dateA - dateB; }); setUpcomingContent(sorted); setCache('homepage_upcoming', sorted); } } } catch (error) { console.error('获取TMDB即将上映数据失败:', error); } setLoading(false); } catch (error) { console.error('获取推荐数据失败:', error); setLoading(false); } })(); } }, []); const handleCloseAnnouncement = (announcement: string) => { setShowAnnouncement(false); localStorage.setItem('hasSeenAnnouncement', announcement); // 记录已查看弹窗 }; // 渲染模块的函数 const renderModule = (moduleId: string) => { switch (moduleId) { case 'hotMovies': return (

热门电影

查看更多
{loading ? Array.from({ length: 8 }).map((_, index) => (
)) : hotMovies.map((movie) => (
))}
); case 'hotDuanju': if (hotDuanju.length === 0) return null; return (

热播短剧

查看更多
{loading ? Array.from({ length: 8 }).map((_, index) => (
)) : hotDuanju.map((duanju) => (
))}
); case 'bangumiCalendar': return (

新番放送

查看更多
{loading ? Array.from({ length: 8 }).map((_, index) => (
)) : (() => { const today = new Date(); const weekdays = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', ]; const currentWeekday = weekdays[today.getDay()]; const todayAnimes = bangumiCalendarData .find((item) => item.weekday.en === currentWeekday) ?.items.filter((anime) => anime.images) || []; return todayAnimes.map((anime, index) => (
)); })()}
); case 'hotTvShows': return (

热门剧集

查看更多
{loading ? Array.from({ length: 8 }).map((_, index) => (
)) : hotTvShows.map((tvShow) => (
))}
); case 'hotVarietyShows': return (

热门综艺

查看更多
{loading ? Array.from({ length: 8 }).map((_, index) => (
)) : hotVarietyShows.map((varietyShow) => (
))}
); case 'upcomingContent': if (upcomingContent.length === 0) return null; return (

即将上映

{upcomingContent.map((item) => (
0 ? item.vote_average.toFixed(1) : '' } type={item.media_type === 'tv' ? 'tv' : 'movie'} from='douban' releaseDate={item.release_date} isUpcoming={true} />
))}
); default: return null; } }; return ( {/* TMDB 热门轮播图 */} {homeBannerEnabled && (
)}
{/* 首页内容 */} <> {/* 源站寻片和AI问片入口 */}
{musicEnabled && ( )} {mangaEnabled && ( )} {booksEnabled && ( )} {/* 源站寻片入口 */} {sourceSearchEnabled && ( )} {/* AI问片入口 */} {aiEnabled && ( )}
{/* 继续观看 */} {homeContinueWatchingEnabled && } {/* 根据配置动态渲染首页模块 */} {homeModules .filter((module) => module.enabled) .sort((a, b) => a.order - b.order) .map((module) => renderModule(module.id))}
{/* HTTP 环境警告弹窗 */} {showHttpWarning && ( setShowHttpWarning(false)} /> )} {/* AI问片面板 */} {aiEnabled && ( setShowAIChat(false)} welcomeMessage={aiDefaultMessageNoVideo} /> )} {/* 公告弹窗 */} {showAnnouncement && (

公告

{announcement}
)} {showDirectPlayDialog && (
setShowDirectPlayDialog(false)} >
event.stopPropagation()} >

直链播放

请输入可直接播放的视频链接。
{netdiskTempPlayEnabled && (
支持夸克、UC、百度、天翼、移动、123、115 网盘在线播放。
)} setDirectPlayUrl(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') { submitDirectPlay(); } }} placeholder='https://example.com/video.m3u8' className='w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500' />
)} {toast && }
); } export default function Home() { return ( ); }