import React, { useEffect, useRef, useState } from 'react'; interface MediaGridProps { type: 'movie' | 'book'; title: string; doubanId: string; } interface MediaItem { title: string; imageUrl: string; link: string; } interface PaginationInfo { current: number; hasNext: boolean; } const MediaGrid: React.FC = ({ type, title, doubanId }) => { const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(false); const [hasMoreContent, setHasMoreContent] = useState(true); const [currentPage, setCurrentPage] = useState(1); const [error, setError] = useState(null); const itemsPerPage = 15; const mediaListRef = useRef(null); const lastScrollTime = useRef(0); // 使用ref来跟踪关键状态,避免闭包问题 const stateRef = useRef({ isLoading: false, hasMoreContent: true, currentPage: 1, error: null as string | null }); // 封装fetch函数但不使用useCallback以避免依赖循环 const fetchMedia = async (page = 1, append = false) => { // 使用ref中的最新状态 if (stateRef.current.isLoading || (!append && !stateRef.current.hasMoreContent) || (append && !stateRef.current.hasMoreContent)) { return; } // 更新状态和ref setIsLoading(true); stateRef.current.isLoading = true; // 只在首次加载时清除错误 if (!append) { setError(null); stateRef.current.error = null; } const start = (page - 1) * itemsPerPage; try { const response = await fetch(`/api/douban?type=${type}&start=${start}&doubanId=${doubanId}`); if (!response.ok) { // 解析响应内容,获取详细错误信息 let errorMessage = `获取${type === 'movie' ? '电影' : '图书'}数据失败`; try { const errorData = await response.json(); if (errorData && errorData.error) { errorMessage = errorData.error; if (errorData.message) { errorMessage += `: ${errorData.message}`; } } } catch (e) { // 无法解析JSON,使用默认错误信息 } // 针对不同错误提供更友好的提示 if (response.status === 403) { errorMessage = "豆瓣接口访问受限,可能是请求过于频繁,请稍后再试"; } else if (response.status === 404) { // 对于404错误,如果是追加模式,说明已经到了最后一页,设置hasMoreContent为false if (append) { setHasMoreContent(false); stateRef.current.hasMoreContent = false; setIsLoading(false); stateRef.current.isLoading = false; return; // 直接返回,不设置错误,不清空已有数据 } else { errorMessage = "未找到相关内容,请检查豆瓣ID是否正确"; } } // 设置错误状态和ref setError(errorMessage); stateRef.current.error = errorMessage; // 只有非追加模式才清空数据 if (!append) { setItems([]); } throw new Error(errorMessage); } const data = await response.json(); if (data.items.length === 0) { // 如果返回的项目为空,则认为已经没有更多内容 setHasMoreContent(false); stateRef.current.hasMoreContent = false; if (!append) { setItems([]); } } else { if (append) { setItems(prev => { const newItems = [...prev, ...data.items]; return newItems; }); } else { setItems(data.items); } // 更新页码状态和ref setCurrentPage(data.pagination.current); stateRef.current.currentPage = data.pagination.current; // 更新是否有更多内容的状态和ref const newHasMoreContent = data.pagination.hasNext; setHasMoreContent(newHasMoreContent); stateRef.current.hasMoreContent = newHasMoreContent; } } catch (error) { // 只有在非追加模式下才清空已加载的内容 if (!append) { setItems([]); } } finally { // 重置加载状态 setIsLoading(false); stateRef.current.isLoading = false; } }; // 处理滚动事件 const handleScroll = () => { // 获取关键滚动值 const scrollY = window.scrollY; const windowHeight = window.innerHeight; const documentHeight = document.documentElement.scrollHeight; const scrollPosition = scrollY + windowHeight; const threshold = documentHeight - 300; // 限制滚动日志频率,每秒最多输出一次 const now = Date.now(); if (now - lastScrollTime.current < 1000) { return; } lastScrollTime.current = now; // 使用ref中的最新状态来检查 if (stateRef.current.isLoading || !stateRef.current.hasMoreContent || stateRef.current.error) { return; } // 当滚动到距离底部300px时加载更多 if (scrollPosition >= threshold) { fetchMedia(stateRef.current.currentPage + 1, true); } }; // 更新ref值以跟踪状态变化 useEffect(() => { stateRef.current.isLoading = isLoading; }, [isLoading]); useEffect(() => { stateRef.current.hasMoreContent = hasMoreContent; }, [hasMoreContent]); useEffect(() => { stateRef.current.currentPage = currentPage; }, [currentPage]); useEffect(() => { stateRef.current.error = error; }, [error]); // 组件初始化和依赖变化时重置 useEffect(() => { // 重置状态 setCurrentPage(1); stateRef.current.currentPage = 1; setHasMoreContent(true); stateRef.current.hasMoreContent = true; setError(null); stateRef.current.error = null; setIsLoading(false); stateRef.current.isLoading = false; // 清空列表 setItems([]); // 加载第一页数据 fetchMedia(1, false); // 管理滚动事件 const scrollListener = handleScroll; // 移除任何现有监听器 window.removeEventListener('scroll', scrollListener); // 添加滚动事件监听器 - 使用passive: true可提高滚动性能 window.addEventListener('scroll', scrollListener, { passive: true }); // 创建一个IntersectionObserver作为备选检测方案 const observerOptions = { root: null, rootMargin: '300px', threshold: 0.1 }; const intersectionObserver = new IntersectionObserver((entries) => { const entry = entries[0]; if (entry.isIntersecting && !stateRef.current.isLoading && stateRef.current.hasMoreContent && !stateRef.current.error) { fetchMedia(stateRef.current.currentPage + 1, true); } }, observerOptions); // 添加检测底部的元素 - 放在grid容器的后面而不是内部 const footer = document.createElement('div'); footer.id = 'scroll-detector'; footer.style.width = '100%'; footer.style.height = '10px'; // 确保mediaListRef有父元素 if (mediaListRef.current && mediaListRef.current.parentElement) { // 插入到grid后面而不是内部 mediaListRef.current.parentElement.insertBefore(footer, mediaListRef.current.nextSibling); intersectionObserver.observe(footer); } // 初始检查一次,以防内容不足一屏 const timeoutId = setTimeout(() => { if (stateRef.current.hasMoreContent && !stateRef.current.isLoading) { scrollListener(); } }, 500); // 清理函数 return () => { clearTimeout(timeoutId); window.removeEventListener('scroll', scrollListener); intersectionObserver.disconnect(); document.getElementById('scroll-detector')?.remove(); }; }, [type, doubanId]); // 只在关键属性变化时执行 // 错误提示组件 const ErrorMessage = () => { if (!error) return null; return (

访问错误

{error}

); }; // 没有更多内容提示 const EndMessage = () => { if (isLoading || !items.length || error) return null; return (

已加载全部内容

); }; return (

{title}

{error && items.length === 0 ? ( ) : items.length > 0 ? ( items.map((item, index) => ( )) ) : !isLoading ? (
暂无{type === 'movie' ? '电影' : '图书'}数据
) : null}
{error && items.length > 0 && (
)} {isLoading && (

加载更多...(第{currentPage}页)

)} {!hasMoreContent && items.length > 0 && !isLoading && ( )}
); }; export default MediaGrid;