feat: 实现真正的无限滚动加载

- 修改 PaginatedRow 组件支持动态加载更多数据
- 添加 onLoadMore 回调函数和加载状态管理
- 在首页三个版块实现真正的分页加载新内容
- 第一页时隐藏左箭头,避免无效操作
- 移除底部页码指示器,界面更简洁
- 右箭头点击时动态从豆瓣API加载新数据
This commit is contained in:
katelya
2025-09-04 16:36:59 +08:00
parent 8f23545439
commit 63120d418b
2 changed files with 164 additions and 16 deletions
+127 -3
View File
@@ -82,6 +82,21 @@ function HomeClient() {
const [loading, setLoading] = useState(true);
const { announcement } = useSite();
// 分页状态管理
const [moviePage, setMoviePage] = useState(0);
const [tvShowPage, setTvShowPage] = useState(0);
const [varietyShowPage, setVarietyShowPage] = useState(0);
const [loadingMore, setLoadingMore] = useState({
movies: false,
tvShows: false,
varietyShows: false,
});
const [hasMoreData, setHasMoreData] = useState({
movies: true,
tvShows: true,
varietyShows: true,
});
const [showAnnouncement, setShowAnnouncement] = useState(false);
// 检查公告弹窗状态
@@ -148,6 +163,100 @@ function HomeClient() {
fetchDoubanData();
}, []);
// 加载更多电影
const loadMoreMovies = async () => {
if (loadingMore.movies || !hasMoreData.movies) return;
setLoadingMore(prev => ({ ...prev, movies: true }));
try {
const nextPage = moviePage + 1;
const moviesData = await getDoubanCategories({
kind: 'movie',
category: '热门',
type: '全部',
pageStart: nextPage * 20,
pageLimit: 20,
});
if (moviesData.code === 200 && moviesData.list.length > 0) {
setHotMovies(prev => [...prev, ...moviesData.list]);
setMoviePage(nextPage);
// 如果返回的数据少于请求的数量,说明没有更多数据了
if (moviesData.list.length < 20) {
setHasMoreData(prev => ({ ...prev, movies: false }));
}
} else {
setHasMoreData(prev => ({ ...prev, movies: false }));
}
} catch (error) {
// 静默处理错误
} finally {
setLoadingMore(prev => ({ ...prev, movies: false }));
}
};
// 加载更多剧集
const loadMoreTvShows = async () => {
if (loadingMore.tvShows || !hasMoreData.tvShows) return;
setLoadingMore(prev => ({ ...prev, tvShows: true }));
try {
const nextPage = tvShowPage + 1;
const tvShowsData = await getDoubanCategories({
kind: 'tv',
category: 'tv',
type: 'tv',
pageStart: nextPage * 20,
pageLimit: 20,
});
if (tvShowsData.code === 200 && tvShowsData.list.length > 0) {
setHotTvShows(prev => [...prev, ...tvShowsData.list]);
setTvShowPage(nextPage);
if (tvShowsData.list.length < 20) {
setHasMoreData(prev => ({ ...prev, tvShows: false }));
}
} else {
setHasMoreData(prev => ({ ...prev, tvShows: false }));
}
} catch (error) {
// 静默处理错误
} finally {
setLoadingMore(prev => ({ ...prev, tvShows: false }));
}
};
// 加载更多综艺
const loadMoreVarietyShows = async () => {
if (loadingMore.varietyShows || !hasMoreData.varietyShows) return;
setLoadingMore(prev => ({ ...prev, varietyShows: true }));
try {
const nextPage = varietyShowPage + 1;
const varietyShowsData = await getDoubanCategories({
kind: 'tv',
category: 'show',
type: 'show',
pageStart: nextPage * 20,
pageLimit: 20,
});
if (varietyShowsData.code === 200 && varietyShowsData.list.length > 0) {
setHotVarietyShows(prev => [...prev, ...varietyShowsData.list]);
setVarietyShowPage(nextPage);
if (varietyShowsData.list.length < 20) {
setHasMoreData(prev => ({ ...prev, varietyShows: false }));
}
} else {
setHasMoreData(prev => ({ ...prev, varietyShows: false }));
}
} catch (error) {
// 静默处理错误
} finally {
setLoadingMore(prev => ({ ...prev, varietyShows: false }));
}
};
// 处理收藏数据更新的函数
const updateFavoriteItems = async (allFavorites: Record<string, Favorite>) => {
const allPlayRecords = await getAllPlayRecords();
@@ -292,7 +401,12 @@ function HomeClient() {
<ChevronRight className='w-4 h-4 ml-1' />
</Link>
</div>
<PaginatedRow itemsPerPage={10}>
<PaginatedRow
itemsPerPage={10}
onLoadMore={loadMoreMovies}
hasMoreData={hasMoreData.movies}
isLoading={loadingMore.movies}
>
{loading
? // 加载状态显示灰色占位数据 (显示10个,2行x5列)
Array.from({ length: 10 }).map((_, index) => (
@@ -340,7 +454,12 @@ function HomeClient() {
<ChevronRight className='w-4 h-4 ml-1' />
</Link>
</div>
<PaginatedRow itemsPerPage={10}>
<PaginatedRow
itemsPerPage={10}
onLoadMore={loadMoreTvShows}
hasMoreData={hasMoreData.tvShows}
isLoading={loadingMore.tvShows}
>
{loading
? // 加载状态显示灰色占位数据 (显示10个,2行x5列)
Array.from({ length: 10 }).map((_, index) => (
@@ -387,7 +506,12 @@ function HomeClient() {
<ChevronRight className='w-4 h-4 ml-1' />
</Link>
</div>
<PaginatedRow itemsPerPage={10}>
<PaginatedRow
itemsPerPage={10}
onLoadMore={loadMoreVarietyShows}
hasMoreData={hasMoreData.varietyShows}
isLoading={loadingMore.varietyShows}
>
{loading
? // 加载状态显示灰色占位数据 (显示10个,2行x5列)
Array.from({ length: 10 }).map((_, index) => (
+37 -13
View File
@@ -7,12 +7,18 @@ interface PaginatedRowProps {
children: React.ReactNode[];
itemsPerPage?: number;
className?: string;
onLoadMore?: () => Promise<void>; // 新增:加载更多数据的回调函数
hasMoreData?: boolean; // 新增:是否还有更多数据可加载
isLoading?: boolean; // 新增:是否正在加载中
}
export default function PaginatedRow({
children,
itemsPerPage = 10,
className = '',
onLoadMore,
hasMoreData = true,
isLoading = false,
}: PaginatedRowProps) {
const [startIndex, setStartIndex] = useState(0);
const [isHovered, setIsHovered] = useState(false);
@@ -40,19 +46,32 @@ export default function PaginatedRow({
});
};
// 向后翻页 - 真正的无限浏览
const handleNextPage = () => {
setStartIndex((prev) => {
const newIndex = prev + itemsPerPage;
// 当超出总长度时,从头开始,实现无限循环
return newIndex >= children.length ? 0 : newIndex;
});
// 向后翻页 - 支持动态加载更多数据
const handleNextPage = async () => {
const newIndex = startIndex + itemsPerPage;
// 如果即将超出当前数据范围,且有更多数据可加载,且有加载回调函数
if (newIndex >= children.length && hasMoreData && onLoadMore && !isLoading) {
try {
await onLoadMore(); // 加载更多数据
// 加载完成后,直接设置到下一页
setStartIndex(newIndex);
} catch (error) {
// 静默处理加载错误,保持用户体验
}
} else if (newIndex < children.length) {
// 如果还在当前数据范围内,直接翻页
setStartIndex(newIndex);
} else {
// 如果没有更多数据可加载,循环回到第一页
setStartIndex(0);
}
};
// 检查是否可以向前翻页
const canGoPrev = startIndex > 0;
// 总是可以向后翻页(无限循环)
const canGoNext = children.length > itemsPerPage;
// 检查是否可以向后翻页:有更多数据或者当前不在最后一页
const canGoNext = children.length > itemsPerPage && (startIndex + itemsPerPage < children.length || hasMoreData || startIndex + itemsPerPage >= children.length);
// 如果没有足够的内容需要分页,就不显示按钮
const needsPagination = children.length > itemsPerPage;
@@ -88,20 +107,25 @@ export default function PaginatedRow({
</button>
)}
{/* 右箭头按钮 - 总是显示,支持无限循环 */}
{/* 右箭头按钮 - 总是显示,支持动态加载 */}
{canGoNext && (
<button
onClick={handleNextPage}
className={`absolute -right-12 z-20 w-10 h-10 bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 rounded-full shadow-lg hover:shadow-xl flex items-center justify-center transition-all duration-300 hover:scale-110 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:ring-offset-2 dark:focus:ring-offset-gray-900 ${
disabled={isLoading}
className={`absolute -right-12 z-20 w-10 h-10 bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 rounded-full shadow-lg hover:shadow-xl flex items-center justify-center transition-all duration-300 hover:scale-110 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:ring-offset-2 dark:focus:ring-offset-gray-900 disabled:opacity-50 disabled:cursor-not-allowed ${
isHovered ? 'opacity-100' : 'opacity-0'
}`}
style={{
// 确保按钮在两行中间
top: 'calc(50% - 20px)',
}}
aria-label='下一页'
aria-label={isLoading ? '加载中...' : '下一页'}
>
<ChevronRight className='w-5 h-5 text-white' />
{isLoading ? (
<div className='w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin' />
) : (
<ChevronRight className='w-5 h-5 text-white' />
)}
</button>
)}
</>