feat: 添加事件详情面板

This commit is contained in:
zdl
2025-10-31 14:38:43 +08:00
parent c372832f1f
commit 9fd9fcb731
3 changed files with 346 additions and 7 deletions

View File

@@ -817,6 +817,105 @@ export function generatePopularKeywords(limit = 20) {
}));
}
/**
* 生成历史事件对比数据
* @param {string} industry - 行业
* @param {number} index - 索引
* @returns {Array} - 历史事件列表
*/
function generateHistoricalEvents(industry, index) {
const historicalCount = 3 + (index % 3); // 3-5个历史事件
const historical = [];
const baseDate = new Date();
for (let i = 0; i < historicalCount; i++) {
// 生成过去1-6个月的随机时间
const monthsAgo = 1 + Math.floor(Math.random() * 6);
const eventDate = new Date(baseDate);
eventDate.setMonth(eventDate.getMonth() - monthsAgo);
const similarityScore = 0.6 + Math.random() * 0.35; // 60%-95%相似度
historical.push({
id: `hist_${industry}_${index}_${i}`,
title: generateEventTitle(industry, i + index * 10),
created_at: eventDate.toISOString(),
related_avg_chg: parseFloat((Math.random() * 15 - 3).toFixed(2)),
related_max_chg: parseFloat((Math.random() * 25).toFixed(2)),
similarity_score: parseFloat(similarityScore.toFixed(2)),
view_count: Math.floor(Math.random() * 3000) + 500,
});
}
// 按相似度排序
historical.sort((a, b) => b.similarity_score - a.similarity_score);
return historical;
}
/**
* 生成传导链数据
* @param {string} industry - 行业
* @param {number} index - 索引
* @returns {Object} - 传导链数据 { nodes, edges }
*/
function generateTransmissionChain(industry, index) {
const nodeTypes = ['event', 'industry', 'company', 'policy', 'technology', 'market'];
const impactTypes = ['positive', 'negative', 'neutral', 'mixed'];
const strengthLevels = ['strong', 'medium', 'weak'];
const nodes = [];
const edges = [];
// 主事件节点
nodes.push({
id: 1,
name: '主事件',
type: 'event',
extra: { is_main_event: true, description: `${industry}重要事件` }
});
// 生成5-8个相关节点
const nodeCount = 5 + (index % 4);
for (let i = 2; i <= nodeCount; i++) {
const nodeType = nodeTypes[i % nodeTypes.length];
const industryStock = stockPool.find(s => s.industry === industry);
let nodeName;
if (nodeType === 'company' && industryStock) {
nodeName = industryStock.name;
} else if (nodeType === 'industry') {
nodeName = `${industry}产业`;
} else if (nodeType === 'policy') {
nodeName = '相关政策';
} else if (nodeType === 'technology') {
nodeName = '技术创新';
} else if (nodeType === 'market') {
nodeName = '市场需求';
} else {
nodeName = `节点${i}`;
}
nodes.push({
id: i,
name: nodeName,
type: nodeType,
extra: { description: `${nodeName}相关信息` }
});
// 创建与主事件或其他节点的连接
const targetId = i === 2 ? 1 : Math.max(1, Math.floor(Math.random() * (i - 1)) + 1);
edges.push({
source: targetId,
target: i,
impact: impactTypes[i % impactTypes.length],
strength: strengthLevels[i % strengthLevels.length],
description: `传导路径${i}`
});
}
return { nodes, edges };
}
/**
* 生成动态新闻事件(实时要闻·动态追踪专用)
* @param {Object} timeRange - 时间范围 { startTime, endTime }
@@ -855,23 +954,40 @@ export function generateDynamicNewsEvents(timeRange = null, count = 30) {
const relatedMaxChg = (Math.random() * 25).toFixed(2); // 0% 到 25%
const relatedWeekChg = (Math.random() * 30 - 10).toFixed(2); // -10% 到 20%
// 为每个事件随机选择2-5个相关股票
// 为每个事件随机选择2-5个相关股票(完整对象)
const relatedStockCount = 2 + (i % 4);
const relatedStocks = [];
const industryStocks = stockPool.filter(s => s.industry === industry);
const relationDescriptions = [
'直接受益标的',
'产业链上游企业',
'产业链下游企业',
'行业龙头企业',
'潜在受益标的',
'概念相关个股'
];
// 优先选择同行业股票
if (industryStocks.length > 0) {
for (let j = 0; j < Math.min(relatedStockCount, industryStocks.length); j++) {
relatedStocks.push(industryStocks[j % industryStocks.length].stock_code);
const stock = industryStocks[j % industryStocks.length];
relatedStocks.push({
stock_code: stock.stock_code,
stock_name: stock.name,
relation_desc: relationDescriptions[j % relationDescriptions.length]
});
}
}
// 如果同行业股票不够,从整个 stockPool 中补充
while (relatedStocks.length < relatedStockCount && relatedStocks.length < stockPool.length) {
const randomStock = stockPool[relatedStocks.length % stockPool.length];
if (!relatedStocks.includes(randomStock.stock_code)) {
relatedStocks.push(randomStock.stock_code);
if (!relatedStocks.some(s => s.stock_code === randomStock.stock_code)) {
relatedStocks.push({
stock_code: randomStock.stock_code,
stock_name: randomStock.name,
relation_desc: relationDescriptions[relatedStocks.length % relationDescriptions.length]
});
}
}
@@ -896,6 +1012,8 @@ export function generateDynamicNewsEvents(timeRange = null, count = 30) {
is_ai_generated: i % 3 === 0, // 33% 的事件是AI生成
industry: industry,
related_stocks: relatedStocks,
historical_events: generateHistoricalEvents(industry, i),
transmission_chain: generateTransmissionChain(industry, i),
creator: {
username: authorPool[i % authorPool.length],
avatar_url: null

View File

@@ -1,7 +1,7 @@
// src/views/Community/components/DynamicNewsCard.js
// 横向滚动事件卡片组件(实时要闻·动态追踪)
import React, { forwardRef, useRef, useState } from 'react';
import React, { forwardRef, useRef, useState, useEffect } from 'react';
import {
Card,
CardHeader,
@@ -20,6 +20,7 @@ import {
} from '@chakra-ui/react';
import { ChevronLeftIcon, ChevronRightIcon, TimeIcon } from '@chakra-ui/icons';
import DynamicNewsEventCard from './EventCard/DynamicNewsEventCard';
import DynamicNewsDetailPanel from './DynamicNewsDetailPanel';
/**
* 实时要闻·动态追踪 - 横向滚动卡片组件
@@ -43,6 +44,14 @@ const DynamicNewsCard = forwardRef(({
const scrollContainerRef = useRef(null);
const [showLeftArrow, setShowLeftArrow] = useState(false);
const [showRightArrow, setShowRightArrow] = useState(true);
const [selectedEvent, setSelectedEvent] = useState(null);
// 默认选中第一个事件
useEffect(() => {
if (events && events.length > 0 && !selectedEvent) {
setSelectedEvent(events[0]);
}
}, [events, selectedEvent]);
// 滚动到左侧
const scrollLeft = () => {
@@ -214,11 +223,15 @@ const DynamicNewsCard = forwardRef(({
index={index}
isFollowing={false}
followerCount={event.follower_count || 0}
onEventClick={onEventClick}
onEventClick={(clickedEvent) => {
setSelectedEvent(clickedEvent);
if (onEventClick) onEventClick(clickedEvent);
}}
onTitleClick={(e) => {
e.preventDefault();
e.stopPropagation();
onEventClick(event);
setSelectedEvent(event);
if (onEventClick) onEventClick(event);
}}
onToggleFollow={() => {}}
timelineStyle={getTimelineBoxStyle()}
@@ -229,6 +242,13 @@ const DynamicNewsCard = forwardRef(({
</Flex>
</Box>
)}
{/* 详情面板 */}
{!loading && events && events.length > 0 && (
<Box mt={6}>
<DynamicNewsDetailPanel event={selectedEvent} />
</Box>
)}
</CardBody>
</Card>
);

