104 lines
2.7 KiB
React
104 lines
2.7 KiB
React
|
|
import { useEffect, useRef, useState, useCallback } from "react";
|
|||
|
|
import { Empty, Spin } from "@arco-design/web-react";
|
|||
|
|
import "./index.css";
|
|||
|
|
|
|||
|
|
const InfiniteScroll = ({
|
|||
|
|
loadMore,
|
|||
|
|
hasMore,
|
|||
|
|
children,
|
|||
|
|
threshold = 0.1, // 触发阈值,元素可见比例
|
|||
|
|
className = "",
|
|||
|
|
empty = false,
|
|||
|
|
}) => {
|
|||
|
|
const containerRef = useRef(null);
|
|||
|
|
const sentinelRef = useRef(null);
|
|||
|
|
const observerRef = useRef(null);
|
|||
|
|
const throttleRef = useRef(null); // 节流控制
|
|||
|
|
const [loading, setLoading] = useState(false);
|
|||
|
|
const [hasInitialized, setHasInitialized] = useState(false); // 首次挂载
|
|||
|
|
|
|||
|
|
// 加载更多数据的处理函数(带节流)
|
|||
|
|
const handleLoadMore = useCallback(() => {
|
|||
|
|
if (loading || !hasMore) return;
|
|||
|
|
|
|||
|
|
// 节流处理:500ms内只能触发一次
|
|||
|
|
if (throttleRef.current) {
|
|||
|
|
clearTimeout(throttleRef.current);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
throttleRef.current = setTimeout(() => {
|
|||
|
|
setLoading(true);
|
|||
|
|
loadMore().finally(() => {
|
|||
|
|
setLoading(false);
|
|||
|
|
throttleRef.current = null;
|
|||
|
|
});
|
|||
|
|
}, 10);
|
|||
|
|
}, [hasMore, loadMore, loading]);
|
|||
|
|
|
|||
|
|
// 设置IntersectionObserver
|
|||
|
|
useEffect(() => {
|
|||
|
|
// 初始化观察器
|
|||
|
|
const options = {
|
|||
|
|
root: containerRef.current,
|
|||
|
|
rootMargin: "0px 0px 50px 0px", // 减少提前触发距离
|
|||
|
|
threshold,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 创建观察器实例
|
|||
|
|
observerRef.current = new IntersectionObserver((entries) => {
|
|||
|
|
const [entry] = entries;
|
|||
|
|
if (entry.isIntersecting) {
|
|||
|
|
handleLoadMore();
|
|||
|
|
}
|
|||
|
|
}, options);
|
|||
|
|
|
|||
|
|
// 开始观察哨兵元素
|
|||
|
|
if (sentinelRef.current) {
|
|||
|
|
observerRef.current.observe(sentinelRef.current);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始加载检查 - 仅在组件首次挂载时执行
|
|||
|
|
if (hasMore && !loading && !hasInitialized) {
|
|||
|
|
setHasInitialized(true);
|
|||
|
|
handleLoadMore();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清理函数
|
|||
|
|
return () => {
|
|||
|
|
if (observerRef.current) {
|
|||
|
|
observerRef.current.disconnect();
|
|||
|
|
}
|
|||
|
|
// 清理节流定时器
|
|||
|
|
if (throttleRef.current) {
|
|||
|
|
clearTimeout(throttleRef.current);
|
|||
|
|
throttleRef.current = null;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
}, [loadMore, hasMore, threshold, loading, hasInitialized, handleLoadMore]);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
ref={containerRef}
|
|||
|
|
className={`infinite-scroll-container ${className}`}
|
|||
|
|
>
|
|||
|
|
{children}
|
|||
|
|
|
|||
|
|
{/* 哨兵元素 - 用于触发加载更多 */}
|
|||
|
|
<div ref={sentinelRef} className="sentinel-element"></div>
|
|||
|
|
|
|||
|
|
{!hasMore && empty && (
|
|||
|
|
<Empty description="暂无数据" className="empty-data" />
|
|||
|
|
)}
|
|||
|
|
{/* 滚动加载指示器 */}
|
|||
|
|
{loading && hasMore && (
|
|||
|
|
<div className="loading-container">
|
|||
|
|
<Spin size={20} />
|
|||
|
|
<span className="loading-text">加载中...</span>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default InfiniteScroll;
|