Files
vf_react/src/views/Community/components/DynamicNewsCard.js

392 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 (
<Card ref={ref} {...rest} bg={cardBg} borderColor={borderColor} mb={4}>
{/* 标题部分 */}
<CardHeader>
<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}
/>
</Box>
</CardHeader>
{/* 主体内容 */}
<CardBody position="relative" pt={0}>
{/* 横向滚动事件列表 - 始终渲染(除非为空) */}
{currentPageEvents && currentPageEvents.length > 0 ? (
<EventScrollList
events={currentPageEvents}
displayEvents={displayEvents} // 累积显示的事件列表(平铺模式)
loadNextPage={loadNextPage} // 加载下一页
loadPrevPage={loadPrevPage} // 加载上一页
onFourRowEventClick={handleFourRowEventClick} // 四排模式事件点击
selectedEvent={selectedEvent}
onEventSelect={setSelectedEvent}
borderColor={borderColor}
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
loading={loadingPage !== null}
loadingPage={loadingPage}
error={error}
mode={mode}
onModeChange={handleModeToggle}
eventFollowStatus={eventFollowStatus}
onToggleFollow={handleToggleFollow}
hasMore={hasMore}
/>
) : !loading ? (
/* Empty 状态 - 只在非加载且无数据时显示 */
<Center py={10}>
<VStack>
<Text fontSize="lg" color="gray.500">暂无事件数据</Text>
</VStack>
</Center>
) : (
/* 首次加载状态 */
<Center py={10}>
<VStack>
<Spinner size="xl" color="blue.500" thickness="4px" />
<Text color="gray.500">正在加载最新事件...</Text>
</VStack>
</Center>
)}
</CardBody>
{/* 四排模式详情弹窗 - 未打开时不渲染 */}
{isModalOpen && (
<Modal isOpen={isModalOpen} onClose={onModalClose} size="6xl" scrollBehavior="inside">
<ModalOverlay />
<ModalContent>
<ModalHeader>
{modalEvent?.title || '事件详情'}
</ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>
{modalEvent && <DynamicNewsDetailPanel event={modalEvent} />}
</ModalBody>
</ModalContent>
</Modal>
)}
</Card>
);
});
DynamicNewsCard.displayName = 'DynamicNewsCard';
export default DynamicNewsCard;