diff --git a/src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js b/src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js index cfdf34b7..3926e314 100644 --- a/src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js +++ b/src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js @@ -46,7 +46,7 @@ const DynamicNewsDetailPanel = ({ event }) => { // 折叠状态管理 const [isStocksOpen, setIsStocksOpen] = useState(true); - const [isHistoricalOpen, setIsHistoricalOpen] = useState(false); + const [isHistoricalOpen, setIsHistoricalOpen] = useState(true); const [isTransmissionOpen, setIsTransmissionOpen] = useState(false); // 关注状态管理 diff --git a/src/views/EventDetail/components/HistoricalEvents.js b/src/views/EventDetail/components/HistoricalEvents.js index accc001f..b69df2d6 100644 --- a/src/views/EventDetail/components/HistoricalEvents.js +++ b/src/views/EventDetail/components/HistoricalEvents.js @@ -7,121 +7,107 @@ import { Text, Badge, Button, - Collapse, Skeleton, Alert, AlertIcon, - Card, - CardBody, - CardHeader, - Divider, + SimpleGrid, Icon, useColorModeValue, - Tooltip, Spinner, - Table, - Thead, - Tbody, - Tr, - Th, - Td, - TableContainer, - Link + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + Link, + Flex, + Collapse } from '@chakra-ui/react'; import { - FaExclamationTriangle, - FaClock, - FaCalendarAlt, FaChartLine, - FaEye, - FaTimes, - FaInfoCircle, - FaChevronDown, - FaChevronUp + FaInfoCircle } from 'react-icons/fa'; import { stockService } from '../../../services/eventService'; import { logger } from '../../../utils/logger'; const HistoricalEvents = ({ - events = [], - expectationScore = null, - loading = false, - error = null - }) => { - // 所有 useState/useEffect/useContext/useRef/useCallback/useMemo 必须在组件顶层、顺序一致 - // 不要在 if/循环/回调中调用 Hook - const [expandedEvents, setExpandedEvents] = useState(new Set()); - const [expandedStocks, setExpandedStocks] = useState(new Set()); // 追踪哪些事件的股票列表被展开 + events = [], + expectationScore = null, + loading = false, + error = null +}) => { + // 状态管理 + const [selectedEventForStocks, setSelectedEventForStocks] = useState(null); + const [stocksModalOpen, setStocksModalOpen] = useState(false); const [eventStocks, setEventStocks] = useState({}); const [loadingStocks, setLoadingStocks] = useState({}); // 颜色主题 - const timelineBg = useColorModeValue('#D4AF37', '#B8860B'); const cardBg = useColorModeValue('white', 'gray.800'); const borderColor = useColorModeValue('gray.200', 'gray.600'); const textSecondary = useColorModeValue('gray.600', 'gray.400'); + const nameColor = useColorModeValue('gray.700', 'gray.300'); - // 切换事件展开状态 - const toggleEventExpansion = (eventId) => { - const newExpanded = new Set(expandedEvents); - if (newExpanded.has(eventId)) { - newExpanded.delete(eventId); - } else { - newExpanded.add(eventId); - } - setExpandedEvents(newExpanded); + // 字段兼容函数 + const getEventDate = (event) => { + return event?.event_date || event?.created_at || event?.date || event?.publish_time; }; - // 切换股票列表展开状态 - const toggleStocksExpansion = async (event) => { - const eventId = event.id; - const newExpanded = new Set(expandedStocks); + const getEventContent = (event) => { + return event?.content || event?.description || event?.summary; + }; - // 如果正在收起,直接更新状态 - if (newExpanded.has(eventId)) { - newExpanded.delete(eventId); - setExpandedStocks(newExpanded); - return; + // Debug: 打印实际数据结构 + useEffect(() => { + if (events && events.length > 0) { + console.log('===== Historical Events Debug ====='); + console.log('First Event Data:', events[0]); + console.log('Available Fields:', Object.keys(events[0])); + console.log('Date Field:', getEventDate(events[0])); + console.log('Content Field:', getEventContent(events[0])); + console.log('=================================='); } + }, [events]); - // 如果正在展开,先展开再加载数据 - newExpanded.add(eventId); - setExpandedStocks(newExpanded); + // 点击相关股票按钮 + const handleViewStocks = async (event) => { + setSelectedEventForStocks(event); + setStocksModalOpen(true); // 如果已经加载过该事件的股票数据,不再重复加载 - if (eventStocks[eventId]) { + if (eventStocks[event.id]) { return; } // 标记为加载中 - setLoadingStocks(prev => ({ ...prev, [eventId]: true })); + setLoadingStocks(prev => ({ ...prev, [event.id]: true })); try { // 调用API获取历史事件相关股票 - const response = await stockService.getHistoricalEventStocks(eventId); + const response = await stockService.getHistoricalEventStocks(event.id); setEventStocks(prev => ({ ...prev, - [eventId]: response.data || [] + [event.id]: response.data || [] })); } catch (err) { - logger.error('HistoricalEvents', 'toggleStocksExpansion', err, { - eventId: eventId, + logger.error('HistoricalEvents', 'handleViewStocks', err, { + eventId: event.id, eventTitle: event.title }); setEventStocks(prev => ({ ...prev, - [eventId]: [] + [event.id]: [] })); } finally { - setLoadingStocks(prev => ({ ...prev, [eventId]: false })); + setLoadingStocks(prev => ({ ...prev, [event.id]: false })); } }; - // 获取重要性图标 - const getImportanceIcon = (importance) => { - if (importance >= 4) return FaExclamationTriangle; - if (importance >= 2) return FaCalendarAlt; - return FaClock; + // 关闭弹窗 + const handleCloseModal = () => { + setStocksModalOpen(false); + setSelectedEventForStocks(null); }; // 获取重要性颜色 @@ -153,89 +139,28 @@ const HistoricalEvents = ({ return `${Math.floor(diffDays / 365)}年前`; }; - // 处理关联描述字段的辅助函数 - const getRelationDesc = (relationDesc) => { - // 处理空值 - if (!relationDesc) return ''; - - // 如果是字符串,直接返回 - if (typeof relationDesc === 'string') { - return relationDesc; - } - - // 如果是对象且包含data数组 - if (typeof relationDesc === 'object' && relationDesc.data && Array.isArray(relationDesc.data)) { - const firstItem = relationDesc.data[0]; - if (firstItem) { - // 优先使用 query_part,其次使用 sentences - return firstItem.query_part || firstItem.sentences || ''; - } - } - - // 其他情况返回空字符串 - return ''; - }; - - // 可展开的文本组件 - const ExpandableText = ({ text, maxLength = 20 }) => { - const { isOpen, onToggle } = useDisclosure(); - const [shouldTruncate, setShouldTruncate] = useState(false); - - useEffect(() => { - if (text && text.length > maxLength) { - setShouldTruncate(true); - } else { - setShouldTruncate(false); - } - }, [text, maxLength]); - - if (!text) return --; - - const displayText = shouldTruncate && !isOpen - ? text.substring(0, maxLength) + '...' - : text; - - return ( - - - {displayText}{text.includes('AI合成') ? '' : '(AI合成)'} - - {shouldTruncate && ( - - )} - - ); - }; - // 加载状态 if (loading) { return ( - + {[1, 2, 3].map((i) => ( - - - - - - - - - - - - + + + + + + + + ))} - + ); } @@ -267,208 +192,151 @@ const HistoricalEvents = ({ return ( <> - - {/* 超预期得分显示 */} - {expectationScore && ( - - - - - - - 超预期得分: {expectationScore} - - - 基于历史事件判断当前事件的超预期情况,满分100分(AI合成) - - - - - - )} - - {/* 历史事件时间轴 */} - - {/* 时间轴线 */} - - - {/* 事件列表 */} - - {events.map((event, index) => { - const ImportanceIcon = getImportanceIcon(event.importance); - const importanceColor = getImportanceColor(event.importance); - const isExpanded = expandedEvents.has(event.id); - - return ( - - {/* 时间轴节点 */} - - - - - {/* 事件内容卡片 */} - - - - - {/* 事件标题和操作 */} - - - - - - {formatDate(event.event_date)} - ({getRelativeTime(event.event_date)}) - {event.relevance && ( - - 相关度: {event.relevance} - - )} - - - - - {event.importance && ( - - - 重要性: {event.importance} - - - )} - - - - - {/* 事件简介 */} - - {event.content ? `${event.content}(AI合成)` : '暂无内容'} - - - {/* 展开的详细信息 */} - - - - - 事件ID: {event.id} - - {event.source && ( - - 来源: {event.source} - - )} - {event.tags && event.tags.length > 0 && ( - - 标签: - {event.tags.map((tag, idx) => ( - - {tag} - - ))} - - )} - - - - - {/* 相关股票列表 Collapse */} - - - {loadingStocks[event.id] ? ( - - - 加载相关股票数据... - - ) : ( - - )} - - - - - - - - ); - })} - + {/* 超预期得分显示 */} + {expectationScore && ( + + + + + + 超预期得分: {expectationScore} + + + 基于历史事件判断当前事件的超预期情况,满分100分(AI合成) + + + - + )} + + {/* 历史事件卡片网格 */} + + {events.map((event) => { + const importanceColor = getImportanceColor(event.importance); + + return ( + + + {/* 事件名称 */} + + {event.title || '未命名事件'} + + + {/* 日期 + Badges */} + + + {formatDate(getEventDate(event))} + + + ({getRelativeTime(getEventDate(event))}) + + {event.relevance && ( + + 相关度: {event.relevance} + + )} + {event.importance && ( + + 重要性: {event.importance} + + )} + + + {/* 事件描述 */} + + {getEventContent(event) ? `${getEventContent(event)}(AI合成)` : '暂无内容'} + + + {/* 相关股票按钮 */} + + + + ); + })} + + + {/* 相关股票 Modal - 条件渲染 */} + {stocksModalOpen && ( + + + + + {selectedEventForStocks?.title || '历史事件相关股票'} + + + + {loadingStocks[selectedEventForStocks?.id] ? ( + + + 加载相关股票数据... + + ) : ( + + )} + + + + )} ); }; -// 股票列表子组件 +// 股票列表子组件(卡片式布局) const StocksList = ({ stocks, eventTradingDate }) => { - const textSecondary = useColorModeValue('gray.600', 'gray.400'); + const [expandedStocks, setExpandedStocks] = useState(new Set()); - // 处理股票代码,移除.SZ/.SH后缀 - const formatStockCode = (stockCode) => { - if (!stockCode) return ''; - return stockCode.replace(/\.(SZ|SH)$/i, ''); - }; + const cardBg = useColorModeValue('white', 'gray.800'); + const borderColor = useColorModeValue('gray.200', 'gray.700'); + const dividerColor = useColorModeValue('gray.200', 'gray.600'); + const textSecondary = useColorModeValue('gray.600', 'gray.400'); + const nameColor = useColorModeValue('gray.700', 'gray.300'); // 处理关联描述字段的辅助函数 const getRelationDesc = (relationDesc) => { @@ -493,9 +361,41 @@ const StocksList = ({ stocks, eventTradingDate }) => { return ''; }; + // 切换展开状态 + const toggleExpand = (stockId) => { + const newExpanded = new Set(expandedStocks); + if (newExpanded.has(stockId)) { + newExpanded.delete(stockId); + } else { + newExpanded.add(stockId); + } + setExpandedStocks(newExpanded); + }; + + // 格式化涨跌幅 + const formatChange = (value) => { + if (value === null || value === undefined || isNaN(value)) return '--'; + const prefix = value > 0 ? '+' : ''; + return `${prefix}${parseFloat(value).toFixed(2)}%`; + }; + + // 获取涨跌幅颜色 + const getChangeColor = (value) => { + const num = parseFloat(value); + if (isNaN(num) || num === 0) return 'gray.500'; + return num > 0 ? 'red.500' : 'green.500'; + }; + + // 获取相关度颜色 + const getCorrelationColor = (correlation) => { + if (correlation >= 0.8) return 'red'; + if (correlation >= 0.6) return 'orange'; + return 'green'; + }; + if (!stocks || stocks.length === 0) { return ( - + 暂无相关股票数据 该历史事件暂未关联股票信息 @@ -505,6 +405,7 @@ const StocksList = ({ stocks, eventTradingDate }) => { return ( <> + {/* 事件交易日提示 */} {eventTradingDate && ( @@ -512,74 +413,115 @@ const StocksList = ({ stocks, eventTradingDate }) => { )} - - - - - - - - - - - - - - {stocks.map((stock, index) => ( - - - - - - - - - ))} - -
股票代码股票名称板块相关度事件日涨幅关联原因
- - {stock.stock_code ? stock.stock_code.replace(/\.(SZ|SH)$/i, '') : ''} - - {stock.stock_name || '--'} - - {stock.sector || '未知'} - - - = 0.8 ? 'red' : - stock.correlation >= 0.6 ? 'orange' : 'green' - } - size="sm" - > - {Math.round((stock.correlation || 0) * 100)}% - - - {stock.event_day_change_pct !== null && stock.event_day_change_pct !== undefined ? ( - = 0 ? 'red.500' : 'green.500'} - > - {stock.event_day_change_pct >= 0 ? '+' : ''}{stock.event_day_change_pct.toFixed(2)}% - - ) : ( - -- - )} - + + {/* 股票卡片网格 */} + + {stocks.map((stock, index) => { + const stockId = stock.id || index; + const isExpanded = expandedStocks.has(stockId); + const cleanCode = stock.stock_code ? stock.stock_code.replace(/\.(SZ|SH)$/i, '') : ''; + const relationDesc = getRelationDesc(stock.relation_desc); + const needTruncate = relationDesc && relationDesc.length > 50; + + return ( + + + {/* 顶部:股票代码 + 名称 + 涨跌幅 */} + - - {getRelationDesc(stock.relation_desc) ? `${getRelationDesc(stock.relation_desc)}(AI合成)` : '--'} + + {cleanCode} + + + {stock.stock_name || '--'} -
-
+ + + {formatChange(stock.event_day_change_pct)} + + + + {/* 分隔线 */} + + + {/* 板块和相关度 */} + + + 板块: + + {stock.sector || '未知'} + + + + + 相关度: + + {Math.round((stock.correlation || 0) * 100)}% + + + + + {/* 分隔线 */} + + + {/* 关联原因 */} + {relationDesc && ( + + + 关联原因: + + + + {relationDesc}(AI合成) + + + {needTruncate && ( + + )} + + )} + + + ); + })} + ); }; -export default HistoricalEvents; \ No newline at end of file +export default HistoricalEvents;