feat: 添加事件详情面板
This commit is contained in:
@@ -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 }
|
* @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 relatedMaxChg = (Math.random() * 25).toFixed(2); // 0% 到 25%
|
||||||
const relatedWeekChg = (Math.random() * 30 - 10).toFixed(2); // -10% 到 20%
|
const relatedWeekChg = (Math.random() * 30 - 10).toFixed(2); // -10% 到 20%
|
||||||
|
|
||||||
// 为每个事件随机选择2-5个相关股票
|
// 为每个事件随机选择2-5个相关股票(完整对象)
|
||||||
const relatedStockCount = 2 + (i % 4);
|
const relatedStockCount = 2 + (i % 4);
|
||||||
const relatedStocks = [];
|
const relatedStocks = [];
|
||||||
const industryStocks = stockPool.filter(s => s.industry === industry);
|
const industryStocks = stockPool.filter(s => s.industry === industry);
|
||||||
|
const relationDescriptions = [
|
||||||
|
'直接受益标的',
|
||||||
|
'产业链上游企业',
|
||||||
|
'产业链下游企业',
|
||||||
|
'行业龙头企业',
|
||||||
|
'潜在受益标的',
|
||||||
|
'概念相关个股'
|
||||||
|
];
|
||||||
|
|
||||||
// 优先选择同行业股票
|
// 优先选择同行业股票
|
||||||
if (industryStocks.length > 0) {
|
if (industryStocks.length > 0) {
|
||||||
for (let j = 0; j < Math.min(relatedStockCount, industryStocks.length); j++) {
|
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 中补充
|
// 如果同行业股票不够,从整个 stockPool 中补充
|
||||||
while (relatedStocks.length < relatedStockCount && relatedStocks.length < stockPool.length) {
|
while (relatedStocks.length < relatedStockCount && relatedStocks.length < stockPool.length) {
|
||||||
const randomStock = stockPool[relatedStocks.length % stockPool.length];
|
const randomStock = stockPool[relatedStocks.length % stockPool.length];
|
||||||
if (!relatedStocks.includes(randomStock.stock_code)) {
|
if (!relatedStocks.some(s => s.stock_code === randomStock.stock_code)) {
|
||||||
relatedStocks.push(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生成
|
is_ai_generated: i % 3 === 0, // 33% 的事件是AI生成
|
||||||
industry: industry,
|
industry: industry,
|
||||||
related_stocks: relatedStocks,
|
related_stocks: relatedStocks,
|
||||||
|
historical_events: generateHistoricalEvents(industry, i),
|
||||||
|
transmission_chain: generateTransmissionChain(industry, i),
|
||||||
creator: {
|
creator: {
|
||||||
username: authorPool[i % authorPool.length],
|
username: authorPool[i % authorPool.length],
|
||||||
avatar_url: null
|
avatar_url: null
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/views/Community/components/DynamicNewsCard.js
|
// src/views/Community/components/DynamicNewsCard.js
|
||||||
// 横向滚动事件卡片组件(实时要闻·动态追踪)
|
// 横向滚动事件卡片组件(实时要闻·动态追踪)
|
||||||
|
|
||||||
import React, { forwardRef, useRef, useState } from 'react';
|
import React, { forwardRef, useRef, useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { ChevronLeftIcon, ChevronRightIcon, TimeIcon } from '@chakra-ui/icons';
|
import { ChevronLeftIcon, ChevronRightIcon, TimeIcon } from '@chakra-ui/icons';
|
||||||
import DynamicNewsEventCard from './EventCard/DynamicNewsEventCard';
|
import DynamicNewsEventCard from './EventCard/DynamicNewsEventCard';
|
||||||
|
import DynamicNewsDetailPanel from './DynamicNewsDetailPanel';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 实时要闻·动态追踪 - 横向滚动卡片组件
|
* 实时要闻·动态追踪 - 横向滚动卡片组件
|
||||||
@@ -43,6 +44,14 @@ const DynamicNewsCard = forwardRef(({
|
|||||||
const scrollContainerRef = useRef(null);
|
const scrollContainerRef = useRef(null);
|
||||||
const [showLeftArrow, setShowLeftArrow] = useState(false);
|
const [showLeftArrow, setShowLeftArrow] = useState(false);
|
||||||
const [showRightArrow, setShowRightArrow] = useState(true);
|
const [showRightArrow, setShowRightArrow] = useState(true);
|
||||||
|
const [selectedEvent, setSelectedEvent] = useState(null);
|
||||||
|
|
||||||
|
// 默认选中第一个事件
|
||||||
|
useEffect(() => {
|
||||||
|
if (events && events.length > 0 && !selectedEvent) {
|
||||||
|
setSelectedEvent(events[0]);
|
||||||
|
}
|
||||||
|
}, [events, selectedEvent]);
|
||||||
|
|
||||||
// 滚动到左侧
|
// 滚动到左侧
|
||||||
const scrollLeft = () => {
|
const scrollLeft = () => {
|
||||||
@@ -214,11 +223,15 @@ const DynamicNewsCard = forwardRef(({
|
|||||||
index={index}
|
index={index}
|
||||||
isFollowing={false}
|
isFollowing={false}
|
||||||
followerCount={event.follower_count || 0}
|
followerCount={event.follower_count || 0}
|
||||||
onEventClick={onEventClick}
|
onEventClick={(clickedEvent) => {
|
||||||
|
setSelectedEvent(clickedEvent);
|
||||||
|
if (onEventClick) onEventClick(clickedEvent);
|
||||||
|
}}
|
||||||
onTitleClick={(e) => {
|
onTitleClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onEventClick(event);
|
setSelectedEvent(event);
|
||||||
|
if (onEventClick) onEventClick(event);
|
||||||
}}
|
}}
|
||||||
onToggleFollow={() => {}}
|
onToggleFollow={() => {}}
|
||||||
timelineStyle={getTimelineBoxStyle()}
|
timelineStyle={getTimelineBoxStyle()}
|
||||||
@@ -229,6 +242,13 @@ const DynamicNewsCard = forwardRef(({
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 详情面板 */}
|
||||||
|
{!loading && events && events.length > 0 && (
|
||||||
|
<Box mt={6}>
|
||||||
|
<DynamicNewsDetailPanel event={selectedEvent} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
201
src/views/Community/components/DynamicNewsDetailPanel.js
Normal file
201
src/views/Community/components/DynamicNewsDetailPanel.js
Normal 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;
|
||||||
Reference in New Issue
Block a user