View File

@@ -0,0 +1,201 @@
// src/views/Community/components/DynamicNewsDetailPanel.js
// 动态新闻详情面板组件(固定展示,非弹窗)
import React from 'react';
import {
Box,
Card,
CardBody,
VStack,
HStack,
Text,
Badge,
Tag,
Divider,
Heading,
List,
ListItem,
Button,
useColorModeValue,
} from '@chakra-ui/react';
import moment from 'moment';
import { getImportanceConfig } from '../../../constants/importanceLevels';
import HistoricalEvents from '../../EventDetail/components/HistoricalEvents';
import TransmissionChainAnalysis from '../../EventDetail/components/TransmissionChainAnalysis';
import { eventService } from '../../../services/eventService';
/**
* 动态新闻详情面板组件
* @param {Object} props
* @param {Object} props.event - 事件对象(包含详情数据)
*/
const DynamicNewsDetailPanel = ({ event }) => {
const cardBg = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.700');
const headingColor = useColorModeValue('gray.700', 'gray.200');
const textColor = useColorModeValue('gray.600', 'gray.400');
if (!event) {
return (
<Card bg={cardBg} borderColor={borderColor} borderWidth="1px">
<CardBody>
<Text color={textColor} textAlign="center">请选择一个事件查看详情</Text>
</CardBody>
</Card>
);
}
const importance = getImportanceConfig(event.importance);
// 渲染涨跌幅标签
const renderPriceTag = (value, label) => {
if (value === null || value === undefined) return `${label}: --`;
const color = value > 0 ? 'red' : value < 0 ? 'green' : 'gray';
const prefix = value > 0 ? '+' : '';
return (
<Tag colorScheme={color} mr={2}>
{label}: {prefix}{value.toFixed(2)}%
</Tag>
);
};
return (
<Card bg={cardBg} borderColor={borderColor} borderWidth="1px">
<CardBody>
<VStack align="stretch" spacing={4}>
{/* 标题 */}
<Heading size="md" color={headingColor}>
{event.title}
</Heading>
<Divider />
{/* 基本信息 */}
<VStack align="stretch" spacing={2}>
<HStack spacing={4} flexWrap="wrap">
<Text fontSize="sm" color={textColor}>
<strong>创建时间</strong>
{moment(event.created_at).format('YYYY-MM-DD HH:mm:ss')}
</Text>
<Text fontSize="sm" color={textColor}>
<strong>重要性</strong>
<Badge colorScheme={importance.level === 'S' ? 'purple' : importance.level === 'A' ? 'red' : importance.level === 'B' ? 'orange' : 'green'} ml={1}>
{importance.level}
</Badge>
</Text>
<Text fontSize="sm" color={textColor}>
<strong>浏览数</strong>{event.view_count || 0}
</Text>
</HStack>
{/* 涨跌幅统计 */}
<HStack spacing={2} flexWrap="wrap">
{renderPriceTag(event.related_avg_chg, '平均涨幅')}
{renderPriceTag(event.related_max_chg, '最大涨幅')}
{renderPriceTag(event.related_week_chg, '周涨幅')}
</HStack>
</VStack>
<Divider />
{/* 事件描述 */}
{event.description && (
<Box>
<Heading size="sm" color={headingColor} mb={2}>
事件描述
</Heading>
<Text fontSize="sm" color={textColor} lineHeight="tall">
{event.description}
</Text>
</Box>
)}
{/* 相关概念 */}
{event.keywords && event.keywords.length > 0 && (
<Box>
<Heading size="sm" color={headingColor} mb={2}>
相关概念
</Heading>
<HStack spacing={2} flexWrap="wrap">
{event.keywords.map((keyword, index) => (
<Tag key={index} colorScheme="blue" size="sm">
{keyword}
</Tag>
))}
</HStack>
</Box>
)}
{/* 相关股票 */}
{event.related_stocks && event.related_stocks.length > 0 && (
<Box>
<Heading size="sm" color={headingColor} mb={2}>
相关股票
</Heading>
<List spacing={2}>
{event.related_stocks.map((stock, index) => (
<ListItem
key={index}
p={2}
borderWidth="1px"
borderColor={borderColor}
borderRadius="md"
>
<HStack justify="space-between">
<VStack align="start" spacing={0}>
<Text fontSize="sm" fontWeight="bold">
{stock.stock_name} ({stock.stock_code})
</Text>
<Text fontSize="xs" color={textColor}>
{stock.relation_desc || '相关股票'}
</Text>
</VStack>
<Button
size="sm"
colorScheme="blue"
onClick={() => {
const stockCode = stock.stock_code.split('.')[0];
window.open(`https://valuefrontier.cn/company?scode=${stockCode}`, '_blank');
}}
>
查看详情
</Button>
</HStack>
</ListItem>
))}
</List>
</Box>
)}
{/* 历史事件对比 */}
<Box>
<Heading size="sm" color={headingColor} mb={2}>
历史事件对比
</Heading>
<HistoricalEvents
eventId={event.id}
eventTitle={event.title}
historicalEvents={event.historical_events || []}
eventService={eventService}
/>
</Box>
{/* 传导链分析 */}
<Box>
<Heading size="sm" color={headingColor} mb={2}>
传导链分析
</Heading>
<TransmissionChainAnalysis
eventId={event.id}
eventService={eventService}
/>
</Box>
</VStack>
</CardBody>
</Card>
);
};
export default DynamicNewsDetailPanel;