import React, { useState, useEffect, useCallback, useRef } from 'react'; import ReactMasonryCss from 'react-masonry-css'; interface DoubanItem { imageUrl: string; title: string; subtitle: string; link: string; intro: string; rating: number; date: string; } interface Pagination { current: number; total: number; hasNext: boolean; hasPrev: boolean; } interface DoubanCollectionProps { type: 'movie' | 'book'; doubanId?: string; // 可选参数,使其与 MediaGrid 保持一致 className?: string; // 添加自定义类名 } const DoubanCollection: React.FC = ({ type, doubanId, className = '' }) => { const [items, setItems] = useState([]); const [pagination, setPagination] = useState({ current: 1, total: 1, hasNext: false, hasPrev: false }); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [isPageChanging, setIsPageChanging] = useState(false); // 使用 ref 避免竞态条件 const abortControllerRef = useRef(null); const isMountedRef = useRef(true); const fetchData = useCallback(async (start = 0) => { // 如果已经有一个请求在进行中,取消它 if (abortControllerRef.current) { abortControllerRef.current.abort(); } // 创建新的 AbortController abortControllerRef.current = new AbortController(); setLoading(true); setError(null); const params = new URLSearchParams(); params.append('type', type); params.append('start', start.toString()); if (doubanId) { params.append('doubanId', doubanId); } const url = `/api/douban?${params.toString()}`; try { const response = await fetch(url, { signal: abortControllerRef.current.signal }); // 如果组件已卸载,不继续处理 if (!isMountedRef.current) return; if (!response.ok) { throw new Error(`获取数据失败:状态码 ${response.status}`); } const data = await response.json(); if (data.error) { throw new Error(data.error); } setItems(data.items || []); setPagination(data.pagination || { current: 1, total: 1, hasNext: false, hasPrev: false }); } catch (err) { // 如果是取消请求的错误,不设置错误状态 if (err instanceof Error && err.name === 'AbortError') { return; } // 如果组件已卸载,不设置状态 if (!isMountedRef.current) return; console.error('获取豆瓣数据失败:', err); setError(err instanceof Error ? err.message : '未知错误'); setItems([]); } finally { // 如果组件已卸载,不设置状态 if (!isMountedRef.current) return; setLoading(false); setIsPageChanging(false); } }, [type, doubanId]); useEffect(() => { // 组件挂载时设置标记 isMountedRef.current = true; fetchData(); // 组件卸载时清理 return () => { isMountedRef.current = false; if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, [fetchData]); const handlePageChange = useCallback((page: number) => { if (isPageChanging) return; setIsPageChanging(true); // 计算新页面的起始项 const start = (page - 1) * 15; // 更新分页状态 setPagination(prev => ({ ...prev, current: page })); // 清空当前项目,显示加载状态 setItems([]); setLoading(true); // 获取新页面的数据 fetchData(start); }, [fetchData, isPageChanging]); const renderStars = useCallback((rating: number) => { return (
{[1, 2, 3, 4, 5].map((star) => ( ))}
); }, []); const breakpointColumnsObj = { default: 3, 1100: 2, 700: 1 }; // 加载中状态 if (loading && items.length === 0) { return (

{type === 'movie' ? '观影记录' : '读书记录'}

加载中...

); } // 错误状态 if (error) { return (

{type === 'movie' ? '观影记录' : '读书记录'}

错误: {error}

); } // 数据为空状态 if (items.length === 0) { return (

{type === 'movie' ? '观影记录' : '读书记录'}

暂无{type === 'movie' ? '观影' : '读书'}记录
); } return (

{type === 'movie' ? '观影记录' : '读书记录'}

{items.map((item, index) => ( ))} {/* 分页 */} {pagination.total > 1 && (
{pagination.current} / {pagination.total}
)}
); }; export default DoubanCollection;