Files
vf_react/src/views/Community/components/DynamicNewsCard/VirtualizedFourRowGrid.js
2025-11-05 08:32:54 +08:00

212 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 (
<Center py={6}>
<Text color="gray.500" fontSize="sm">
已加载全部内容
</Text>
</Center>
);
}
if (loading) {
return (
<Center py={6}>
<VStack spacing={2}>
<Spinner size="md" color="blue.500" thickness="3px" />
<Text color="gray.500" fontSize="sm">
加载中...
</Text>
</VStack>
</Center>
);
}
return null;
};
return (
<Box
ref={parentRef}
overflowY="auto"
overflowX="hidden"
maxH="600px"
w="100%"
position="relative"
css={{
// 滚动条样式
'&::-webkit-scrollbar': {
width: '8px',
},
'&::-webkit-scrollbar-track': {
background: scrollbarTrackBg,
borderRadius: '10px',
},
'&::-webkit-scrollbar-thumb': {
background: scrollbarThumbBg,
borderRadius: '10px',
},
'&::-webkit-scrollbar-thumb:hover': {
background: scrollbarThumbHoverBg,
},
scrollBehavior: 'smooth',
WebkitOverflowScrolling: 'touch',
}}
>
{/* 虚拟滚动容器 + 底部加载指示器 */}
<Box position="relative" w="100%">
{/* 虚拟滚动内容 */}
<Box
position="relative"
w="100%"
h={`${rowVirtualizer.getTotalSize()}px`}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const rowEvents = rows[virtualRow.index];
return (
<Box
key={virtualRow.key}
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement}
position="absolute"
top={0}
left={0}
w="100%"
transform={`translateY(${virtualRow.start}px)`}
>
{/* 每行使用 Grid 横向排列4个卡片 */}
<Grid
templateColumns="repeat(4, 1fr)"
gap={4}
w="100%"
>
{rowEvents.map((event, colIndex) => (
<Box key={event.id}>
<DynamicNewsEventCard
event={event}
index={virtualRow.index * 4 + colIndex}
isFollowing={eventFollowStatus[event.id]?.isFollowing || false}
followerCount={eventFollowStatus[event.id]?.followerCount || event.follower_count || 0}
isSelected={selectedEvent?.id === event.id}
onEventClick={(clickedEvent) => {
onEventSelect(clickedEvent);
}}
onTitleClick={(e) => {
e.preventDefault();
e.stopPropagation();
onEventSelect(event);
}}
onToggleFollow={() => onToggleFollow?.(event.id)}
timelineStyle={getTimelineBoxStyle()}
borderColor={borderColor}
/>
</Box>
))}
</Grid>
</Box>
);
})}
</Box>
{/* 底部加载指示器 - 绝对定位在虚拟内容底部 */}
<Box
position="absolute"
top={`${rowVirtualizer.getTotalSize()}px`}
left={0}
right={0}
w="100%"
>
{renderLoadingIndicator()}
</Box>
</Box>
</Box>
);
};
export default VirtualizedFourRowGrid;