638 lines
23 KiB
JavaScript
638 lines
23 KiB
JavaScript
// src/views/Community/components/DynamicNewsCard.js
|
||
// 横向滚动事件卡片组件(实时要闻·动态追踪)
|
||
|
||
import React, { forwardRef, useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||
import { useDispatch, useSelector } from 'react-redux';
|
||
import {
|
||
Card,
|
||
CardHeader,
|
||
CardBody,
|
||
Box,
|
||
Flex,
|
||
VStack,
|
||
HStack,
|
||
Heading,
|
||
Text,
|
||
Badge,
|
||
Center,
|
||
Spinner,
|
||
Modal,
|
||
ModalOverlay,
|
||
ModalContent,
|
||
ModalHeader,
|
||
ModalBody,
|
||
ModalCloseButton,
|
||
useColorModeValue,
|
||
useToast,
|
||
useDisclosure,
|
||
} from '@chakra-ui/react';
|
||
import { TimeIcon } from '@chakra-ui/icons';
|
||
import EventScrollList from './DynamicNewsCard/EventScrollList';
|
||
import ModeToggleButtons from './DynamicNewsCard/ModeToggleButtons';
|
||
import PaginationControl from './DynamicNewsCard/PaginationControl';
|
||
import DynamicNewsDetailPanel from './DynamicNewsDetail';
|
||
import UnifiedSearchBox from './UnifiedSearchBox';
|
||
import {
|
||
fetchDynamicNews,
|
||
toggleEventFollow,
|
||
selectEventFollowStatus,
|
||
selectVerticalEventsWithLoading,
|
||
selectFourRowEventsWithLoading
|
||
} from '../../../store/slices/communityDataSlice';
|
||
import { usePagination } from './DynamicNewsCard/hooks/usePagination';
|
||
import { PAGINATION_CONFIG, DISPLAY_MODES } from './DynamicNewsCard/constants';
|
||
|
||
// 🔍 调试:渲染计数器
|
||
let dynamicNewsCardRenderCount = 0;
|
||
|
||
/**
|
||
* 实时要闻·动态追踪 - 事件展示卡片组件
|
||
* @param {Object} filters - 筛选条件
|
||
* @param {Array} popularKeywords - 热门关键词
|
||
* @param {Date} lastUpdateTime - 最后更新时间
|
||
* @param {Function} onSearch - 搜索回调
|
||
* @param {Function} onSearchFocus - 搜索框获得焦点回调
|
||
* @param {Function} onEventClick - 事件点击回调
|
||
* @param {Function} onViewDetail - 查看详情回调
|
||
* @param {Object} trackingFunctions - PostHog 追踪函数集合
|
||
* @param {Object} ref - 用于滚动的ref
|
||
*/
|
||
const DynamicNewsCard = forwardRef(({
|
||
filters = {},
|
||
popularKeywords = [],
|
||
lastUpdateTime,
|
||
onSearch,
|
||
onSearchFocus,
|
||
onEventClick,
|
||
onViewDetail,
|
||
trackingFunctions = {},
|
||
...rest
|
||
}, ref) => {
|
||
const dispatch = useDispatch();
|
||
const toast = useToast();
|
||
const cardBg = useColorModeValue('white', 'gray.800');
|
||
const borderColor = useColorModeValue('gray.200', 'gray.700');
|
||
|
||
// 固定模式状态
|
||
const [isFixedMode, setIsFixedMode] = useState(false);
|
||
const [headerHeight, setHeaderHeight] = useState(0);
|
||
const cardHeaderRef = useRef(null);
|
||
const cardBodyRef = useRef(null);
|
||
|
||
// 导航栏和页脚固定高度
|
||
const NAVBAR_HEIGHT = 64; // 主导航高度
|
||
const SECONDARY_NAV_HEIGHT = 44; // 二级导航高度
|
||
const FOOTER_HEIGHT = 120; // 页脚高度(预留)
|
||
const TOTAL_NAV_HEIGHT = NAVBAR_HEIGHT + SECONDARY_NAV_HEIGHT; // 总导航高度 128px
|
||
|
||
// 从 Redux 读取关注状态
|
||
const eventFollowStatus = useSelector(selectEventFollowStatus);
|
||
|
||
// 本地状态:模式(先初始化,后面会被 usePagination 更新)
|
||
const [currentMode, setCurrentMode] = useState('vertical');
|
||
|
||
// 根据当前模式从 Redux 读取对应的数据(添加默认值避免 undefined)
|
||
const verticalData = useSelector(selectVerticalEventsWithLoading) || {};
|
||
const fourRowData = useSelector(selectFourRowEventsWithLoading) || {};
|
||
|
||
// 🔍 调试:从 Redux 读取数据
|
||
console.log('%c[DynamicNewsCard] 从 Redux 读取数据', 'color: #3B82F6; font-weight: bold;', {
|
||
currentMode,
|
||
'verticalData.data type': typeof verticalData.data,
|
||
'verticalData.data keys': verticalData.data ? Object.keys(verticalData.data) : [],
|
||
'verticalData.total': verticalData.total,
|
||
'verticalData.cachedPageCount': verticalData.cachedPageCount,
|
||
'verticalData.loading': verticalData.loading,
|
||
'fourRowData.data?.length': fourRowData.data?.length || 0,
|
||
'fourRowData.total': fourRowData.total,
|
||
});
|
||
|
||
// 根据模式选择数据源
|
||
// 纵向模式:data 是页码映射 { 1: [...], 2: [...] }
|
||
// 平铺模式:data 是数组 [...]
|
||
const modeData = currentMode === 'four-row' ? fourRowData : verticalData;
|
||
const {
|
||
data = currentMode === 'vertical' ? {} : [], // 纵向是对象,平铺是数组
|
||
loading = false,
|
||
error = null,
|
||
pagination, // 分页元数据
|
||
total = 0, // 向后兼容
|
||
cachedCount = 0,
|
||
cachedPageCount = 0
|
||
} = modeData;
|
||
|
||
// 传递给 usePagination 的数据
|
||
const allCachedEventsByPage = currentMode === 'vertical' ? data : undefined;
|
||
const allCachedEvents = currentMode === 'four-row' ? data : undefined;
|
||
|
||
// 🔍 调试:选择的数据源
|
||
console.log('%c[DynamicNewsCard] 选择的数据源', 'color: #3B82F6; font-weight: bold;', {
|
||
mode: currentMode,
|
||
'allCachedEventsByPage': allCachedEventsByPage ? Object.keys(allCachedEventsByPage) : 'undefined',
|
||
'allCachedEvents?.length': allCachedEvents?.length,
|
||
total,
|
||
cachedCount,
|
||
cachedPageCount,
|
||
loading,
|
||
error
|
||
});
|
||
|
||
// 🔍 调试:记录每次渲染
|
||
dynamicNewsCardRenderCount++;
|
||
console.log(`%c🔍 [DynamicNewsCard] 渲染 #${dynamicNewsCardRenderCount} - mode=${currentMode}, allCachedEvents.length=${allCachedEvents?.length || 0}, total=${total}`, 'color: #FF9800; font-weight: bold; font-size: 14px;');
|
||
|
||
// 关注按钮点击处理
|
||
const handleToggleFollow = useCallback((eventId) => {
|
||
dispatch(toggleEventFollow(eventId));
|
||
}, [dispatch]);
|
||
|
||
// 本地状态
|
||
const [selectedEvent, setSelectedEvent] = useState(null);
|
||
|
||
// 弹窗状态(用于四排模式)
|
||
const { isOpen: isModalOpen, onOpen: onModalOpen, onClose: onModalClose } = useDisclosure();
|
||
const [modalEvent, setModalEvent] = useState(null);
|
||
|
||
// 初始化标记 - 确保初始加载只执行一次
|
||
const hasInitialized = useRef(false);
|
||
// 追踪是否已自动选中过首个事件
|
||
const hasAutoSelectedFirstEvent = useRef(false);
|
||
// 追踪筛选条件 useEffect 是否是第一次渲染(避免初始加载时重复请求)
|
||
const isFirstRenderForFilters = useRef(true);
|
||
|
||
// 使用分页 Hook
|
||
const {
|
||
currentPage,
|
||
mode,
|
||
loadingPage,
|
||
pageSize,
|
||
totalPages,
|
||
hasMore,
|
||
currentPageEvents,
|
||
displayEvents, // 当前显示的事件列表
|
||
handlePageChange,
|
||
handleModeToggle,
|
||
loadNextPage, // 加载下一页
|
||
loadPrevPage // 加载上一页
|
||
} = usePagination({
|
||
allCachedEventsByPage, // 纵向模式:页码映射
|
||
allCachedEvents, // 平铺模式:数组
|
||
pagination, // 分页元数据对象
|
||
total, // 向后兼容
|
||
cachedCount,
|
||
dispatch,
|
||
toast,
|
||
filters, // 传递筛选条件
|
||
initialMode: currentMode // 传递当前显示模式
|
||
});
|
||
|
||
// 同步 mode 到 currentMode
|
||
useEffect(() => {
|
||
setCurrentMode(mode);
|
||
}, [mode]);
|
||
|
||
// 监听 error 状态,显示空数据提示
|
||
useEffect(() => {
|
||
if (error && error.includes('暂无更多数据')) {
|
||
toast({
|
||
title: '提示',
|
||
description: error,
|
||
status: 'info',
|
||
duration: 2000,
|
||
isClosable: true,
|
||
});
|
||
}
|
||
}, [error, toast]);
|
||
|
||
// 四排模式的事件点击处理(打开弹窗)
|
||
const handleFourRowEventClick = useCallback((event) => {
|
||
console.log('%c🔲 [四排模式] 点击事件,打开详情弹窗', 'color: #8B5CF6; font-weight: bold;', { eventId: event.id, title: event.title });
|
||
|
||
// 🎯 追踪事件详情打开
|
||
if (trackingFunctions.trackNewsDetailOpened) {
|
||
trackingFunctions.trackNewsDetailOpened({
|
||
eventId: event.id,
|
||
eventTitle: event.title,
|
||
importance: event.importance,
|
||
source: 'four_row_mode',
|
||
displayMode: 'modal',
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
}
|
||
|
||
setModalEvent(event);
|
||
onModalOpen();
|
||
}, [onModalOpen, trackingFunctions]);
|
||
|
||
// 初始加载 - 只在组件首次挂载且对应模式数据为空时执行
|
||
useEffect(() => {
|
||
// 添加防抖:如果已经初始化,不再执行
|
||
if (hasInitialized.current) return;
|
||
|
||
const isDataEmpty = currentMode === 'vertical'
|
||
? Object.keys(allCachedEventsByPage || {}).length === 0
|
||
: (allCachedEvents?.length || 0) === 0;
|
||
|
||
if (isDataEmpty) {
|
||
hasInitialized.current = true;
|
||
dispatch(fetchDynamicNews({
|
||
mode: mode, // 传递当前模式
|
||
per_page: pageSize,
|
||
pageSize: pageSize, // 传递 pageSize 确保索引计算一致
|
||
clearCache: true,
|
||
...filters, // 先展开筛选条件
|
||
page: PAGINATION_CONFIG.INITIAL_PAGE, // 然后覆盖 page 参数
|
||
}));
|
||
}
|
||
}, [dispatch, currentMode, mode, pageSize]); // 移除 allCachedEventsByPage, allCachedEvents 依赖,避免数据更新触发重复请求
|
||
|
||
// 监听筛选条件变化 - 清空缓存并重新请求数据
|
||
useEffect(() => {
|
||
// 跳过初始加载(由上面的 useEffect 处理)
|
||
if (!hasInitialized.current) return;
|
||
|
||
// 跳过第一次渲染(避免与初始加载 useEffect 重复)
|
||
if (isFirstRenderForFilters.current) {
|
||
isFirstRenderForFilters.current = false;
|
||
return;
|
||
}
|
||
|
||
console.log('%c🔍 [筛选] 筛选条件改变,重新请求数据', 'color: #8B5CF6; font-weight: bold;', filters);
|
||
|
||
// 筛选条件改变时,清空对应模式的缓存并从第1页开始加载
|
||
dispatch(fetchDynamicNews({
|
||
mode: mode, // 传递当前模式
|
||
per_page: pageSize,
|
||
pageSize: pageSize,
|
||
clearCache: true, // 清空缓存
|
||
...filters, // 先展开筛选条件
|
||
page: PAGINATION_CONFIG.INITIAL_PAGE, // 然后覆盖 page 参数
|
||
}));
|
||
}, [
|
||
filters.sort,
|
||
filters.importance,
|
||
filters.q,
|
||
filters.start_date, // 时间筛选参数:开始时间
|
||
filters.end_date, // 时间筛选参数:结束时间
|
||
filters.recent_days, // 时间筛选参数:近N天
|
||
filters.industry_code,
|
||
filters._forceRefresh, // 强制刷新标志(用于重置按钮)
|
||
mode, // 添加 mode 到依赖
|
||
pageSize, // 添加 pageSize 到依赖
|
||
dispatch
|
||
]); // 只监听筛选参数的变化,不监听 page
|
||
|
||
// 监听模式切换 - 如果新模式数据为空,请求数据
|
||
useEffect(() => {
|
||
const isDataEmpty = currentMode === 'vertical'
|
||
? Object.keys(allCachedEventsByPage || {}).length === 0
|
||
: (allCachedEvents?.length || 0) === 0;
|
||
|
||
if (hasInitialized.current && isDataEmpty) {
|
||
console.log(`%c🔄 [模式切换] ${mode} 模式数据为空,开始加载`, 'color: #8B5CF6; font-weight: bold;');
|
||
|
||
// 🔧 根据 mode 直接计算 per_page,避免使用可能过时的 pageSize prop
|
||
const modePageSize = mode === DISPLAY_MODES.FOUR_ROW
|
||
? PAGINATION_CONFIG.FOUR_ROW_PAGE_SIZE // 30
|
||
: PAGINATION_CONFIG.VERTICAL_PAGE_SIZE; // 10
|
||
|
||
console.log(`%c 计算的 per_page: ${modePageSize} (mode: ${mode})`, 'color: #8B5CF6;');
|
||
|
||
dispatch(fetchDynamicNews({
|
||
mode: mode,
|
||
per_page: modePageSize, // 使用计算的值,不是 pageSize prop
|
||
pageSize: modePageSize,
|
||
clearCache: true,
|
||
...filters, // 先展开筛选条件
|
||
page: PAGINATION_CONFIG.INITIAL_PAGE, // 然后覆盖 page 参数
|
||
}));
|
||
}
|
||
}, [mode, currentMode, allCachedEventsByPage, allCachedEvents, dispatch]); // 移除 filters 依赖,避免与筛选 useEffect 循环触发 // 添加所有依赖
|
||
|
||
// 自动选中逻辑 - 只在首次加载时自动选中第一个事件,翻页时不自动选中
|
||
useEffect(() => {
|
||
if (currentPageEvents.length > 0) {
|
||
// 情况1: 首次加载 - 自动选中第一个事件并触发详情加载
|
||
if (!hasAutoSelectedFirstEvent.current && !selectedEvent) {
|
||
console.log('%c🎯 [首次加载] 自动选中第一个事件', 'color: #10B981; font-weight: bold;');
|
||
hasAutoSelectedFirstEvent.current = true;
|
||
setSelectedEvent(currentPageEvents[0]);
|
||
|
||
// 🎯 追踪事件点击(首次自动选中)
|
||
if (trackingFunctions.trackNewsArticleClicked) {
|
||
trackingFunctions.trackNewsArticleClicked({
|
||
eventId: currentPageEvents[0].id,
|
||
eventTitle: currentPageEvents[0].title,
|
||
importance: currentPageEvents[0].importance,
|
||
source: 'auto_select_first',
|
||
displayMode: mode,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 情况2: 翻页 - 如果选中的事件不在当前页,根据模式决定处理方式
|
||
const selectedEventInCurrentPage = currentPageEvents.find(
|
||
e => e.id === selectedEvent?.id
|
||
);
|
||
}
|
||
}, [currentPageEvents, selectedEvent?.id, mode, trackingFunctions]);
|
||
|
||
// 组件卸载时清理选中状态
|
||
useEffect(() => {
|
||
return () => {
|
||
setSelectedEvent(null);
|
||
};
|
||
}, []);
|
||
|
||
// 页码切换时滚动到顶部
|
||
const handlePageChangeWithScroll = useCallback((page) => {
|
||
// 先切换页码
|
||
handlePageChange(page);
|
||
|
||
// 延迟一帧,确保DOM更新完成后再滚动
|
||
requestAnimationFrame(() => {
|
||
// 查找所有标记为滚动容器的元素
|
||
const containers = document.querySelectorAll('[data-scroll-container]');
|
||
containers.forEach(container => {
|
||
container.scrollTo({ top: 0, behavior: 'smooth' });
|
||
});
|
||
console.log('📜 页码切换,滚动到顶部', { containersFound: containers.length });
|
||
});
|
||
}, [handlePageChange]);
|
||
|
||
// 测量 CardHeader 高度
|
||
useEffect(() => {
|
||
const cardHeaderElement = cardHeaderRef.current;
|
||
if (!cardHeaderElement) return;
|
||
|
||
// 测量并更新高度
|
||
const updateHeaderHeight = () => {
|
||
const height = cardHeaderElement.offsetHeight;
|
||
setHeaderHeight(height);
|
||
};
|
||
|
||
// 初始测量
|
||
updateHeaderHeight();
|
||
|
||
// 监听窗口大小变化(响应式调整)
|
||
window.addEventListener('resize', updateHeaderHeight);
|
||
|
||
return () => {
|
||
window.removeEventListener('resize', updateHeaderHeight);
|
||
};
|
||
}, []);
|
||
|
||
// 监听 CardHeader 是否到达触发点,动态切换固定模式
|
||
useEffect(() => {
|
||
const cardHeaderElement = cardHeaderRef.current;
|
||
const cardBodyElement = cardBodyRef.current;
|
||
if (!cardHeaderElement || !cardBodyElement) return;
|
||
|
||
let ticking = false;
|
||
const TRIGGER_OFFSET = 150; // 提前 150px 触发(进入固定模式)
|
||
const EXIT_OFFSET = 200; // 提前 200px 退出(退出比进入更容易)
|
||
const EXIT_THRESHOLD = 30; // 接近顶部 30px 内即可退出
|
||
|
||
// 外部滚动监听:触发固定模式
|
||
const handleExternalScroll = () => {
|
||
// 只在非固定模式下监听外部滚动
|
||
if (!isFixedMode && !ticking) {
|
||
window.requestAnimationFrame(() => {
|
||
// 获取 CardHeader 相对视口的位置
|
||
const rect = cardHeaderElement.getBoundingClientRect();
|
||
const elementTop = rect.top;
|
||
|
||
// 计算触发点:总导航高度 + 150px 偏移量
|
||
const triggerPoint = TOTAL_NAV_HEIGHT + TRIGGER_OFFSET;
|
||
|
||
// 向上滑动:元素顶部到达触发点 → 激活固定模式
|
||
if (elementTop <= triggerPoint) {
|
||
setIsFixedMode(true);
|
||
console.log('🔒 切换为固定全屏模式', {
|
||
elementTop,
|
||
triggerPoint,
|
||
offset: TRIGGER_OFFSET
|
||
});
|
||
}
|
||
|
||
ticking = false;
|
||
});
|
||
|
||
ticking = true;
|
||
}
|
||
};
|
||
|
||
// 内部滚动监听:退出固定模式
|
||
const handleWheel = (e) => {
|
||
// 只在固定模式下监听内部滚动
|
||
if (!isFixedMode) return;
|
||
|
||
// 检测向上滚动(deltaY < 0)
|
||
if (e.deltaY < 0) {
|
||
window.requestAnimationFrame(() => {
|
||
// 🎯 检查 1:CardHeader 位置(主要条件)
|
||
const rect = cardHeaderElement.getBoundingClientRect();
|
||
const elementTop = rect.top;
|
||
const exitPoint = TOTAL_NAV_HEIGHT + EXIT_OFFSET;
|
||
|
||
// 🎯 检查 2:左侧事件列表滚动位置(辅助条件)
|
||
const eventListContainers = cardBodyElement.querySelectorAll('[data-event-list-container]');
|
||
const allNearTop = eventListContainers.length === 0 ||
|
||
Array.from(eventListContainers).every(
|
||
container => container.scrollTop <= EXIT_THRESHOLD
|
||
);
|
||
|
||
// 🎯 退出条件:CardHeader 超过退出点 OR 左侧列表接近顶部
|
||
if (elementTop > exitPoint || allNearTop) {
|
||
setIsFixedMode(false);
|
||
console.log('🔓 恢复正常文档流模式', {
|
||
elementTop,
|
||
exitPoint,
|
||
listNearTop: allNearTop,
|
||
exitThreshold: EXIT_THRESHOLD,
|
||
reason: elementTop > exitPoint ? 'CardHeader位置' : '左侧列表滚动'
|
||
});
|
||
}
|
||
});
|
||
}
|
||
};
|
||
|
||
// 监听外部滚动
|
||
window.addEventListener('scroll', handleExternalScroll, { passive: true });
|
||
|
||
// 监听内部滚轮事件(固定模式下)
|
||
if (isFixedMode) {
|
||
cardBodyElement.addEventListener('wheel', handleWheel, { passive: true });
|
||
}
|
||
|
||
// 初次检查位置
|
||
handleExternalScroll();
|
||
|
||
return () => {
|
||
window.removeEventListener('scroll', handleExternalScroll);
|
||
cardBodyElement.removeEventListener('wheel', handleWheel);
|
||
};
|
||
}, [isFixedMode]);
|
||
|
||
return (
|
||
<Card
|
||
ref={ref}
|
||
{...rest}
|
||
bg={cardBg}
|
||
borderColor={borderColor}
|
||
mb={4}
|
||
>
|
||
{/* 标题部分 */}
|
||
<CardHeader
|
||
ref={cardHeaderRef}
|
||
position={isFixedMode ? 'fixed' : 'relative'}
|
||
top={isFixedMode ? `${TOTAL_NAV_HEIGHT}px` : 'auto'}
|
||
left={isFixedMode ? 0 : 'auto'}
|
||
right={isFixedMode ? 0 : 'auto'}
|
||
maxW={isFixedMode ? '1600px' : '100%'}
|
||
mx={isFixedMode ? 'auto' : 0}
|
||
px={isFixedMode ? { base: 3, md: 4 } : undefined}
|
||
zIndex={isFixedMode ? 999 : 1}
|
||
bg={cardBg}
|
||
>
|
||
<Flex justify="space-between" align="center">
|
||
<VStack align="start" spacing={1}>
|
||
<Heading size="md">
|
||
<HStack>
|
||
<TimeIcon />
|
||
<Text>实时要闻·动态追踪</Text>
|
||
</HStack>
|
||
</Heading>
|
||
<HStack fontSize="sm" color="gray.500">
|
||
<Badge colorScheme="red">实时</Badge>
|
||
<Badge colorScheme="green">盘中</Badge>
|
||
<Badge colorScheme="blue">快讯</Badge>
|
||
</HStack>
|
||
</VStack>
|
||
<Text fontSize="xs" color="gray.500">
|
||
最后更新: {lastUpdateTime?.toLocaleTimeString() || '未知'}
|
||
</Text>
|
||
</Flex>
|
||
|
||
{/* 搜索和筛选组件 */}
|
||
<Box mt={4}>
|
||
<UnifiedSearchBox
|
||
onSearch={onSearch}
|
||
onSearchFocus={onSearchFocus}
|
||
popularKeywords={popularKeywords}
|
||
filters={filters}
|
||
mode={mode}
|
||
pageSize={pageSize}
|
||
trackingFunctions={trackingFunctions}
|
||
/>
|
||
</Box>
|
||
</CardHeader>
|
||
|
||
{/* 主体内容 */}
|
||
<CardBody
|
||
ref={cardBodyRef}
|
||
position={isFixedMode ? 'fixed' : 'relative'}
|
||
top={isFixedMode ? `${TOTAL_NAV_HEIGHT + headerHeight}px` : 'auto'}
|
||
left={isFixedMode ? 0 : 'auto'}
|
||
right={isFixedMode ? 0 : 'auto'}
|
||
bottom={isFixedMode ? `${FOOTER_HEIGHT}px` : 'auto'}
|
||
maxW={isFixedMode ? '1600px' : '100%'}
|
||
mx={isFixedMode ? 'auto' : 0}
|
||
h={isFixedMode ? `calc(100vh - ${TOTAL_NAV_HEIGHT + headerHeight + FOOTER_HEIGHT}px)` : 'auto'}
|
||
px={isFixedMode ? { base: 3, md: 4 } : undefined}
|
||
pt={4}
|
||
display="flex"
|
||
flexDirection="column"
|
||
overflow="hidden"
|
||
zIndex={isFixedMode ? 1000 : 1}
|
||
bg={cardBg}
|
||
>
|
||
{/* 顶部控制栏:模式切换按钮 + 筛选按钮 + 分页控制器(固定不滚动) */}
|
||
<Flex justify="space-between" align="center" mb={2} flexShrink={0}>
|
||
{/* 左侧:模式切换按钮 + 筛选按钮 */}
|
||
<ModeToggleButtons mode={mode} onModeChange={handleModeToggle} />
|
||
|
||
{/* 右侧:分页控制器(仅在纵向模式显示) */}
|
||
{mode === 'vertical' && totalPages > 1 && (
|
||
<PaginationControl
|
||
currentPage={currentPage}
|
||
totalPages={totalPages}
|
||
onPageChange={handlePageChangeWithScroll}
|
||
/>
|
||
)}
|
||
</Flex>
|
||
|
||
{/* 内容区域 - 撑满剩余高度 */}
|
||
<Box flex="1" minH={0} position="relative">
|
||
{/* Loading 蒙层 - 数据请求时显示 */}
|
||
{loading && (
|
||
<Box
|
||
position="absolute"
|
||
top={0}
|
||
left={0}
|
||
right={0}
|
||
bottom={0}
|
||
bg={useColorModeValue('rgba(255, 255, 255, 0.85)', 'rgba(26, 32, 44, 0.85)')}
|
||
display="flex"
|
||
alignItems="center"
|
||
justifyContent="center"
|
||
zIndex={10}
|
||
borderRadius="md"
|
||
>
|
||
<VStack spacing={3}>
|
||
<Spinner size="xl" color="blue.500" thickness="4px" />
|
||
<Text color={useColorModeValue('gray.600', 'gray.300')} fontWeight="medium">
|
||
正在加载最新事件...
|
||
</Text>
|
||
</VStack>
|
||
</Box>
|
||
)}
|
||
|
||
{/* 列表内容 - 始终渲染 */}
|
||
<EventScrollList
|
||
events={currentPageEvents}
|
||
displayEvents={displayEvents} // 累积显示的事件列表(平铺模式)
|
||
loadNextPage={loadNextPage} // 加载下一页
|
||
loadPrevPage={loadPrevPage} // 加载上一页
|
||
onFourRowEventClick={handleFourRowEventClick} // 四排模式事件点击
|
||
selectedEvent={selectedEvent}
|
||
onEventSelect={setSelectedEvent}
|
||
borderColor={borderColor}
|
||
currentPage={currentPage}
|
||
totalPages={totalPages}
|
||
onPageChange={handlePageChangeWithScroll}
|
||
loading={loadingPage !== null}
|
||
error={error}
|
||
mode={mode}
|
||
eventFollowStatus={eventFollowStatus}
|
||
onToggleFollow={handleToggleFollow}
|
||
hasMore={hasMore}
|
||
/>
|
||
</Box>
|
||
</CardBody>
|
||
|
||
{/* 四排模式详情弹窗 - 未打开时不渲染 */}
|
||
{isModalOpen && (
|
||
<Modal isOpen={isModalOpen} onClose={onModalClose} size="full" scrollBehavior="inside">
|
||
<ModalOverlay />
|
||
<ModalContent maxW="1600px" mx="auto" my={8}>
|
||
<ModalHeader>
|
||
{modalEvent?.title || '事件详情'}
|
||
</ModalHeader>
|
||
<ModalCloseButton />
|
||
<ModalBody pb={6}>
|
||
{modalEvent && <DynamicNewsDetailPanel event={modalEvent} />}
|
||
</ModalBody>
|
||
</ModalContent>
|
||
</Modal>
|
||
)}
|
||
</Card>
|
||
);
|
||
});
|
||
|
||
DynamicNewsCard.displayName = 'DynamicNewsCard';
|
||
|
||
export default DynamicNewsCard;
|