// src/views/Community/components/DynamicNewsCard.js // 横向滚动事件卡片组件(实时要闻·动态追踪) import React, { forwardRef, useState, useEffect, useMemo, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Card, CardHeader, CardBody, Box, Flex, VStack, HStack, Heading, Text, Badge, Center, Spinner, useColorModeValue, useToast } 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 } from '../../../store/slices/communityDataSlice'; /** * 实时要闻·动态追踪 - 事件展示卡片组件 * @param {Array} allCachedEvents - 完整缓存事件列表(从 Redux 传入) * @param {boolean} loading - 加载状态 * @param {number} total - 服务端总数量 * @param {number} cachedCount - 已缓存数量 * @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(({ allCachedEvents = [], loading, total = 0, cachedCount = 0, 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); // 关注按钮点击处理 const handleToggleFollow = useCallback((eventId) => { dispatch(toggleEventFollow(eventId)); }, [dispatch]); // 本地状态 const [selectedEvent, setSelectedEvent] = useState(null); const [mode, setMode] = useState('carousel'); // 'carousel' 或 'grid',默认单排 const [currentPage, setCurrentPage] = useState(1); // 当前页码 const [loadingPage, setLoadingPage] = useState(null); // 正在加载的目标页码(用于 UX 提示) // 根据模式决定每页显示数量 const pageSize = mode === 'carousel' ? 5 : 10; // 计算总页数(基于服务端总数据量) const totalPages = Math.ceil(total / pageSize) || 1; // 检查是否还有更多数据 const hasMore = cachedCount < total; // 从缓存中切片获取当前页数据(过滤 null 占位符) const currentPageEvents = useMemo(() => { const startIndex = (currentPage - 1) * pageSize; const endIndex = startIndex + pageSize; return allCachedEvents.slice(startIndex, endIndex).filter(event => event !== null); }, [allCachedEvents, currentPage, pageSize]); // 翻页处理(智能预加载) const handlePageChange = useCallback(async (newPage) => { // 🔍 诊断日志 - 记录翻页开始状态 console.log('[handlePageChange] 开始翻页', { currentPage, newPage, pageSize, totalPages, hasMore, total, allCachedEventsLength: allCachedEvents.length, cachedCount }); // 0. 首先检查目标页数据是否已完整缓存 const targetPageStartIndex = (newPage - 1) * pageSize; const targetPageEndIndex = targetPageStartIndex + pageSize; const targetPageData = allCachedEvents.slice(targetPageStartIndex, targetPageEndIndex); const validTargetData = targetPageData.filter(e => e !== null); const expectedCount = Math.min(pageSize, total - targetPageStartIndex); const isTargetPageCached = validTargetData.length >= expectedCount; console.log('[handlePageChange] 目标页缓存检查', { newPage, targetPageStartIndex, targetPageEndIndex, targetPageDataLength: targetPageData.length, validTargetDataLength: validTargetData.length, expectedCount, isTargetPageCached }); // 1. 判断翻页类型:连续翻页(上一页/下一页)还是跳转翻页(点击页码/输入跳转) const isSequentialNavigation = Math.abs(newPage - currentPage) === 1; // 2. 计算预加载范围 let preloadRange; if (isSequentialNavigation) { // 连续翻页:前后各2页(共5页) const start = Math.max(1, newPage - 2); const end = Math.min(totalPages, newPage + 2); preloadRange = Array.from( { length: end - start + 1 }, (_, i) => start + i ); } else { // 跳转翻页:只加载当前页 preloadRange = [newPage]; } // 3. 检查哪些页面的数据还未缓存(检查是否包含 null 或超出数组长度) const missingPages = preloadRange.filter(page => { const pageStartIndex = (page - 1) * pageSize; const pageEndIndex = pageStartIndex + pageSize; // 如果该页超出数组范围,说明未缓存 if (pageEndIndex > allCachedEvents.length) { console.log(`[missingPages] 页面${page}超出数组范围`, { pageStartIndex, pageEndIndex, allCachedEventsLength: allCachedEvents.length }); return true; } // 检查该页的数据是否包含 null 占位符或数据不足 const pageData = allCachedEvents.slice(pageStartIndex, pageEndIndex); const validData = pageData.filter(e => e !== null); const expectedCount = Math.min(pageSize, total - pageStartIndex); const hasNullOrIncomplete = validData.length < expectedCount; console.log(`[missingPages] 页面${page}检查`, { pageStartIndex, pageEndIndex, pageDataLength: pageData.length, validDataLength: validData.length, expectedCount, hasNullOrIncomplete }); return hasNullOrIncomplete; }); console.log('[handlePageChange] 缺失页面检测完成', { preloadRange, missingPages, missingPagesCount: missingPages.length }); // 4. 如果目标页已缓存,立即切换页码,然后在后台静默预加载其他页 if (isTargetPageCached && missingPages.length > 0 && hasMore) { console.log('[DynamicNewsCard] 目标页已缓存,立即切换', { currentPage, newPage, 缺失页面: missingPages, 目标页已缓存: true }); // 立即切换页码(用户无感知延迟) setCurrentPage(newPage); // 在后台静默预加载其他缺失页面(拆分为单页请求) try { console.log('[DynamicNewsCard] 开始后台预加载', { 缺失页面: missingPages, 每页数量: pageSize }); // 拆分为单页请求,避免 per_page 动态值导致后端返回空数据 for (const page of missingPages) { await dispatch(fetchDynamicNews({ page: page, per_page: pageSize, // 固定值(5或10),不使用动态计算 pageSize: pageSize, clearCache: false })).unwrap(); console.log(`[DynamicNewsCard] 后台预加载第 ${page} 页完成`); } console.log('[DynamicNewsCard] 后台预加载全部完成', { 预加载页面: missingPages }); } catch (error) { console.error('[DynamicNewsCard] 后台预加载失败', error); // 静默失败,不影响用户体验 } return; // 提前返回,不执行下面的加载逻辑 } // 5. 如果目标页未缓存,显示 loading 并等待加载完成 if (missingPages.length > 0 && hasMore) { console.log('[DynamicNewsCard] 目标页未缓存,显示loading', { currentPage, newPage, 翻页类型: isSequentialNavigation ? '连续翻页' : '跳转翻页', 预加载范围: preloadRange, 缺失页面: missingPages, 每页数量: pageSize, 目标页已缓存: false }); try { // 设置加载状态(显示"正在加载第X页...") setLoadingPage(newPage); // 拆分为单页请求,避免 per_page 动态值导致后端返回空数据 for (const page of missingPages) { console.log(`[DynamicNewsCard] 开始加载第 ${page} 页`); await dispatch(fetchDynamicNews({ page: page, per_page: pageSize, // 固定值(5或10),不使用动态计算 pageSize: pageSize, // 传递原始 pageSize,用于正确计算索引 clearCache: false })).unwrap(); console.log(`[DynamicNewsCard] 第 ${page} 页加载完成`); } console.log('[DynamicNewsCard] 所有缺失页面加载完成', { 缺失页面: missingPages }); // 数据加载成功后才更新当前页码 setCurrentPage(newPage); } catch (error) { console.error('[DynamicNewsCard] 翻页加载失败', error); // 显示错误提示 toast({ title: '加载失败', description: `无法加载第 ${newPage} 页数据,请稍后重试`, status: 'error', duration: 3000, isClosable: true, position: 'top' }); // 加载失败时不更新页码,保持在当前页 } finally { // 清除加载状态 setLoadingPage(null); } } else if (missingPages.length === 0) { // 只有在确实不需要加载时才直接切换 console.log('[handlePageChange] 无需加载,直接切换', { currentPage, newPage, preloadRange, missingPages, reason: '所有页面均已缓存' }); setCurrentPage(newPage); } else { // 理论上不应该到这里(missingPages.length > 0 但 hasMore=false) console.warn('[handlePageChange] 意外分支:有缺失页面但无法加载', { missingPages, hasMore, currentPage, newPage, total, cachedCount }); // 尝试切换页码,但可能会显示空数据 setCurrentPage(newPage); toast({ title: '数据不完整', description: `第 ${newPage} 页数据可能不完整`, status: 'warning', duration: 2000, isClosable: true, position: 'top' }); } }, [currentPage, allCachedEvents, pageSize, totalPages, hasMore, dispatch, total, toast, cachedCount]); // 模式切换处理 const handleModeToggle = useCallback((newMode) => { if (newMode === mode) return; setMode(newMode); setCurrentPage(1); const newPageSize = newMode === 'carousel' ? 5 : 10; // 检查第1页的数据是否完整(排除 null) const firstPageData = allCachedEvents.slice(0, newPageSize); const validFirstPageCount = firstPageData.filter(e => e !== null).length; const needsRefetch = validFirstPageCount < Math.min(newPageSize, total); if (needsRefetch) { // 第1页数据不完整,清空缓存重新请求 dispatch(fetchDynamicNews({ page: 1, per_page: newPageSize, pageSize: newPageSize, // 传递 pageSize 确保索引计算一致 clearCache: true })); } // 如果第1页数据完整,不发起请求,直接切换 }, [mode, allCachedEvents, total, dispatch]); // 初始加载 useEffect(() => { if (allCachedEvents.length === 0) { dispatch(fetchDynamicNews({ page: 1, per_page: 5, pageSize: 5, // 传递 pageSize 确保索引计算一致 clearCache: true })); } }, [dispatch, allCachedEvents.length]); // 默认选中第一个事件 useEffect(() => { if (currentPageEvents.length > 0 && !selectedEvent) { setSelectedEvent(currentPageEvents[0]); } }, [currentPageEvents, selectedEvent]); return ( {/* 标题部分 */} 实时要闻·动态追踪 实时 盘中 快讯 最后更新: {lastUpdateTime?.toLocaleTimeString() || '未知'} {/* 搜索和筛选组件 */} {/* 主体内容 */} {/* 横向滚动事件列表 - 始终渲染(除非为空) */} {currentPageEvents && currentPageEvents.length > 0 ? ( ) : !loading ? ( /* Empty 状态 - 只在非加载且无数据时显示 */
暂无事件数据
) : ( /* 首次加载状态 */
正在加载最新事件...
)} {/* 详情面板 - 始终显示(如果有选中事件) */} {currentPageEvents && currentPageEvents.length > 0 && selectedEvent && ( )}
); }); DynamicNewsCard.displayName = 'DynamicNewsCard'; export default DynamicNewsCard;