2025-11-13 08:38:25 +00:00
|
|
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
|
|
|
|
|
|
|
|
interface UseInfiniteScrollOptions {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 是否有更多数据可以加载
|
|
|
|
|
|
*/
|
|
|
|
|
|
hasNextPage: boolean;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 是否正在加载
|
|
|
|
|
|
*/
|
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 加载下一页的函数
|
|
|
|
|
|
*/
|
|
|
|
|
|
fetchNextPage: () => void;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 触发加载的阈值(px),当距离容器底部多少像素时触发加载
|
|
|
|
|
|
*/
|
|
|
|
|
|
threshold?: number;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 是否启用(默认为true)
|
|
|
|
|
|
*/
|
|
|
|
|
|
enabled?: boolean;
|
2025-11-24 03:47:20 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* 是否有错误
|
|
|
|
|
|
*/
|
|
|
|
|
|
isError?: boolean;
|
2025-11-13 08:38:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 无限滚动Hook
|
|
|
|
|
|
* 支持向下滚动和向上滚动加载更多
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function useInfiniteScroll({
|
|
|
|
|
|
hasNextPage,
|
|
|
|
|
|
isLoading,
|
|
|
|
|
|
fetchNextPage,
|
|
|
|
|
|
threshold = 200,
|
|
|
|
|
|
enabled = true,
|
2025-11-24 03:47:20 +00:00
|
|
|
|
isError = false,
|
2025-11-13 08:38:25 +00:00
|
|
|
|
}: UseInfiniteScrollOptions) {
|
|
|
|
|
|
const [isFetching, setIsFetching] = useState(false);
|
|
|
|
|
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
|
|
|
|
|
const loadMoreRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
|
|
// 加载更多数据
|
|
|
|
|
|
const loadMore = useCallback(async () => {
|
2025-11-24 03:47:20 +00:00
|
|
|
|
// 如果有错误,不继续加载
|
|
|
|
|
|
if (!hasNextPage || isLoading || isFetching || isError) return;
|
2025-11-13 08:38:25 +00:00
|
|
|
|
|
|
|
|
|
|
setIsFetching(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
fetchNextPage();
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
// 延迟重置状态,避免快速重复触发
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
setIsFetching(false);
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
}
|
2025-11-24 03:47:20 +00:00
|
|
|
|
}, [hasNextPage, isLoading, isFetching, isError, fetchNextPage]);
|
2025-11-13 08:38:25 +00:00
|
|
|
|
|
|
|
|
|
|
// 设置Intersection Observer
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!enabled || !loadMoreRef.current) return;
|
|
|
|
|
|
|
|
|
|
|
|
const options = {
|
|
|
|
|
|
root: null,
|
|
|
|
|
|
rootMargin: `${threshold}px`,
|
|
|
|
|
|
threshold: 0.1,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
observerRef.current = new IntersectionObserver(
|
|
|
|
|
|
(entries) => {
|
|
|
|
|
|
const [entry] = entries;
|
|
|
|
|
|
if (entry.isIntersecting) {
|
|
|
|
|
|
loadMore();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
options
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const currentRef = loadMoreRef.current;
|
|
|
|
|
|
if (currentRef) {
|
|
|
|
|
|
observerRef.current.observe(currentRef);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
if (observerRef.current && currentRef) {
|
|
|
|
|
|
observerRef.current.unobserve(currentRef);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [enabled, threshold, loadMore]);
|
|
|
|
|
|
|
|
|
|
|
|
// 清理observer
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
if (observerRef.current) {
|
|
|
|
|
|
observerRef.current.disconnect();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
loadMoreRef,
|
|
|
|
|
|
isFetching,
|
|
|
|
|
|
loadMore,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|