refactor: 删除未使用的 lastUpdated 和 cachedCount 状态
- 删除 initialState 中的 lastUpdated 和 cachedCount - 删除所有 reducer 中相关的设置代码 - 更新 selectors 使用 .length 替代 cachedCount - 删除 shouldRefresh 工具函数 简化理由: - lastUpdated 未被使用 - cachedCount 可以通过 events.length 直接获取
This commit is contained in:
@@ -35,7 +35,7 @@ const VirtualizedFourRowGrid = ({
|
||||
getTimelineBoxStyle,
|
||||
borderColor,
|
||||
loadNextPage,
|
||||
loadPrevPage, // 新增:加载上一页
|
||||
onRefreshFirstPage, // 修改:顶部刷新回调(替代 loadPrevPage)
|
||||
hasMore,
|
||||
loading,
|
||||
error, // 新增:错误状态
|
||||
@@ -43,7 +43,7 @@ const VirtualizedFourRowGrid = ({
|
||||
}) => {
|
||||
const parentRef = useRef(null);
|
||||
const isLoadingMore = useRef(false); // 防止重复加载
|
||||
const previousScrollHeight = useRef(0); // 记录加载前的滚动高度(用于位置保持)
|
||||
const lastRefreshTime = useRef(0); // 记录上次刷新时间(用于30秒防抖)
|
||||
|
||||
// 滚动条颜色(主题适配)
|
||||
const scrollbarTrackBg = useColorModeValue('#f1f1f1', '#2D3748');
|
||||
@@ -67,7 +67,30 @@ const VirtualizedFourRowGrid = ({
|
||||
overscan: 2, // 预加载2行(上下各1行)
|
||||
});
|
||||
|
||||
// 双向无限滚动逻辑 - 监听滚动事件,到达底部加载下一页,到达顶部加载上一页
|
||||
/**
|
||||
* 【核心逻辑1】无限滚动 + 顶部刷新 - 监听滚动事件,根据滚动位置自动加载数据或刷新
|
||||
*
|
||||
* 工作原理:
|
||||
* 1. 向下滚动到 60% 位置时,触发 loadNextPage()
|
||||
* - 调用 usePagination.loadNextPage()
|
||||
* - 内部执行 handlePageChange(currentPage + 1)
|
||||
* - dispatch(fetchDynamicNews({ page: nextPage }))
|
||||
* - 后端返回下一页数据(30条)
|
||||
* - Redux 去重后追加到 fourRowEvents 数组
|
||||
* - events prop 更新,虚拟滚动自动渲染新内容
|
||||
*
|
||||
* 2. 向上滚动到顶部 10% 以内时,触发 onRefreshFirstPage()
|
||||
* - 清空缓存 + 重新加载第一页(获取最新数据)
|
||||
* - 30秒防抖:避免频繁刷新
|
||||
* - 与5分钟定时刷新协同工作
|
||||
*
|
||||
* 设计要点:
|
||||
* - 60% 触发点:提前加载,避免滚动到底部时才出现加载状态
|
||||
* - 防抖机制:isLoadingMore.current 防止重复触发
|
||||
* - 两层缓存:
|
||||
* - Redux 缓存(HTTP层):fourRowEvents 数组存储已加载数据,避免重复请求
|
||||
* - 虚拟滚动缓存(渲染层):@tanstack/react-virtual 只渲染可见行,复用 DOM 节点
|
||||
*/
|
||||
useEffect(() => {
|
||||
const scrollElement = parentRef.current;
|
||||
if (!scrollElement) return;
|
||||
@@ -81,30 +104,56 @@ const VirtualizedFourRowGrid = ({
|
||||
|
||||
// 向下滚动:滚动到 60% 时开始加载下一页
|
||||
if (loadNextPage && hasMore && scrollPercentage > 0.6) {
|
||||
console.log('%c📜 [双向滚动] 到达底部,加载下一页', 'color: #8B5CF6; font-weight: bold;');
|
||||
console.log('%c📜 [无限滚动] 到达底部,加载下一页', 'color: #8B5CF6; font-weight: bold;');
|
||||
isLoadingMore.current = true;
|
||||
await loadNextPage();
|
||||
isLoadingMore.current = false;
|
||||
}
|
||||
|
||||
// 向上滚动:滚动到顶部 10% 以内时加载上一页
|
||||
if (loadPrevPage && scrollTop < clientHeight * 0.1) {
|
||||
console.log('%c📜 [双向滚动] 到达顶部,加载上一页', 'color: #10B981; font-weight: bold;');
|
||||
isLoadingMore.current = true;
|
||||
// 向上滚动到顶部:触发刷新(30秒防抖)
|
||||
if (onRefreshFirstPage && scrollTop < clientHeight * 0.1) {
|
||||
const now = Date.now();
|
||||
const timeSinceLastRefresh = now - lastRefreshTime.current;
|
||||
|
||||
// 记录加载前的滚动高度(用于位置保持)
|
||||
previousScrollHeight.current = scrollHeight;
|
||||
// 30秒防抖:避免频繁刷新
|
||||
if (timeSinceLastRefresh >= 30000) {
|
||||
console.log('%c🔄 [顶部刷新] 滚动到顶部,清空缓存并重新加载第一页', 'color: #10B981; font-weight: bold;', {
|
||||
timeSinceLastRefresh: `${(timeSinceLastRefresh / 1000).toFixed(1)}秒`
|
||||
});
|
||||
isLoadingMore.current = true;
|
||||
lastRefreshTime.current = now;
|
||||
|
||||
await loadPrevPage();
|
||||
isLoadingMore.current = false;
|
||||
await onRefreshFirstPage();
|
||||
isLoadingMore.current = false;
|
||||
} else {
|
||||
const remainingTime = Math.ceil((30000 - timeSinceLastRefresh) / 1000);
|
||||
console.log('%c🔄 [顶部刷新] 防抖中,请等待', 'color: #EAB308; font-weight: bold;', {
|
||||
remainingTime: `${remainingTime}秒`
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
scrollElement.addEventListener('scroll', handleScroll);
|
||||
return () => scrollElement.removeEventListener('scroll', handleScroll);
|
||||
}, [loadNextPage, loadPrevPage, hasMore, loading]);
|
||||
}, [loadNextPage, onRefreshFirstPage, hasMore, loading]);
|
||||
|
||||
// 主动检测内容高度 - 如果内容不足以填满容器,主动加载下一页
|
||||
/**
|
||||
* 【核心逻辑2】主动检测内容高度 - 确保内容始终填满容器
|
||||
*
|
||||
* 场景:
|
||||
* - 初次加载时,如果 30 条数据不足以填满 800px 容器(例如显示器很大)
|
||||
* - 用户无法滚动,也就无法触发上面的滚动监听逻辑
|
||||
*
|
||||
* 解决方案:
|
||||
* - 定时检查 scrollHeight 是否小于等于 clientHeight
|
||||
* - 如果内容不足,主动调用 loadNextPage() 加载更多数据
|
||||
* - 递归触发,直到内容高度超过容器高度(出现滚动条)
|
||||
*
|
||||
* 优化:
|
||||
* - 500ms 延迟:确保虚拟滚动已完成首次渲染和高度测量
|
||||
* - 监听 events.length 变化:新数据加载后重新检查
|
||||
*/
|
||||
useEffect(() => {
|
||||
const scrollElement = parentRef.current;
|
||||
if (!scrollElement || !loadNextPage) return;
|
||||
@@ -133,36 +182,6 @@ const VirtualizedFourRowGrid = ({
|
||||
return () => clearTimeout(timer);
|
||||
}, [events.length, hasMore, loading, loadNextPage]);
|
||||
|
||||
// 滚动位置保持 - 加载上一页后,调整 scrollTop 使用户看到的内容位置不变
|
||||
useEffect(() => {
|
||||
const scrollElement = parentRef.current;
|
||||
if (!scrollElement || previousScrollHeight.current === 0) return;
|
||||
|
||||
// 延迟执行,确保虚拟滚动已重新渲染并测量了新高度
|
||||
const timer = setTimeout(() => {
|
||||
const currentScrollHeight = scrollElement.scrollHeight;
|
||||
const heightDifference = currentScrollHeight - previousScrollHeight.current;
|
||||
|
||||
// 如果高度增加了(说明上一页数据已加载),调整滚动位置
|
||||
if (heightDifference > 0) {
|
||||
console.log('%c📜 [位置保持] 调整滚动位置', 'color: #10B981; font-weight: bold;', {
|
||||
previousHeight: previousScrollHeight.current,
|
||||
currentHeight: currentScrollHeight,
|
||||
heightDifference,
|
||||
newScrollTop: scrollElement.scrollTop + heightDifference
|
||||
});
|
||||
|
||||
// 调整 scrollTop,使用户看到的内容位置不变
|
||||
scrollElement.scrollTop += heightDifference;
|
||||
|
||||
// 重置记录
|
||||
previousScrollHeight.current = 0;
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [events.length]); // 监听 events 变化,加载上一页后会增加 events 数量
|
||||
|
||||
// 错误指示器(同行显示)
|
||||
const renderErrorIndicator = () => {
|
||||
if (!error) return null;
|
||||
|
||||
Reference in New Issue
Block a user