From c5b8fe91c3937c9e61f76f3c3f76c3b5b2057f70 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Wed, 5 Nov 2025 09:48:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E7=BA=B5=E5=90=91?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E5=92=8C=E5=B9=B3=E9=93=BA=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E7=9A=84=E5=8F=8C=E5=90=91=E6=97=A0=E9=99=90=E6=BB=9A=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题描述: - 纵向模式下,用户向上滑动触发懒加载后,向下滑动无法回到之前的内容 - 原因:纵向模式未启用累积模式,且缺少向上滚动加载上一页的功能 解决方案: 实现类似社交媒体的双向无限滚动机制: - 向下滚动到 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 --- .../Community/components/DynamicNewsCard.js | 4 +- .../DynamicNewsCard/EventScrollList.js | 9 ++- .../DynamicNewsCard/VirtualizedFourRowGrid.js | 58 ++++++++++++++++--- .../DynamicNewsCard/hooks/usePagination.js | 30 +++++++++- 4 files changed, 87 insertions(+), 14 deletions(-) diff --git a/src/views/Community/components/DynamicNewsCard.js b/src/views/Community/components/DynamicNewsCard.js index c59e0078..16c2f9b4 100644 --- a/src/views/Community/components/DynamicNewsCard.js +++ b/src/views/Community/components/DynamicNewsCard.js @@ -107,7 +107,8 @@ const DynamicNewsCard = forwardRef(({ isAccumulateMode, // 新增:是否累积模式 handlePageChange, handleModeToggle, - loadNextPage // 新增:加载下一页 + loadNextPage, // 新增:加载下一页 + loadPrevPage // 新增:加载上一页 } = usePagination({ allCachedEvents, total, @@ -244,6 +245,7 @@ const DynamicNewsCard = forwardRef(({ displayEvents={displayEvents} // 新增:累积显示的事件列表 isAccumulateMode={isAccumulateMode} // 新增:是否累积模式 loadNextPage={loadNextPage} // 新增:加载下一页 + loadPrevPage={loadPrevPage} // 新增:加载上一页 onFourRowEventClick={handleFourRowEventClick} // 新增:四排模式事件点击 selectedEvent={selectedEvent} onEventSelect={setSelectedEvent} diff --git a/src/views/Community/components/DynamicNewsCard/EventScrollList.js b/src/views/Community/components/DynamicNewsCard/EventScrollList.js index d47db8c2..10a8cc44 100644 --- a/src/views/Community/components/DynamicNewsCard/EventScrollList.js +++ b/src/views/Community/components/DynamicNewsCard/EventScrollList.js @@ -46,6 +46,7 @@ const EventScrollList = ({ displayEvents, // 累积显示的事件列表(四排模式用) isAccumulateMode, // 是否累积模式 loadNextPage, // 加载下一页(无限滚动) + loadPrevPage, // 加载上一页(双向无限滚动) onFourRowEventClick, // 四排模式事件点击回调(打开弹窗) selectedEvent, onEventSelect, @@ -270,7 +271,7 @@ const EventScrollList = ({ )} - {/* 模式3: 四排网格模式 - 使用虚拟滚动 + 无限滚动 */} + {/* 模式3: 四排网格模式 - 使用虚拟滚动 + 双向无限滚动 */} {mode === 'four-row' && ( @@ -289,7 +291,7 @@ const EventScrollList = ({ {/* 模式4: 纵向分栏模式 - 横向布局(时间在左,卡片在右) */} {mode === 'vertical' && ( - {/* 左侧:事件列表 (33.3%) - 使用虚拟滚动 + 无限滚动 */} + {/* 左侧:事件列表 (33.3%) - 使用虚拟滚动 + 双向无限滚动 */} diff --git a/src/views/Community/components/DynamicNewsCard/VirtualizedFourRowGrid.js b/src/views/Community/components/DynamicNewsCard/VirtualizedFourRowGrid.js index 9b941c75..74bb6d74 100644 --- a/src/views/Community/components/DynamicNewsCard/VirtualizedFourRowGrid.js +++ b/src/views/Community/components/DynamicNewsCard/VirtualizedFourRowGrid.js @@ -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) { diff --git a/src/views/Community/components/DynamicNewsCard/hooks/usePagination.js b/src/views/Community/components/DynamicNewsCard/hooks/usePagination.js index 7c734a6d..2ac070fb 100644 --- a/src/views/Community/components/DynamicNewsCard/hooks/usePagination.js +++ b/src/views/Community/components/DynamicNewsCard/hooks/usePagination.js @@ -53,8 +53,8 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t // 检查是否还有更多数据 const hasMore = cachedCount < total; - // 判断是否使用累积模式(四排模式) - const isAccumulateMode = mode === DISPLAY_MODES.FOUR_ROW; + // 判断是否使用累积模式(四排模式 + 纵向模式) + const isAccumulateMode = mode === DISPLAY_MODES.FOUR_ROW || mode === DISPLAY_MODES.VERTICAL; // 从缓存中切片获取当前页数据(过滤 null 占位符) const currentPageEvents = useMemo(() => { @@ -282,6 +282,29 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t } }, [currentPage, totalPages, loadingPage, handlePageChange]); + // 加载上一页(用于双向无限滚动) + const loadPrevPage = useCallback(async () => { + if (currentPage <= 1 || loadingPage !== null) { + logger.debug('DynamicNewsCard', '无法加载上一页', { + currentPage, + loadingPage, + reason: currentPage <= 1 ? '已是第一页' : '正在加载中' + }); + return Promise.resolve(false); // 已经是第一页或正在加载 + } + + const prevPage = currentPage - 1; + logger.debug('DynamicNewsCard', '懒加载:加载上一页', { currentPage, prevPage }); + + try { + await handlePageChange(prevPage); + return true; + } catch (error) { + logger.error('DynamicNewsCard', '懒加载上一页失败', error, { prevPage }); + return false; + } + }, [currentPage, loadingPage, handlePageChange]); + // 模式切换处理 const handleModeToggle = useCallback((newMode) => { if (newMode === mode) return; @@ -337,6 +360,7 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t // 方法 handlePageChange, handleModeToggle, - loadNextPage // 新增:加载下一页(用于无限滚动) + loadNextPage, // 新增:加载下一页(用于无限滚动) + loadPrevPage // 新增:加载上一页(用于双向无限滚动) }; };