// src/views/EventDetail/components/HistoricalEvents.js import React, { useState, useEffect } from 'react'; import { Box, VStack, HStack, Text, Badge, Button, Collapse, Skeleton, Alert, AlertIcon, Card, CardBody, CardHeader, Divider, Icon, useColorModeValue, Tooltip, Spinner, Table, Thead, Tbody, Tr, Th, Td, TableContainer, Link } from '@chakra-ui/react'; import { FaExclamationTriangle, FaClock, FaCalendarAlt, FaChartLine, FaEye, FaTimes, FaInfoCircle, FaChevronDown, FaChevronUp } 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()); // 追踪哪些事件的股票列表被展开 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 toggleEventExpansion = (eventId) => { const newExpanded = new Set(expandedEvents); if (newExpanded.has(eventId)) { newExpanded.delete(eventId); } else { newExpanded.add(eventId); } setExpandedEvents(newExpanded); }; // 切换股票列表展开状态 const toggleStocksExpansion = async (event) => { const eventId = event.id; const newExpanded = new Set(expandedStocks); // 如果正在收起,直接更新状态 if (newExpanded.has(eventId)) { newExpanded.delete(eventId); setExpandedStocks(newExpanded); return; } // 如果正在展开,先展开再加载数据 newExpanded.add(eventId); setExpandedStocks(newExpanded); // 如果已经加载过该事件的股票数据,不再重复加载 if (eventStocks[eventId]) { return; } // 标记为加载中 setLoadingStocks(prev => ({ ...prev, [eventId]: true })); try { // 调用API获取历史事件相关股票 const response = await stockService.getHistoricalEventStocks(eventId); setEventStocks(prev => ({ ...prev, [eventId]: response.data || [] })); } catch (err) { logger.error('HistoricalEvents', 'toggleStocksExpansion', err, { eventId: eventId, eventTitle: event.title }); setEventStocks(prev => ({ ...prev, [eventId]: [] })); } finally { setLoadingStocks(prev => ({ ...prev, [eventId]: false })); } }; // 获取重要性图标 const getImportanceIcon = (importance) => { if (importance >= 4) return FaExclamationTriangle; if (importance >= 2) return FaCalendarAlt; return FaClock; }; // 获取重要性颜色 const getImportanceColor = (importance) => { if (importance >= 4) return 'red'; if (importance >= 2) return 'orange'; return 'green'; }; // 格式化日期 const formatDate = (dateString) => { if (!dateString) return '日期未知'; return new Date(dateString).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }); }; // 计算相对时间 const getRelativeTime = (dateString) => { if (!dateString) return ''; const date = new Date(dateString); const now = new Date(); const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24)); if (diffDays < 30) return `${diffDays}天前`; if (diffDays < 365) return `${Math.floor(diffDays / 30)}个月前`; 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) => ( ))} ); } // 错误状态 if (error) { return ( 加载历史事件失败: {error} ); } // 无数据状态 if (!events || events.length === 0) { return ( 暂无历史事件 历史事件数据将在这里显示 ); } 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] ? ( 加载相关股票数据... ) : ( )} ); })} ); }; // 股票列表子组件 const StocksList = ({ stocks, eventTradingDate }) => { const textSecondary = useColorModeValue('gray.600', 'gray.400'); // 处理股票代码,移除.SZ/.SH后缀 const formatStockCode = (stockCode) => { if (!stockCode) return ''; return stockCode.replace(/\.(SZ|SH)$/i, ''); }; // 处理关联描述字段的辅助函数 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 ''; }; if (!stocks || stocks.length === 0) { return ( 暂无相关股票数据 该历史事件暂未关联股票信息 ); } return ( <> {eventTradingDate && ( 📅 事件对应交易日:{new Date(eventTradingDate).toLocaleDateString('zh-CN')} )} {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)}% ) : ( -- )} {getRelationDesc(stock.relation_desc) ? `${getRelationDesc(stock.relation_desc)}(AI合成)` : '--'}
); }; export default HistoricalEvents;