From 67127aa61560ec05760dc8cfc89850ec671fbea0 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Wed, 5 Nov 2025 08:32:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9B=E5=BB=BA=E8=99=9A=E6=8B=9F?= =?UTF-8?q?=E5=8C=96=E5=9B=9B=E6=8E=92=E7=BD=91=E6=A0=BC=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DynamicNewsCard/VirtualizedFourRowGrid.js | 211 ++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 src/views/Community/components/DynamicNewsCard/VirtualizedFourRowGrid.js diff --git a/src/views/Community/components/DynamicNewsCard/VirtualizedFourRowGrid.js b/src/views/Community/components/DynamicNewsCard/VirtualizedFourRowGrid.js new file mode 100644 index 00000000..c6531d04 --- /dev/null +++ b/src/views/Community/components/DynamicNewsCard/VirtualizedFourRowGrid.js @@ -0,0 +1,211 @@ +// src/views/Community/components/DynamicNewsCard/VirtualizedFourRowGrid.js +// 四列纵向滚动虚拟化网格组件(每行4列,纵向滚动 + 无限滚动) + +import React, { useRef, useMemo, useEffect } from 'react'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import { Box, Grid, Spinner, Text, VStack, Center } from '@chakra-ui/react'; +import { useColorModeValue } from '@chakra-ui/react'; +import DynamicNewsEventCard from '../EventCard/DynamicNewsEventCard'; + +/** + * 四列纵向滚动虚拟化网格组件(支持无限滚动) + * @param {Object} props + * @param {Array} props.events - 事件列表(累积显示) + * @param {Object} props.selectedEvent - 当前选中的事件 + * @param {Function} props.onEventSelect - 事件选择回调 + * @param {Object} props.eventFollowStatus - 事件关注状态 + * @param {Function} props.onToggleFollow - 关注切换回调 + * @param {Function} props.getTimelineBoxStyle - 时间轴样式获取函数 + * @param {string} props.borderColor - 边框颜色 + * @param {Function} props.loadNextPage - 加载下一页(无限滚动) + * @param {boolean} props.hasMore - 是否还有更多数据 + * @param {boolean} props.loading - 加载状态 + */ +const VirtualizedFourRowGrid = ({ + events, + selectedEvent, + onEventSelect, + eventFollowStatus, + onToggleFollow, + getTimelineBoxStyle, + borderColor, + loadNextPage, + hasMore, + loading, +}) => { + const parentRef = useRef(null); + const isLoadingMore = useRef(false); // 防止重复加载 + + // 滚动条颜色(主题适配) + const scrollbarTrackBg = useColorModeValue('#f1f1f1', '#2D3748'); + const scrollbarThumbBg = useColorModeValue('#888', '#4A5568'); + const scrollbarThumbHoverBg = useColorModeValue('#555', '#718096'); + + // 将事件按4个一组分成行(每行4列) + const rows = useMemo(() => { + const r = []; + for (let i = 0; i < events.length; i += 4) { + r.push(events.slice(i, i + 4)); + } + return r; + }, [events]); + + // 配置虚拟滚动器(纵向滚动 + 动态高度测量) + const rowVirtualizer = useVirtualizer({ + count: rows.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 250, // 提供初始估算值,库会自动测量实际高度 + overscan: 2, // 预加载2行(上下各1行) + }); + + // 无限滚动逻辑 - 监听滚动事件,到达底部时加载下一页 + useEffect(() => { + const scrollElement = parentRef.current; + if (!scrollElement || !loadNextPage) return; + + const handleScroll = async () => { + // 防止重复触发 + if (isLoadingMore.current || !hasMore || loading) return; + + const { scrollTop, scrollHeight, clientHeight } = scrollElement; + const scrollPercentage = (scrollTop + clientHeight) / scrollHeight; + + // 滚动到 80% 时开始加载下一页 + if (scrollPercentage > 0.8) { + console.log('%c📜 [无限滚动] 到达底部,加载下一页', 'color: #8B5CF6; font-weight: bold;'); + isLoadingMore.current = true; + await loadNextPage(); + isLoadingMore.current = false; + } + }; + + scrollElement.addEventListener('scroll', handleScroll); + return () => scrollElement.removeEventListener('scroll', handleScroll); + }, [loadNextPage, hasMore, loading]); + + // 底部加载指示器 + const renderLoadingIndicator = () => { + if (!hasMore) { + return ( +
+ + 已加载全部内容 + +
+ ); + } + if (loading) { + return ( +
+ + + + 加载中... + + +
+ ); + } + return null; + }; + + return ( + + {/* 虚拟滚动容器 + 底部加载指示器 */} + + {/* 虚拟滚动内容 */} + + {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const rowEvents = rows[virtualRow.index]; + + return ( + + {/* 每行使用 Grid 横向排列4个卡片 */} + + {rowEvents.map((event, colIndex) => ( + + { + onEventSelect(clickedEvent); + }} + onTitleClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + onEventSelect(event); + }} + onToggleFollow={() => onToggleFollow?.(event.id)} + timelineStyle={getTimelineBoxStyle()} + borderColor={borderColor} + /> + + ))} + + + ); + })} + + + {/* 底部加载指示器 - 绝对定位在虚拟内容底部 */} + + {renderLoadingIndicator()} + + + + ); +}; + +export default VirtualizedFourRowGrid;