feat: 实现纵向模式和平铺模式的双向无限滚动

问题描述:
- 纵向模式下,用户向上滑动触发懒加载后,向下滑动无法回到之前的内容
- 原因:纵向模式未启用累积模式,且缺少向上滚动加载上一页的功能

解决方案:
实现类似社交媒体的双向无限滚动机制:
- 向下滚动到 60% 时自动加载下一页(新内容)
- 向上滚动到顶部 10% 时自动加载上一页(旧内容)
- 加载上一页后自动调整滚动位置,保持用户视图不跳动

技术实现:

1. usePagination.js
   - 将 VERTICAL 模式加入累积模式判断 (line 57)
   - 实现 loadPrevPage 方法,支持加载上一页 (lines 285-306)
   - 导出 loadPrevPage 供组件使用 (line 364)

2. VirtualizedFourRowGrid.js
   - 添加 loadPrevPage prop 和 previousScrollHeight ref
   - 合并双向滚动检测逻辑 (lines 67-102):
     * 向下滚动: scrollPercentage > 0.6 触发 loadNextPage
     * 向上滚动: scrollTop < clientHeight * 0.1 触发 loadPrevPage
   - 实现滚动位置保持机制 (lines 133-161):
     * 记录加载前的 scrollHeight
     * 加载完成后计算高度差
     * 调整 scrollTop += heightDifference 保持视图位置

3. DynamicNewsCard.js
   - 从 usePagination 获取 loadPrevPage
   - 传递给 EventScrollList 组件

4. EventScrollList.js
   - 接收并传递 loadPrevPage 到 VirtualizedFourRowGrid
   - 四排模式和纵向模式均支持双向滚动

影响范围:
- 纵向模式 (vertical mode)
- 平铺模式 (four-row mode)

测试建议:
1. 切换到纵向模式
2. 向下滚动观察是否自动加载下一页
3. 向上滚动到顶部观察是否:
   - 自动加载上一页
   - 滚动位置保持不变,内容不跳动
4. 切换到平铺模式验证双向滚动同样生效

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-11-05 09:48:01 +08:00
parent f919ce255a
commit c5b8fe91c3
4 changed files with 87 additions and 14 deletions

View File

@@ -34,11 +34,13 @@ const VirtualizedFourRowGrid = ({
getTimelineBoxStyle,
borderColor,
loadNextPage,
loadPrevPage, // 新增:加载上一页
hasMore,
loading,
}) => {
const parentRef = useRef(null);
const isLoadingMore = useRef(false); // 防止重复加载
const previousScrollHeight = useRef(0); // 记录加载前的滚动高度(用于位置保持)
// 滚动条颜色(主题适配)
const scrollbarTrackBg = useColorModeValue('#f1f1f1', '#2D3748');
@@ -62,30 +64,42 @@ const VirtualizedFourRowGrid = ({
overscan: 2, // 预加载2行上下各1行
});
// 无限滚动逻辑 - 监听滚动事件,到达底部加载下一页
// 双向无限滚动逻辑 - 监听滚动事件,到达底部加载下一页,到达顶部加载上一页
useEffect(() => {
const scrollElement = parentRef.current;
if (!scrollElement || !loadNextPage) return;
if (!scrollElement) return;
const handleScroll = async () => {
// 防止重复触发
if (isLoadingMore.current || !hasMore || loading) return;
if (isLoadingMore.current || loading) return;
const { scrollTop, scrollHeight, clientHeight } = scrollElement;
const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
// 滚动到 60% 时开始加载下一页(降低阈值,更早触发)
if (scrollPercentage > 0.6) {
console.log('%c📜 [无限滚动] 到达底部,加载下一页', 'color: #8B5CF6; font-weight: bold;');
// 向下滚动:滚动到 60% 时开始加载下一页
if (loadNextPage && hasMore && scrollPercentage > 0.6) {
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;
// 记录加载前的滚动高度(用于位置保持)
previousScrollHeight.current = scrollHeight;
await loadPrevPage();
isLoadingMore.current = false;
}
};
scrollElement.addEventListener('scroll', handleScroll);
return () => scrollElement.removeEventListener('scroll', handleScroll);
}, [loadNextPage, hasMore, loading]);
}, [loadNextPage, loadPrevPage, hasMore, loading]);
// 主动检测内容高度 - 如果内容不足以填满容器,主动加载下一页
useEffect(() => {
@@ -116,6 +130,36 @@ 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 renderLoadingIndicator = () => {
if (!hasMore) {