/* eslint-disable no-console,react-hooks/exhaustive-deps,@typescript-eslint/no-explicit-any */ 'use client'; import { useSearchParams } from 'next/navigation'; import { Suspense } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react'; import { GetBangumiCalendarData } from '@/lib/bangumi.client'; import { getDoubanCategories, getDoubanList, getDoubanRecommends, } from '@/lib/douban.client'; import { DoubanItem, DoubanResult } from '@/lib/types'; import DoubanCardSkeleton from '@/components/DoubanCardSkeleton'; import DoubanCustomSelector from '@/components/DoubanCustomSelector'; import DoubanSelector from '@/components/DoubanSelector'; import PageLayout from '@/components/PageLayout'; import VideoCard from '@/components/VideoCard'; function DoubanPageClient() { const searchParams = useSearchParams(); const [doubanData, setDoubanData] = useState([]); const [loading, setLoading] = useState(false); const [currentPage, setCurrentPage] = useState(0); const [hasMore, setHasMore] = useState(true); const [isLoadingMore, setIsLoadingMore] = useState(false); const [selectorsReady, setSelectorsReady] = useState(false); const observerRef = useRef(null); const loadingRef = useRef(null); const debounceTimeoutRef = useRef(null); // 用于存储最新参数值的 refs const currentParamsRef = useRef({ type: '', primarySelection: '', secondarySelection: '', multiLevelSelection: {} as Record, selectedWeekday: '', currentPage: 0, }); const type = searchParams.get('type') || 'movie'; // 获取 runtimeConfig 中的自定义分类数据 const [customCategories, setCustomCategories] = useState< Array<{ name: string; type: 'movie' | 'tv'; query: string }> >([]); // 选择器状态 - 完全独立,不依赖URL参数 const [primarySelection, setPrimarySelection] = useState(() => { if (type === 'movie') return '热门'; if (type === 'tv' || type === 'show') return '最近热门'; if (type === 'anime') return '每日放送'; return ''; }); const [secondarySelection, setSecondarySelection] = useState(() => { if (type === 'movie') return '全部'; if (type === 'tv') return 'tv'; if (type === 'show') return 'show'; return '全部'; }); // MultiLevelSelector 状态 const [multiLevelValues, setMultiLevelValues] = useState< Record >({ type: 'all', region: 'all', year: 'all', platform: 'all', label: 'all', sort: 'T', }); // 星期选择器状态 const [selectedWeekday, setSelectedWeekday] = useState(''); // 获取自定义分类数据 useEffect(() => { const runtimeConfig = (window as any).RUNTIME_CONFIG; if (runtimeConfig?.CUSTOM_CATEGORIES?.length > 0) { setCustomCategories(runtimeConfig.CUSTOM_CATEGORIES); } }, []); // 同步最新参数值到 ref useEffect(() => { currentParamsRef.current = { type, primarySelection, secondarySelection, multiLevelSelection: multiLevelValues, selectedWeekday, currentPage, }; }, [ type, primarySelection, secondarySelection, multiLevelValues, selectedWeekday, currentPage, ]); // 初始化时标记选择器为准备好状态 useEffect(() => { // 短暂延迟确保初始状态设置完成 const timer = setTimeout(() => { setSelectorsReady(true); }, 50); return () => clearTimeout(timer); }, []); // 只在组件挂载时执行一次 // type变化时立即重置selectorsReady(最高优先级) useEffect(() => { setSelectorsReady(false); setLoading(true); // 立即显示loading状态 }, [type]); // 当type变化时重置选择器状态 useEffect(() => { if (type === 'custom' && customCategories.length > 0) { // 自定义分类模式:优先选择 movie,如果没有 movie 则选择 tv const types = Array.from( new Set(customCategories.map((cat) => cat.type)) ); if (types.length > 0) { // 优先选择 movie,如果没有 movie 则选择 tv let selectedType = types[0]; // 默认选择第一个 if (types.includes('movie')) { selectedType = 'movie'; } else { selectedType = 'tv'; } setPrimarySelection(selectedType); // 设置选中类型的第一个分类的 query 作为二级选择 const firstCategory = customCategories.find( (cat) => cat.type === selectedType ); if (firstCategory) { setSecondarySelection(firstCategory.query); } } } else { // 原有逻辑 if (type === 'movie') { setPrimarySelection('热门'); setSecondarySelection('全部'); } else if (type === 'tv') { setPrimarySelection('最近热门'); setSecondarySelection('tv'); } else if (type === 'show') { setPrimarySelection('最近热门'); setSecondarySelection('show'); } else if (type === 'anime') { setPrimarySelection('每日放送'); setSecondarySelection('全部'); } else { setPrimarySelection(''); setSecondarySelection('全部'); } } // 清空 MultiLevelSelector 状态 setMultiLevelValues({ type: 'all', region: 'all', year: 'all', platform: 'all', label: 'all', sort: 'T', }); // 使用短暂延迟确保状态更新完成后标记选择器准备好 const timer = setTimeout(() => { setSelectorsReady(true); }, 50); return () => clearTimeout(timer); }, [type, customCategories]); // 生成骨架屏数据 const skeletonData = Array.from({ length: 25 }, (_, index) => index); // 参数快照比较函数 const isSnapshotEqual = useCallback( ( snapshot1: { type: string; primarySelection: string; secondarySelection: string; multiLevelSelection: Record; selectedWeekday: string; currentPage: number; }, snapshot2: { type: string; primarySelection: string; secondarySelection: string; multiLevelSelection: Record; selectedWeekday: string; currentPage: number; } ) => { return ( snapshot1.type === snapshot2.type && snapshot1.primarySelection === snapshot2.primarySelection && snapshot1.secondarySelection === snapshot2.secondarySelection && snapshot1.selectedWeekday === snapshot2.selectedWeekday && snapshot1.currentPage === snapshot2.currentPage && JSON.stringify(snapshot1.multiLevelSelection) === JSON.stringify(snapshot2.multiLevelSelection) ); }, [] ); // 生成API请求参数的辅助函数 const getRequestParams = useCallback( (pageStart: number) => { // 当type为tv或show时,kind统一为'tv',category使用type本身 if (type === 'tv' || type === 'show') { return { kind: 'tv' as const, category: type, type: secondarySelection, pageLimit: 25, pageStart, }; } // 电影类型保持原逻辑 return { kind: type as 'tv' | 'movie', category: primarySelection, type: secondarySelection, pageLimit: 25, pageStart, }; }, [type, primarySelection, secondarySelection] ); // 防抖的数据加载函数 const loadInitialData = useCallback(async () => { // 创建当前参数的快照 const requestSnapshot = { type, primarySelection, secondarySelection, multiLevelSelection: multiLevelValues, selectedWeekday, currentPage: 0, }; try { setLoading(true); // 确保在加载初始数据时重置页面状态 setDoubanData([]); setCurrentPage(0); setHasMore(true); setIsLoadingMore(false); let data: DoubanResult; if (type === 'custom') { // 自定义分类模式:根据选中的一级和二级选项获取对应的分类 const selectedCategory = customCategories.find( (cat) => cat.type === primarySelection && cat.query === secondarySelection ); if (selectedCategory) { data = await getDoubanList({ tag: selectedCategory.query, type: selectedCategory.type, pageLimit: 25, pageStart: 0, }); } else { throw new Error('没有找到对应的分类'); } } else if (type === 'anime' && primarySelection === '每日放送') { const calendarData = await GetBangumiCalendarData(); const weekdayData = calendarData.find( (item) => item.weekday.en === selectedWeekday ); if (weekdayData) { data = { code: 200, message: 'success', list: weekdayData.items.map((item) => ({ id: item.id?.toString() || '', title: item.name_cn || item.name, poster: item.images?.large || item.images?.common || item.images?.medium || item.images?.small || item.images?.grid || '', // 空字符串,让 VideoCard 组件处理图片加载失败 rate: item.rating?.score?.toFixed(1) || '', year: item.air_date?.split('-')?.[0] || '', })), }; } else { throw new Error('没有找到对应的日期'); } } else if (type === 'anime') { data = await getDoubanRecommends({ kind: primarySelection === '番剧' ? 'tv' : 'movie', pageLimit: 25, pageStart: 0, category: '动画', format: primarySelection === '番剧' ? '电视剧' : '', region: multiLevelValues.region ? (multiLevelValues.region as string) : '', year: multiLevelValues.year ? (multiLevelValues.year as string) : '', platform: multiLevelValues.platform ? (multiLevelValues.platform as string) : '', sort: multiLevelValues.sort ? (multiLevelValues.sort as string) : '', label: multiLevelValues.label ? (multiLevelValues.label as string) : '', }); } else if (primarySelection === '全部') { data = await getDoubanRecommends({ kind: type === 'show' ? 'tv' : (type as 'tv' | 'movie'), pageLimit: 25, pageStart: 0, // 初始数据加载始终从第一页开始 category: multiLevelValues.type ? (multiLevelValues.type as string) : '', format: type === 'show' ? '综艺' : type === 'tv' ? '电视剧' : '', region: multiLevelValues.region ? (multiLevelValues.region as string) : '', year: multiLevelValues.year ? (multiLevelValues.year as string) : '', platform: multiLevelValues.platform ? (multiLevelValues.platform as string) : '', sort: multiLevelValues.sort ? (multiLevelValues.sort as string) : '', label: multiLevelValues.label ? (multiLevelValues.label as string) : '', }); } else { data = await getDoubanCategories(getRequestParams(0)); } if (data.code === 200) { // 检查参数是否仍然一致,如果一致才设置数据 // 使用 ref 获取最新的当前值 const currentSnapshot = { ...currentParamsRef.current }; if (isSnapshotEqual(requestSnapshot, currentSnapshot)) { setDoubanData(data.list); setHasMore(data.list.length !== 0); setLoading(false); } else { console.log('参数不一致,不执行任何操作,避免设置过期数据'); } // 如果参数不一致,不执行任何操作,避免设置过期数据 } else { throw new Error(data.message || '获取数据失败'); } } catch (err) { console.error(err); setLoading(false); // 发生错误时总是停止loading状态 } }, [ type, primarySelection, secondarySelection, multiLevelValues, selectedWeekday, getRequestParams, customCategories, ]); // 只在选择器准备好后才加载数据 useEffect(() => { // 只有在选择器准备好时才开始加载 if (!selectorsReady) { return; } // 清除之前的防抖定时器 if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); } // 使用防抖机制加载数据,避免连续状态更新触发多次请求 debounceTimeoutRef.current = setTimeout(() => { loadInitialData(); }, 100); // 100ms 防抖延迟 // 清理函数 return () => { if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); } }; }, [ selectorsReady, type, primarySelection, secondarySelection, multiLevelValues, selectedWeekday, loadInitialData, ]); // 单独处理 currentPage 变化(加载更多) useEffect(() => { if (currentPage > 0) { const fetchMoreData = async () => { // 创建当前参数的快照 const requestSnapshot = { type, primarySelection, secondarySelection, multiLevelSelection: multiLevelValues, selectedWeekday, currentPage, }; try { setIsLoadingMore(true); let data: DoubanResult; if (type === 'custom') { // 自定义分类模式:根据选中的一级和二级选项获取对应的分类 const selectedCategory = customCategories.find( (cat) => cat.type === primarySelection && cat.query === secondarySelection ); if (selectedCategory) { data = await getDoubanList({ tag: selectedCategory.query, type: selectedCategory.type, pageLimit: 25, pageStart: currentPage * 25, }); } else { throw new Error('没有找到对应的分类'); } } else if (type === 'anime' && primarySelection === '每日放送') { // 每日放送模式下,不进行数据请求,返回空数据 data = { code: 200, message: 'success', list: [], }; } else if (type === 'anime') { data = await getDoubanRecommends({ kind: primarySelection === '番剧' ? 'tv' : 'movie', pageLimit: 25, pageStart: currentPage * 25, category: '动画', format: primarySelection === '番剧' ? '电视剧' : '', region: multiLevelValues.region ? (multiLevelValues.region as string) : '', year: multiLevelValues.year ? (multiLevelValues.year as string) : '', platform: multiLevelValues.platform ? (multiLevelValues.platform as string) : '', sort: multiLevelValues.sort ? (multiLevelValues.sort as string) : '', label: multiLevelValues.label ? (multiLevelValues.label as string) : '', }); } else if (primarySelection === '全部') { data = await getDoubanRecommends({ kind: type === 'show' ? 'tv' : (type as 'tv' | 'movie'), pageLimit: 25, pageStart: currentPage * 25, category: multiLevelValues.type ? (multiLevelValues.type as string) : '', format: type === 'show' ? '综艺' : type === 'tv' ? '电视剧' : '', region: multiLevelValues.region ? (multiLevelValues.region as string) : '', year: multiLevelValues.year ? (multiLevelValues.year as string) : '', platform: multiLevelValues.platform ? (multiLevelValues.platform as string) : '', sort: multiLevelValues.sort ? (multiLevelValues.sort as string) : '', label: multiLevelValues.label ? (multiLevelValues.label as string) : '', }); } else { data = await getDoubanCategories( getRequestParams(currentPage * 25) ); } if (data.code === 200) { // 检查参数是否仍然一致,如果一致才设置数据 // 使用 ref 获取最新的当前值 const currentSnapshot = { ...currentParamsRef.current }; if (isSnapshotEqual(requestSnapshot, currentSnapshot)) { setDoubanData((prev) => [...prev, ...data.list]); setHasMore(data.list.length !== 0); } else { console.log('参数不一致,不执行任何操作,避免设置过期数据'); } } else { throw new Error(data.message || '获取数据失败'); } } catch (err) { console.error(err); } finally { setIsLoadingMore(false); } }; fetchMoreData(); } }, [ currentPage, type, primarySelection, secondarySelection, customCategories, multiLevelValues, selectedWeekday, ]); // 设置滚动监听 useEffect(() => { // 如果没有更多数据或正在加载,则不设置监听 if (!hasMore || isLoadingMore || loading) { return; } // 确保 loadingRef 存在 if (!loadingRef.current) { return; } const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasMore && !isLoadingMore) { setCurrentPage((prev) => prev + 1); } }, { threshold: 0.1 } ); observer.observe(loadingRef.current); observerRef.current = observer; return () => { if (observerRef.current) { observerRef.current.disconnect(); } }; }, [hasMore, isLoadingMore, loading]); // 处理选择器变化 const handlePrimaryChange = useCallback( (value: string) => { // 只有当值真正改变时才设置loading状态 if (value !== primarySelection) { setLoading(true); // 立即重置页面状态,防止基于旧状态的请求 setCurrentPage(0); setDoubanData([]); setHasMore(true); setIsLoadingMore(false); // 清空 MultiLevelSelector 状态 setMultiLevelValues({ type: 'all', region: 'all', year: 'all', platform: 'all', label: 'all', sort: 'T', }); // 如果是自定义分类模式,同时更新一级和二级选择器 if (type === 'custom' && customCategories.length > 0) { const firstCategory = customCategories.find( (cat) => cat.type === value ); if (firstCategory) { // 批量更新状态,避免多次触发数据加载 setPrimarySelection(value); setSecondarySelection(firstCategory.query); } else { setPrimarySelection(value); } } else { // 电视剧和综艺切换到"最近热门"时,重置二级分类为第一个选项 if ((type === 'tv' || type === 'show') && value === '最近热门') { setPrimarySelection(value); if (type === 'tv') { setSecondarySelection('tv'); } else if (type === 'show') { setSecondarySelection('show'); } } else { setPrimarySelection(value); } } } }, [primarySelection, type, customCategories] ); const handleSecondaryChange = useCallback( (value: string) => { // 只有当值真正改变时才设置loading状态 if (value !== secondarySelection) { setLoading(true); // 立即重置页面状态,防止基于旧状态的请求 setCurrentPage(0); setDoubanData([]); setHasMore(true); setIsLoadingMore(false); setSecondarySelection(value); } }, [secondarySelection] ); const handleMultiLevelChange = useCallback( (values: Record) => { // 比较两个对象是否相同,忽略顺序 const isEqual = ( obj1: Record, obj2: Record ) => { const keys1 = Object.keys(obj1).sort(); const keys2 = Object.keys(obj2).sort(); if (keys1.length !== keys2.length) return false; return keys1.every((key) => obj1[key] === obj2[key]); }; // 如果相同,则不设置loading状态 if (isEqual(values, multiLevelValues)) { return; } setLoading(true); // 立即重置页面状态,防止基于旧状态的请求 setCurrentPage(0); setDoubanData([]); setHasMore(true); setIsLoadingMore(false); setMultiLevelValues(values); }, [multiLevelValues] ); const handleWeekdayChange = useCallback((weekday: string) => { setSelectedWeekday(weekday); }, []); const getPageTitle = () => { // 根据 type 生成标题 return type === 'movie' ? '电影' : type === 'tv' ? '电视剧' : type === 'anime' ? '动漫' : type === 'show' ? '综艺' : '自定义'; }; const getPageDescription = () => { if (type === 'anime' && primarySelection === '每日放送') { return '来自 Bangumi 番组计划的精选内容'; } return '来自豆瓣的精选内容'; }; const getActivePath = () => { const params = new URLSearchParams(); if (type) params.set('type', type); const queryString = params.toString(); const activePath = `/douban${queryString ? `?${queryString}` : ''}`; return activePath; }; return (
{/* 页面标题和选择器 */}
{/* 页面标题 */}

{getPageTitle()}

{getPageDescription()}

{/* 选择器组件 */} {type !== 'custom' ? (
) : (
)}
{/* 内容展示区域 */}
{/* 内容网格 */}
{loading || !selectorsReady ? // 显示骨架屏 skeletonData.map((index) => ) : // 显示实际数据 doubanData.map((item, index) => (
))}
{/* 加载更多指示器 */} {hasMore && !loading && (
{ if (el && el.offsetParent !== null) { ( loadingRef as React.MutableRefObject ).current = el; } }} className='flex justify-center mt-12 py-8' > {isLoadingMore && (
加载中...
)}
)} {/* 没有更多数据提示 */} {!hasMore && doubanData.length > 0 && (
已加载全部内容
)} {/* 空状态 */} {!loading && doubanData.length === 0 && (
暂无相关内容
)}
); } export default function DoubanPage() { return ( ); }