// src/views/EventDetail/components/HistoricalEvents.js import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { Box, VStack, HStack, Text, Badge, Button, Skeleton, Alert, AlertIcon, SimpleGrid, Icon, useColorModeValue, Spinner, Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, Link, Flex, Collapse } from '@chakra-ui/react'; import { FaChartLine, FaInfoCircle } from 'react-icons/fa'; import { stockService } from '../../../services/eventService'; import { logger } from '../../../utils/logger'; import CitedContent from '../../../components/Citation/CitedContent'; const HistoricalEvents = ({ events = [], expectationScore = null, loading = false, error = null }) => { const navigate = useNavigate(); // 状态管理 const [selectedEventForStocks, setSelectedEventForStocks] = useState(null); const [stocksModalOpen, setStocksModalOpen] = useState(false); const [eventStocks, setEventStocks] = useState({}); const [loadingStocks, setLoadingStocks] = useState({}); // 颜色主题 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 getEventDate = (event) => { return event?.event_date || event?.created_at || event?.date || event?.publish_time; }; const getEventContent = (event) => { return event?.content || event?.description || event?.summary; }; // 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]); // 点击相关股票按钮 const handleViewStocks = async (event) => { setSelectedEventForStocks(event); setStocksModalOpen(true); // 如果已经加载过该事件的股票数据,不再重复加载 if (eventStocks[event.id]) { return; } // 标记为加载中 setLoadingStocks(prev => ({ ...prev, [event.id]: true })); try { // 调用API获取历史事件相关股票 const response = await stockService.getHistoricalEventStocks(event.id); setEventStocks(prev => ({ ...prev, [event.id]: response.data || [] })); } catch (err) { logger.error('HistoricalEvents', 'handleViewStocks', err, { eventId: event.id, eventTitle: event.title }); setEventStocks(prev => ({ ...prev, [event.id]: [] })); } finally { setLoadingStocks(prev => ({ ...prev, [event.id]: false })); } }; // 关闭弹窗 const handleCloseModal = () => { setStocksModalOpen(false); setSelectedEventForStocks(null); }; // 处理卡片点击跳转到事件详情页 const handleCardClick = (event) => { navigate(`/event-detail/${event.id}`); }; // 获取重要性颜色 const getImportanceColor = (importance) => { if (importance >= 4) return 'red'; if (importance >= 2) return 'orange'; return 'green'; }; // 获取相关度颜色(1-10) const getSimilarityColor = (similarity) => { if (similarity >= 8) return 'green'; if (similarity >= 6) return 'blue'; if (similarity >= 4) return 'orange'; return 'gray'; }; // 格式化日期 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)}年前`; }; // 加载状态 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) => { const importanceColor = getImportanceColor(event.importance); return ( handleCardClick(event)} _before={{ content: '""', position: 'absolute', top: 0, left: 0, right: 0, height: '3px', bgGradient: 'linear(to-r, blue.400, purple.500, pink.500)', borderTopLeftRadius: 'lg', borderTopRightRadius: 'lg', }} _hover={{ boxShadow: 'lg', borderColor: 'blue.400', }} transition="all 0.2s" > {/* 顶部区域:左侧(标题+时间) + 右侧(按钮) */} {/* 左侧:标题 + 时间信息(允许折行) */} {/* 标题 */} { e.stopPropagation(); handleCardClick(event); }} _hover={{ textDecoration: 'underline' }} > {event.title || '未命名事件'} {/* 时间 + Badges(允许折行) */} {formatDate(getEventDate(event))} ({getRelativeTime(getEventDate(event))}) {event.importance && ( 重要性: {event.importance} )} {event.avg_change_pct !== undefined && event.avg_change_pct !== null && ( 0 ? 'red' : event.avg_change_pct < 0 ? 'green' : 'gray'} size="sm" > 涨幅: {event.avg_change_pct > 0 ? '+' : ''}{event.avg_change_pct.toFixed(2)}% )} {event.similarity !== undefined && event.similarity !== null && ( 相关度: {event.similarity} )} {/* 右侧:相关股票按钮 */} {/* 底部:描述(独占整行)- 升级和降级处理 */} {(() => { const content = getEventContent(event); // 检查是否有 data 结构(升级版本) if (content && typeof content === 'object' && content.data) { return ( ); } // 降级版本:纯文本 return ( {content ? `${content}(AI合成)` : '暂无内容'} ); })()} ); })} {/* 相关股票 Modal - 条件渲染 */} {stocksModalOpen && ( {selectedEventForStocks?.title || '历史事件相关股票'} {loadingStocks[selectedEventForStocks?.id] ? ( 加载相关股票数据... ) : ( )} )} ); }; // 股票列表子组件(卡片式布局) const StocksList = ({ stocks, eventTradingDate }) => { const [expandedStocks, setExpandedStocks] = useState(new Set()); 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) => { // 处理空值 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 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 ( 暂无相关股票数据 该历史事件暂未关联股票信息 ); } return ( <> {/* 事件交易日提示 */} {eventTradingDate && ( 📅 事件对应交易日:{new Date(eventTradingDate).toLocaleDateString('zh-CN')} )} {/* 股票卡片网格 */} {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 ( {/* 顶部:股票代码 + 名称 + 涨跌幅 */} {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;