// 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 { Tag } from 'antd';
import { RobotOutlined } from '@ant-design/icons';
import { stockService } from '@services/eventService';
import { logger } from '@utils/logger';
import CitedContent from '@components/Citation/CitedContent';
import { PROFESSIONAL_COLORS } from '@constants/professionalTheme';
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 = PROFESSIONAL_COLORS.background.card;
const borderColor = PROFESSIONAL_COLORS.border.default;
const textSecondary = PROFESSIONAL_COLORS.text.secondary;
const nameColor = PROFESSIONAL_COLORS.text.primary;
// 字段兼容函数
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'}
fontSize="xs"
px={2}
>
涨幅: {event.avg_change_pct > 0 ? '+' : ''}{event.avg_change_pct.toFixed(2)}%
)}
{event.similarity !== undefined && event.similarity !== null && (
相关度: {event.similarity}
)}
{/* 右侧:相关股票按钮 */}
}
onClick={(e) => {
e.stopPropagation();
handleViewStocks(event);
}}
colorScheme="blue"
variant="outline"
flexShrink={0}
>
相关股票
{/* 底部:描述(独占整行)- 升级和降级处理 */}
{(() => {
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;