diff --git a/src/views/Community/components/DynamicNews/DynamicNewsCard.js b/src/views/Community/components/DynamicNews/DynamicNewsCard.js
index 6a027793..5042088b 100644
--- a/src/views/Community/components/DynamicNews/DynamicNewsCard.js
+++ b/src/views/Community/components/DynamicNews/DynamicNewsCard.js
@@ -81,7 +81,7 @@ const DynamicNewsCardComponent = forwardRef(({
// Refs
const cardHeaderRef = useRef(null);
const cardBodyRef = useRef(null);
- const virtualizedGridRef = useRef(null); // ⚡ VirtualizedFourRowGrid 的 ref(用于获取滚动位置)
+ const mainlineRef = useRef(null); // MainlineTimelineView 的 ref
// 从 Redux 读取关注状态
const eventFollowStatus = useSelector(selectEventFollowStatus);
@@ -107,9 +107,9 @@ const [currentMode, setCurrentMode] = useState('vertical');
// 根据模式选择数据源(使用 useMemo 缓存,避免重复计算)
// 纵向模式:data 是页码映射 { 1: [...], 2: [...] }
- // 平铺模式 / 主线模式:data 是数组 [...] (共用 fourRowData)
+ // 主线模式:使用独立 API,不需要 Redux 数据
const modeData = useMemo(
- () => (currentMode === 'four-row' || currentMode === 'mainline') ? fourRowData : verticalData,
+ () => currentMode === 'mainline' ? fourRowData : verticalData,
[currentMode, fourRowData, verticalData]
);
const {
@@ -128,7 +128,7 @@ const [currentMode, setCurrentMode] = useState('vertical');
[currentMode, data]
);
const allCachedEvents = useMemo(
- () => (currentMode === 'four-row' || currentMode === 'mainline') ? data : undefined,
+ () => currentMode === 'mainline' ? data : undefined,
[currentMode, data]
);
@@ -243,14 +243,14 @@ const [currentMode, setCurrentMode] = useState('vertical');
} else {
console.log(`[DynamicNewsCard] 纵向模式 + 第${state.currentPage}页 → 不刷新(避免打断用户)`);
}
- } else if (mode === 'four-row' || mode === 'mainline') {
- // ========== 平铺模式 / 主线模式 ==========
+ } else if (mode === 'mainline') {
+ // ========== 主线模式 ==========
// 检查滚动位置,只有在顶部时才刷新
- const scrollPos = virtualizedGridRef.current?.getScrollPosition();
+ const scrollPos = mainlineRef.current?.getScrollPosition();
if (scrollPos?.isNearTop) {
// 用户在顶部 10% 区域,安全刷新
- console.log(`[DynamicNewsCard] ${mode === 'mainline' ? '主线' : '平铺'}模式 + 滚动在顶部 → 刷新列表`);
+ console.log(`[DynamicNewsCard] 主线模式 + 滚动在顶部 → 刷新列表`);
handlePageChange(1); // 清空并刷新
toast({
title: '检测到新事件,已刷新',
@@ -346,9 +346,9 @@ const [currentMode, setCurrentMode] = useState('vertical');
}
}, [error, toast]);
- // 四排模式的事件点击处理(打开弹窗)
- const handleFourRowEventClick = useCallback((event) => {
- console.log('%c🔲 [四排模式] 点击事件,打开详情弹窗', 'color: #8B5CF6; font-weight: bold;', { eventId: event.id, title: event.title });
+ // 主线模式的事件点击处理(打开弹窗)
+ const handleMainlineEventClick = useCallback((event) => {
+ console.log('%c🔲 [主线模式] 点击事件,打开详情弹窗', 'color: #8B5CF6; font-weight: bold;', { eventId: event.id, title: event.title });
// 🎯 追踪事件详情打开
if (trackingFunctions.trackNewsDetailOpened) {
@@ -356,7 +356,7 @@ const [currentMode, setCurrentMode] = useState('vertical');
eventId: event.id,
eventTitle: event.title,
importance: event.importance,
- source: 'four_row_mode',
+ source: 'mainline_mode',
displayMode: 'modal',
timestamp: new Date().toISOString(),
});
@@ -456,10 +456,13 @@ const [currentMode, setCurrentMode] = useState('vertical');
if (hasInitialized.current && isDataEmpty) {
console.log(`%c🔄 [模式切换] ${mode} 模式数据为空,开始加载`, 'color: #8B5CF6; font-weight: bold;');
- // 🔧 根据 mode 直接计算 per_page
- const modePageSize = mode === DISPLAY_MODES.FOUR_ROW
- ? PAGINATION_CONFIG.FOUR_ROW_PAGE_SIZE // 30
- : PAGINATION_CONFIG.VERTICAL_PAGE_SIZE; // 10
+ // 主线模式使用独立 API,不需要通过 Redux 加载
+ if (mode === DISPLAY_MODES.MAINLINE) {
+ console.log('%c 主线模式 - 由 MainlineTimelineView 自己加载数据', 'color: #8B5CF6;');
+ return;
+ }
+
+ const modePageSize = PAGINATION_CONFIG.VERTICAL_PAGE_SIZE; // 10
console.log(`%c 计算的 per_page: ${modePageSize} (mode: ${mode})`, 'color: #8B5CF6;');
@@ -468,8 +471,8 @@ const [currentMode, setCurrentMode] = useState('vertical');
per_page: modePageSize,
pageSize: modePageSize,
clearCache: true,
- ...filters, // 先展开筛选条件
- page: PAGINATION_CONFIG.INITIAL_PAGE, // 然后覆盖 page 参数
+ ...filters,
+ page: PAGINATION_CONFIG.INITIAL_PAGE,
}));
}
}, [mode, currentMode, allCachedEventsByPage, allCachedEvents, dispatch, filters]); // 添加 filters 依赖
@@ -664,11 +667,8 @@ const [currentMode, setCurrentMode] = useState('vertical');
{/* 列表内容 - 始终渲染 */}
diff --git a/src/views/Community/components/DynamicNews/EventScrollList.js b/src/views/Community/components/DynamicNews/EventScrollList.js
index 27f04e3a..6d64cf5c 100644
--- a/src/views/Community/components/DynamicNews/EventScrollList.js
+++ b/src/views/Community/components/DynamicNews/EventScrollList.js
@@ -1,20 +1,16 @@
// src/views/Community/components/DynamicNews/EventScrollList.js
-// 横向滚动事件列表组件
+// 事件列表组件
import React, { useRef, useCallback } from "react";
import { Box, useColorModeValue } from "@chakra-ui/react";
-import VirtualizedFourRowGrid from "./layouts/VirtualizedFourRowGrid";
import MainlineTimelineView from "./layouts/MainlineTimelineView";
import VerticalModeLayout from "./layouts/VerticalModeLayout";
/**
- * 事件列表组件 - 支持纵向、平铺、主线三种展示模式
+ * 事件列表组件 - 支持纵向、主线两种展示模式
* @param {Array} events - 当前页的事件列表(服务端已分页)
- * @param {Array} displayEvents - 累积显示的事件列表(平铺模式用)
* @param {Object} filters - 筛选条件(主线模式用)
- * @param {Function} loadNextPage - 加载下一页(无限滚动)
- * @param {Function} loadPrevPage - 加载上一页(双向无限滚动)
- * @param {Function} onFourRowEventClick - 平铺/主线模式事件点击回调(打开弹窗)
+ * @param {Function} onMainlineEventClick - 主线模式事件点击回调(打开弹窗)
* @param {Object} selectedEvent - 当前选中的事件
* @param {Function} onEventSelect - 事件选择回调
* @param {string} borderColor - 边框颜色
@@ -23,20 +19,16 @@ import VerticalModeLayout from "./layouts/VerticalModeLayout";
* @param {Function} onPageChange - 页码改变回调
* @param {boolean} loading - 全局加载状态
* @param {Object} error - 错误状态
- * @param {string} mode - 展示模式:'vertical'(纵向分栏)| 'four-row'(平铺网格)| 'mainline'(主线时间轴)
- * @param {boolean} hasMore - 是否还有更多数据
+ * @param {string} mode - 展示模式:'vertical'(纵向分栏)| 'mainline'(主线时间轴)
* @param {Object} eventFollowStatus - 事件关注状态 { [eventId]: { isFollowing, followerCount } }
* @param {Function} onToggleFollow - 关注按钮回调
- * @param {React.Ref} virtualizedGridRef - VirtualizedFourRowGrid/MainlineTimelineView 的 ref
+ * @param {React.Ref} mainlineRef - MainlineTimelineView 的 ref
*/
const EventScrollList = React.memo(
({
events,
- displayEvents,
filters = {},
- loadNextPage,
- loadPrevPage,
- onFourRowEventClick,
+ onMainlineEventClick,
selectedEvent,
onEventSelect,
borderColor,
@@ -46,23 +38,17 @@ const EventScrollList = React.memo(
loading = false,
error,
mode = "vertical",
- hasMore = true,
eventFollowStatus = {},
onToggleFollow,
- virtualizedGridRef,
+ mainlineRef,
}) => {
const scrollContainerRef = useRef(null);
- // 所有 useColorModeValue 必须在组件顶层调用(不能在条件渲染中)
+ // 所有 useColorModeValue 必须在组件顶层调用
const timelineBg = useColorModeValue("gray.50", "gray.700");
const timelineBorderColor = useColorModeValue("gray.400", "gray.500");
const timelineTextColor = useColorModeValue("blue.600", "blue.400");
- // 滚动条颜色
- const scrollbarTrackBg = useColorModeValue("#f1f1f1", "#2D3748");
- const scrollbarThumbBg = useColorModeValue("#888", "#4A5568");
- const scrollbarThumbHoverBg = useColorModeValue("#555", "#718096");
-
const getTimelineBoxStyle = () => {
return {
bg: timelineBg,
@@ -73,75 +59,24 @@ const EventScrollList = React.memo(
};
};
- // 重试函数
- const handleRetry = useCallback(() => {
- if (onPageChange) {
- onPageChange(currentPage);
- }
- }, [onPageChange, currentPage]);
-
- {
- /* 事件卡片容器 */
- }
return (
- {/* 平铺网格模式 - 使用虚拟滚动 + 双向无限滚动 */}
-
-
{/* 主线时间轴模式 - 按 lv2 概念分组,调用独立 API */}
{
- const parentRef = useRef(null);
- const isLoadingMore = useRef(false); // 防止重复加载
- const lastRefreshTime = useRef(0); // 记录上次刷新时间(用于30秒防抖)
-
- // 滚动条颜色(主题适配)
- const scrollbarTrackBg = useColorModeValue('#f1f1f1', '#2D3748');
- const scrollbarThumbBg = useColorModeValue('#888', '#4A5568');
- const scrollbarThumbHoverBg = useColorModeValue('#555', '#718096');
-
- // 响应式列数
- const responsiveColumns = useBreakpointValue({
- base: 1, // 移动端:单列
- sm: 2, // 小屏:2列
- md: 2, // 中屏:2列
- lg: 3, // 大屏:3列
- xl: 4, // 超大屏:4列
- });
-
- // 使用响应式列数或传入的列数
- const actualColumnsPerRow = responsiveColumns || columnsPerRow;
-
- // 将事件按 actualColumnsPerRow 个一组分成行
- const rows = useMemo(() => {
- const r = [];
- for (let i = 0; i < events.length; i += actualColumnsPerRow) {
- r.push(events.slice(i, i + actualColumnsPerRow));
- }
- return r;
- }, [events, actualColumnsPerRow]);
-
- // 配置虚拟滚动器(纵向滚动 + 动态高度测量)
- const rowVirtualizer = useVirtualizer({
- count: rows.length,
- getScrollElement: () => parentRef.current,
- estimateSize: () => 250, // 提供初始估算值,库会自动测量实际高度
- overscan: 2, // 预加载2行(上下各1行)
- });
-
- /**
- * ⚡ 暴露方法给父组件(用于 Socket 刷新判断)
- */
- useImperativeHandle(ref, () => ({
- /**
- * 获取当前滚动位置信息
- * @returns {Object|null} 滚动位置信息
- */
- getScrollPosition: () => {
- const scrollElement = parentRef.current;
- if (!scrollElement) return null;
-
- const { scrollTop, scrollHeight, clientHeight } = scrollElement;
- const isNearTop = scrollTop < clientHeight * 0.1; // 顶部 10% 区域
-
- return {
- scrollTop,
- scrollHeight,
- clientHeight,
- isNearTop,
- scrollPercentage: ((scrollTop + clientHeight) / scrollHeight) * 100,
- };
- },
- }), []);
-
- /**
- * 【核心逻辑1】无限滚动 + 顶部刷新 - 监听滚动事件,根据滚动位置自动加载数据或刷新
- *
- * 工作原理:
- * 1. 向下滚动到 90% 位置时,触发 loadNextPage()
- * - 调用 usePagination.loadNextPage()
- * - 内部执行 handlePageChange(currentPage + 1)
- * - dispatch(fetchDynamicNews({ page: nextPage }))
- * - 后端返回下一页数据(30条)
- * - Redux 去重后追加到 fourRowEvents 数组
- * - events prop 更新,虚拟滚动自动渲染新内容
- *
- * 2. 向上滚动到顶部 10% 以内时,触发 onRefreshFirstPage()
- * - 清空缓存 + 重新加载第一页(获取最新数据)
- * - 30秒防抖:避免频繁刷新
- * - 与5分钟定时刷新协同工作
- *
- * 设计要点:
- * - 90% 触发点:接近底部才加载,避免过早触发影响用户体验
- * - 防抖机制:isLoadingMore.current 防止重复触发
- * - 两层缓存:
- * - Redux 缓存(HTTP层):fourRowEvents 数组存储已加载数据,避免重复请求
- * - 虚拟滚动缓存(渲染层):@tanstack/react-virtual 只渲染可见行,复用 DOM 节点
- */
- useEffect(() => {
- // 如果组件被隐藏,不执行滚动监听
- if (display === 'none') return;
-
- const scrollElement = parentRef.current;
- if (!scrollElement) return;
-
- const handleScroll = async () => {
- // 防止重复触发
- if (isLoadingMore.current || loading) return;
-
- const { scrollTop, scrollHeight, clientHeight } = scrollElement;
- const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
-
- // 向下滚动:滚动到 90% 时开始加载下一页(更接近底部,避免过早触发)
- if (loadNextPage && hasMore && scrollPercentage > 0.9) {
- console.log('%c📜 [无限滚动] 接近底部,加载下一页', 'color: #8B5CF6; font-weight: bold;');
- isLoadingMore.current = true;
- await loadNextPage();
- isLoadingMore.current = false;
- }
-
- // 向上滚动到顶部:触发刷新(30秒防抖)
- if (onRefreshFirstPage && scrollTop < clientHeight * 0.1) {
- const now = Date.now();
- const timeSinceLastRefresh = now - lastRefreshTime.current;
-
- // 30秒防抖:避免频繁刷新
- if (timeSinceLastRefresh >= 30000) {
- console.log('%c🔄 [顶部刷新] 滚动到顶部,清空缓存并重新加载第一页', 'color: #10B981; font-weight: bold;', {
- timeSinceLastRefresh: `${(timeSinceLastRefresh / 1000).toFixed(1)}秒`
- });
- isLoadingMore.current = true;
- lastRefreshTime.current = now;
-
- 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);
- }, [display, loadNextPage, onRefreshFirstPage, hasMore, loading]);
-
- /**
- * 【核心逻辑2】主动检测内容高度 - 确保内容始终填满容器
- *
- * 场景:
- * - 初次加载时,如果 30 条数据不足以填满 800px 容器(例如显示器很大)
- * - 用户无法滚动,也就无法触发上面的滚动监听逻辑
- *
- * 解决方案:
- * - 定时检查 scrollHeight 是否小于等于 clientHeight
- * - 如果内容不足,主动调用 loadNextPage() 加载更多数据
- * - 递归触发,直到内容高度超过容器高度(出现滚动条)
- *
- * 优化:
- * - 500ms 延迟:确保虚拟滚动已完成首次渲染和高度测量
- * - 监听 events.length 变化:新数据加载后重新检查
- */
- useEffect(() => {
- // 如果组件被隐藏,不执行高度检测
- if (display === 'none') return;
-
- const scrollElement = parentRef.current;
- if (!scrollElement || !loadNextPage) return;
-
- // 延迟检查,确保虚拟滚动已渲染
- const timer = setTimeout(() => {
- // 防止重复触发
- if (isLoadingMore.current || !hasMore || loading) return;
-
- const { scrollHeight, clientHeight } = scrollElement;
-
- // 如果内容高度不足以填满容器(没有滚动条),主动加载下一页
- if (scrollHeight <= clientHeight) {
- console.log('%c📜 [无限滚动] 内容不足以填满容器,主动加载下一页', 'color: #8B5CF6; font-weight: bold;', {
- scrollHeight,
- clientHeight,
- eventsCount: events.length
- });
- isLoadingMore.current = true;
- loadNextPage().finally(() => {
- isLoadingMore.current = false;
- });
- }
- }, 500);
-
- return () => clearTimeout(timer);
- }, [display, events.length, hasMore, loading, loadNextPage]);
-
- // 错误指示器(同行显示)
- const renderErrorIndicator = () => {
- if (!error) return null;
-
- return (
-
-
-
- 数据加载失败,
-
- }
- size="sm"
- colorScheme="blue"
- variant="ghost"
- onClick={onRetry}
- aria-label="刷新"
- />
-
- 刷新
-
-
-
- );
- };
-
- // 底部加载指示器
- const renderLoadingIndicator = () => {
- if (!hasMore) {
- return (
-
-
- 已加载全部内容
-
-
- );
- }
- if (loading) {
- return (
-
-
-
-
- 加载中...
-
-
-
- );
- }
- return null;
- };
-
- return (
-
- {/* 虚拟滚动容器 + 底部加载指示器 */}
-
- {/* 虚拟滚动内容 */}
-
- {rowVirtualizer.getVirtualItems().map((virtualRow) => {
- const rowEvents = rows[virtualRow.index];
-
- return (
-
- {/* 使用 Grid 横向排列卡片(列数由 actualColumnsPerRow 决定) */}
-
- {rowEvents.map((event, colIndex) => (
-
- {
- onEventSelect(clickedEvent);
- }}
- onTitleClick={(e) => {
- e.preventDefault();
- e.stopPropagation();
- onEventSelect(event);
- }}
- onToggleFollow={() => onToggleFollow?.(event.id)}
- timelineStyle={getTimelineBoxStyle?.()}
- borderColor={borderColor}
- />
-
- ))}
-
-
- );
- })}
-
-
- {/* 底部加载指示器 - 绝对定位在虚拟内容底部 */}
-
- {error ? renderErrorIndicator() : renderLoadingIndicator()}
-
-
-
- );
-});
-
-VirtualizedFourRowGridComponent.displayName = 'VirtualizedFourRowGrid';
-
-// ⚡ 使用 React.memo 优化性能(减少不必要的重渲染)
-const VirtualizedFourRowGrid = React.memo(VirtualizedFourRowGridComponent);
-
-export default VirtualizedFourRowGrid;