// 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, Switch, Tooltip, Icon, } from '@chakra-ui/react'; import { TimeIcon, BellIcon } from '@chakra-ui/icons'; import { useNotification } from '../../../contexts/NotificationContext'; import EventScrollList from './DynamicNewsCard/EventScrollList'; import ModeToggleButtons from './DynamicNewsCard/ModeToggleButtons'; import PaginationControl from './DynamicNewsCard/PaginationControl'; import DynamicNewsDetailPanel from './DynamicNewsDetail'; import CompactSearchBox from './CompactSearchBox'; import { fetchDynamicNews, toggleEventFollow, selectEventFollowStatus, selectVerticalEventsWithLoading, selectFourRowEventsWithLoading } from '../../../store/slices/communityDataSlice'; import { usePagination } from './DynamicNewsCard/hooks/usePagination'; import { PAGINATION_CONFIG, DISPLAY_MODES } from './DynamicNewsCard/constants'; import { PROFESSIONAL_COLORS } from '../../../constants/professionalTheme'; // 🔍 调试:渲染计数器 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 = PROFESSIONAL_COLORS.background.card; const borderColor = PROFESSIONAL_COLORS.border.default; // 通知权限相关 const { browserPermission, requestBrowserPermission } = useNotification(); // Refs const cardHeaderRef = useRef(null); const cardBodyRef = useRef(null); // 从 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 handleNotificationToggle = useCallback(async () => { if (browserPermission === 'granted') { // 已授权,提示用户去浏览器设置中关闭 toast({ title: '已开启通知', description: '要关闭通知,请在浏览器地址栏左侧点击锁图标,找到"通知"选项进行设置', status: 'info', duration: 5000, isClosable: true, }); } else { // 未授权,请求权限 await requestBrowserPermission(); } }, [browserPermission, requestBrowserPermission, toast]); // 本地状态 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 高度 // 固定模式逻辑已移除,改用 sticky 定位 return ( {/* 标题和搜索部分 - 优化版 */} {/* 第一行:标题 + 通知开关 + 更新时间 */} {/* 左侧:标题 */} 实时要闻·动态追踪 {/* 右侧:通知开关 + 更新时间 */} {/* 通知开关 */} {browserPermission === 'granted' ? '已开启' : '开启通知'} {/* 更新时间 */} 最后更新: {lastUpdateTime?.toLocaleTimeString() || '--'} {/* 第二行:筛选组件 */} {/* 主体内容 */} {/* 顶部控制栏:模式切换按钮 + 分页控制器(滚动时固定在顶部) */} {/* 左侧:模式切换按钮 */} {/* 右侧:分页控制器(仅在纵向模式显示) */} {mode === 'vertical' && totalPages > 1 && ( )} {/* 内容区域 - 撑满剩余高度 */} {/* Loading 蒙层 - 数据请求时显示 */} {loading && ( 正在加载最新事件... )} {/* 列表内容 - 始终渲染 */} {/* 四排模式详情弹窗 - 未打开时不渲染 */} {isModalOpen && ( {modalEvent?.title || '事件详情'} {modalEvent && } )} ); }); DynamicNewsCard.displayName = 'DynamicNewsCard'; export default DynamicNewsCard;