// 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 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 } 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} ref - 用于滚动的ref */ const DynamicNewsCard = forwardRef(({ filters = {}, popularKeywords = [], lastUpdateTime, onSearch, onSearchFocus, onEventClick, onViewDetail, ...rest }, ref) => { const dispatch = useDispatch(); const toast = useToast(); const cardBg = useColorModeValue('white', 'gray.800'); const borderColor = useColorModeValue('gray.200', 'gray.700'); // 从 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 // 传递筛选条件 }); // 同步 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 }); setModalEvent(event); onModalOpen(); }, [onModalOpen]); // 初始加载 - 只在组件首次挂载且对应模式数据为空时执行 useEffect(() => { const isDataEmpty = currentMode === 'vertical' ? Object.keys(allCachedEventsByPage || {}).length === 0 : (allCachedEvents?.length || 0) === 0; if (!hasInitialized.current && isDataEmpty) { hasInitialized.current = true; dispatch(fetchDynamicNews({ mode: mode, // 传递当前模式 per_page: pageSize, pageSize: pageSize, // 传递 pageSize 确保索引计算一致 clearCache: true, ...filters, // 先展开筛选条件 page: PAGINATION_CONFIG.INITIAL_PAGE, // 然后覆盖 page 参数 })); } }, [dispatch, allCachedEventsByPage, allCachedEvents, currentMode, mode, pageSize]); // ✅ 移除 filters 依赖,避免重复触发 // 监听筛选条件变化 - 清空缓存并重新请求数据 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.date_range, filters.industry_code, 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;'); dispatch(fetchDynamicNews({ mode: mode, per_page: pageSize, pageSize: pageSize, clearCache: true, ...filters, // 先展开筛选条件 page: PAGINATION_CONFIG.INITIAL_PAGE, // 然后覆盖 page 参数 })); } }, [mode]); // 只监听 mode 变化 // 自动选中逻辑 - 只在首次加载时自动选中第一个事件,翻页时不自动选中 useEffect(() => { if (currentPageEvents.length > 0) { // 情况1: 首次加载 - 自动选中第一个事件并触发详情加载 if (!hasAutoSelectedFirstEvent.current && !selectedEvent) { console.log('%c🎯 [首次加载] 自动选中第一个事件', 'color: #10B981; font-weight: bold;'); hasAutoSelectedFirstEvent.current = true; setSelectedEvent(currentPageEvents[0]); return; } // 情况2: 翻页 - 如果选中的事件不在当前页,根据模式决定处理方式 const selectedEventInCurrentPage = currentPageEvents.find( e => e.id === selectedEvent?.id ); } }, [currentPageEvents, selectedEvent?.id, mode]); // 组件卸载时清理选中状态 useEffect(() => { return () => { setSelectedEvent(null); }; }, []); return ( {/* 标题部分 */} 实时要闻·动态追踪 实时 盘中 快讯 最后更新: {lastUpdateTime?.toLocaleTimeString() || '未知'} {/* 搜索和筛选组件 */} {/* 主体内容 */} {/* 横向滚动事件列表 - 始终渲染(除非为空) */} {currentPageEvents && currentPageEvents.length > 0 ? ( ) : !loading ? ( /* Empty 状态 - 只在非加载且无数据时显示 */
暂无事件数据
) : ( /* 首次加载状态 */
正在加载最新事件...
)}
{/* 四排模式详情弹窗 - 未打开时不渲染 */} {isModalOpen && ( {modalEvent?.title || '事件详情'} {modalEvent && } )}
); }); DynamicNewsCard.displayName = 'DynamicNewsCard'; export default DynamicNewsCard;