Files
vf_react/src/views/EventDetail/components/TransmissionChainAnalysis.js
2025-10-18 12:12:02 +08:00

1003 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState, useRef } from 'react';
import {
Box,
Button,
Flex,
Spinner,
Alert,
AlertIcon,
Text,
Stat,
StatLabel,
StatNumber,
HStack,
VStack,
Tag,
Badge,
List,
ListItem,
Divider,
CloseButton,
Grid,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
Icon,
useColorModeValue,
Tooltip
} from '@chakra-ui/react';
import { InfoIcon, ViewIcon } from '@chakra-ui/icons';
import ReactECharts from 'echarts-for-react';
import { eventService } from '../../../services/eventService';
import CitedContent from '../../../components/Citation/CitedContent';
import { logger } from '../../../utils/logger';
// 节点样式配置 - 完全复刻Flask版本
const NODE_STYLES = {
'event': { color: '#ff4757', symbol: 'diamond', size: 'large' },
'industry': { color: '#00d2d3', symbol: 'rect', size: 'medium' },
'company': { color: '#54a0ff', symbol: 'circle', size: 'medium' },
'policy': { color: '#10ac84', symbol: 'triangle', size: 'medium' },
'technology': { color: '#ee5a6f', symbol: 'roundRect', size: 'medium' },
'market': { color: '#ffd93d', symbol: 'diamond', size: 'medium' },
'other': { color: '#a4b0be', symbol: 'circle', size: 'small' }
};
// 影响方向颜色映射 - 完全复刻Flask版本
const IMPACT_COLORS = {
'positive': '#52c41a',
'negative': '#ff4d4f',
'neutral': '#999',
'mixed': '#faad14'
};
// 节点类型标签映射
const NODE_TYPE_LABELS = {
'event': '事件',
'industry': '行业',
'company': '公司',
'policy': '政策',
'technology': '技术',
'market': '市场',
'other': '其他'
};
// 过滤孤立节点 - 完全复刻Flask版本
function filterIsolatedNodes(nodes, edges) {
logger.debug('TransmissionChain', '开始过滤孤立节点', {
nodesCount: nodes?.length,
edgesCount: edges?.length
});
if (!nodes || !edges) {
logger.debug('TransmissionChain', '节点或边数据为空');
return [];
}
const connectedNodeIds = new Set();
edges.forEach(edge => {
connectedNodeIds.add(String(edge.source));
connectedNodeIds.add(String(edge.target));
});
// 如果图中只有一个节点且是主事件,也显示它
const mainEventNode = nodes.find(n => n.extra?.is_main_event);
if (mainEventNode) {
connectedNodeIds.add(String(mainEventNode.id));
}
const filteredNodes = nodes.filter(node => {
return connectedNodeIds.has(String(node.id));
});
logger.debug('TransmissionChain', '过滤完成', {
originalCount: nodes.length,
filteredCount: filteredNodes.length,
connectedNodesCount: connectedNodeIds.size
});
return filteredNodes;
}
// 计算节点连接数
function calculateNodeConnections(nodeId, edges) {
return edges.filter(edge =>
String(edge.source) === String(nodeId) || String(edge.target) === String(nodeId)
).length;
}
// 计算节点大小 - 基于连接数,增大整体尺寸方便点击
function calculateNodeSize(connectionCount, isMainEvent = false) {
const baseSize = 35; // 增加基础大小
const maxSize = 70; // 增加最大大小
const minSize = 25; // 增加最小大小
// 主事件节点稍大一些
if (isMainEvent) {
return Math.min(maxSize, baseSize + 20);
}
// 基于连接数计算大小,使用对数函数避免差距过大
const sizeMultiplier = Math.log(connectionCount + 1) * 6;
return Math.min(maxSize, Math.max(minSize, baseSize + sizeMultiplier));
}
// 计算边的宽度 - 基于强度,但差距不要过大
function calculateEdgeWidth(strength) {
const minWidth = 1;
const maxWidth = 4;
if (typeof strength === 'number') {
const normalizedStrength = strength / 100; // 假设强度是0-100
return minWidth + (maxWidth - minWidth) * normalizedStrength;
}
// 如果是字符串类型的强度
switch(strength) {
case 'strong': return 3;
case 'medium': return 2;
case 'weak': return 1;
default: return 2;
}
}
// 力导向图配置 - 完全复刻Flask版本
function getGraphOption(data) {
logger.debug('TransmissionChain', 'getGraphOption被调用', {
hasData: !!data,
nodesCount: data?.nodes?.length,
edgesCount: data?.edges?.length
});
if (!data || !data.nodes || !data.edges || data.nodes.length === 0) {
logger.debug('TransmissionChain', '数据为空或无效');
return {
title: { text: '暂无传导链数据', left: 'center', top: 'center' },
graphic: { type: 'text', left: 'center', top: '60%', style: { text: '当前事件暂无传导链分析数据', fontSize: 14 } }
};
}
const filteredNodes = filterIsolatedNodes(data.nodes, data.edges);
// 进一步过滤:不显示事件类型的节点
const nonEventNodes = filteredNodes.filter(node => node.extra?.node_type !== 'event');
logger.debug('TransmissionChain', '节点过滤结果', {
originalCount: data.nodes.length,
filteredCount: filteredNodes.length,
nonEventCount: nonEventNodes.length
});
if (nonEventNodes.length === 0) {
logger.debug('TransmissionChain', '过滤后没有有效节点');
return {
title: { text: '暂无有效节点数据', left: 'center', top: 'center' },
graphic: { type: 'text', left: 'center', top: '60%', style: { text: '当前事件的传导链节点均为孤立节点', fontSize: 14 } }
};
}
// 生成节点类别(排除事件类型)
const categories = [...new Set(nonEventNodes.map(n => n.extra?.node_type || 'other'))].map(type => ({
name: NODE_TYPE_LABELS[type] || type,
itemStyle: { color: NODE_STYLES[type]?.color || NODE_STYLES.other.color }
}));
// 构建图表节点数据 - 完全复刻Flask版本样式排除事件节点
const chartNodes = nonEventNodes.map(node => {
const nodeType = node.extra?.node_type || 'other';
const nodeStyle = NODE_STYLES[nodeType] || NODE_STYLES['other'];
const connectionCount = calculateNodeConnections(node.id, data.edges);
return {
id: String(node.id),
name: node.name,
value: node.value,
symbol: nodeStyle.symbol,
symbolSize: calculateNodeSize(connectionCount, node.extra?.is_main_event),
category: NODE_TYPE_LABELS[nodeType] || nodeType,
itemStyle: {
color: nodeStyle.color,
borderColor: node.extra?.is_main_event ? '#ffd700' : '#fff',
borderWidth: node.extra?.is_main_event ? 3 : 1,
shadowBlur: 5,
shadowColor: 'rgba(0,0,0,0.2)'
},
label: {
show: true,
position: 'right',
formatter: '{b}',
fontSize: 12,
fontWeight: 'bold',
padding: [5, 8], // 增加标签内边距,扩大点击区域
backgroundColor: 'rgba(255,255,255,0.8)',
borderRadius: 4,
borderColor: '#ddd',
borderWidth: 1
},
emphasis: {
focus: 'none', // 减少高亮敏感性,但保持点击功能
label: {
show: true,
fontSize: 13,
fontWeight: 'bold'
},
itemStyle: {
borderWidth: 3,
shadowBlur: 10
}
},
// 扩大节点的实际点击区域
select: {
itemStyle: {
borderColor: '#007bff',
borderWidth: 3
}
},
extra: node.extra // 包含所有额外信息
};
});
// 构建图表边数据 - 完全复刻Flask版本样式
const chartEdges = data.edges.map(edge => ({
source: String(edge.source),
target: String(edge.target),
value: edge.value,
lineStyle: {
color: IMPACT_COLORS[edge.extra?.direction] || IMPACT_COLORS.neutral,
width: calculateEdgeWidth(edge.extra?.strength || 50),
curveness: edge.extra?.is_circular ? 0.3 : 0,
type: edge.extra?.is_circular ? 'dashed' : 'solid', // 关键:循环边用虚线
},
label: {
show: false // 通常边的标签会很乱,默认关闭
},
symbol: ['none', 'arrow'],
symbolSize: [0, 10],
extra: edge.extra // 包含所有额外信息
}));
// 完全复刻Flask版本的图表选项
return {
tooltip: {
trigger: 'item',
formatter: (params) => {
if (params.dataType === 'node') {
const { name, extra } = params.data;
const connectionCount = calculateNodeConnections(params.data.id, data.edges);
return `<div style="text-align: left;">` +
`<b style="font-size: 14px;">${name}</b><br/>` +
`类型: ${NODE_TYPE_LABELS[extra?.node_type] || extra?.node_type}<br/>` +
`重要性: ${extra?.importance_score}<br/>` +
`连接数: ${connectionCount}` +
`${extra?.is_main_event ? '<br/><span style="color: #ffd700;">★ 主事件</span>' : ''}` +
`<br/><span style="color: #007bff; font-weight: bold;">🖱️ 点击查看详细信息</span>` +
`</div>`;
}
if (params.dataType === 'edge') {
const { source, target, extra } = params.data;
const sourceNode = data.nodes.find(n => String(n.id) === String(source));
const targetNode = data.nodes.find(n => String(n.id) === String(target));
if (!sourceNode || !targetNode) return '加载中...';
return `<div style="text-align: left;">` +
`<b>${sourceNode.name}${targetNode.name}</b><br/>` +
`类型: ${extra?.transmission_type}<br/>` +
`方向: ${extra?.direction}<br/>` +
`强度: ${extra?.strength}` +
`${extra?.is_circular ? '<br/>🔄 循环效应' : ''}` +
`</div>`;
}
return params.name;
}
},
// 隐藏ECharts默认图例使用自定义图例
legend: {
show: false
},
series: [{
type: 'graph',
layout: 'force',
data: chartNodes,
links: chartEdges,
categories: categories,
roam: true,
draggable: true, // 启用节点拖拽
focusNodeAdjacency: false, // 降低敏感性
force: {
repulsion: 250, // 增加节点间距,给点击留更多空间
edgeLength: [100, 180], // 增加边长,扩大节点周围空间
gravity: 0.05, // 降低重力
layoutAnimation: false, // 关闭布局动画降低敏感性
friction: 0.6 // 增加摩擦力,减少移动
},
emphasis: {
focus: 'none', // 降低高亮敏感性
scale: 1.1 // 轻微放大而不是强烈高亮
},
lineStyle: {
opacity: 0.6,
curveness: 0.1
},
label: {
position: 'right',
formatter: '{b}',
show: true,
distance: 10, // 标签距离节点更远一些
fontSize: 12
},
// 增加整体的点击敏感性
silent: false,
triggerLineEvent: true,
triggerEvent: true
}]
};
}
// 桑基图配置
function getSankeyOption(data) {
if (!data || !data.nodes || !data.links) {
return { title: { text: '暂无桑基图数据', left: 'center', top: 'center' } };
}
return {
title: { text: '事件影响力传导流向', left: 'center', top: 10 },
tooltip: {
trigger: 'item',
triggerOn: 'mousemove',
formatter: (params) => {
if (params.dataType === 'node') {
return `<b>${params.name}</b><br/>类型: ${params.data.type || 'N/A'}<br/>层级: ${params.data.level || 'N/A'}<br/>点击查看详情`;
}
return params.name;
}
},
series: [{
type: 'sankey',
layout: 'none',
emphasis: { focus: 'adjacency' },
nodeAlign: 'justify',
layoutIterations: 0,
data: data.nodes.map(node => ({
name: node.name,
type: node.type,
level: node.level,
itemStyle: {
color: node.color,
borderColor: node.level === 0 ? '#ffd700' : 'transparent',
borderWidth: node.level === 0 ? 3 : 0
}
})),
links: data.links.map(link => ({
source: data.nodes[link.source]?.name,
target: data.nodes[link.target]?.name,
value: link.value,
lineStyle: { color: 'source', opacity: 0.6, curveness: 0.5 }
})),
label: { color: 'rgba(0,0,0,0.7)', fontSize: 12 }
}]
};
}
const TransmissionChainAnalysis = ({ eventId }) => {
// 状态管理
const [graphData, setGraphData] = useState(null);
const [sankeyData, setSankeyData] = useState(null);
const [selectedNode, setSelectedNode] = useState(null);
const [nodeDetail, setNodeDetail] = useState(null); // 新增存储API获取的详细节点信息
const [transmissionPath, setTransmissionPath] = useState([]);
const [viewMode, setViewMode] = useState('graph');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [chartReady, setChartReady] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false); // 新增:控制弹窗显示
const [stats, setStats] = useState({
totalNodes: 0,
involvedIndustries: 0,
relatedCompanies: 0,
positiveImpact: 0,
negativeImpact: 0,
circularEffect: 0
});
const containerRef = useRef(null);
const modalBgColor = useColorModeValue('white', 'gray.800');
const modalBorderColor = useColorModeValue('gray.200', 'gray.600');
// 延迟初始化图表确保DOM容器准备好
useEffect(() => {
const timer = setTimeout(() => {
setChartReady(true);
}, 100);
return () => clearTimeout(timer);
}, []);
// 计算统计信息
useEffect(() => {
if (graphData && graphData.nodes) {
setStats({
totalNodes: graphData.nodes.length,
involvedIndustries: graphData.nodes.filter(n => n.extra?.node_type === 'industry').length,
relatedCompanies: graphData.nodes.filter(n => n.extra?.node_type === 'company').length,
positiveImpact: graphData.edges?.filter(e => e.extra?.direction === 'positive').length || 0,
negativeImpact: graphData.edges?.filter(e => e.extra?.direction === 'negative').length || 0,
circularEffect: graphData.edges?.filter(e => e.extra?.is_circular).length || 0
});
}
}, [graphData]);
// 加载数据
useEffect(() => {
async function fetchData() {
setLoading(true);
setError(null);
try {
logger.debug('TransmissionChain', '开始加载传导链数据', { eventId });
const [graphRes, sankeyRes] = await Promise.all([
eventService.getTransmissionChainAnalysis(eventId),
eventService.getSankeyData(eventId)
]);
logger.debug('TransmissionChain', 'API响应', {
graphSuccess: graphRes.success,
graphNodesCount: graphRes.data?.nodes?.length,
graphEdgesCount: graphRes.data?.edges?.length,
sankeySuccess: sankeyRes.success
});
if (graphRes.success && graphRes.data) {
setGraphData(graphRes.data);
} else {
logger.warn('TransmissionChain', '传导链数据加载失败', {
success: graphRes.success,
eventId
});
setGraphData(null);
}
if (sankeyRes.success && sankeyRes.data) {
setSankeyData(sankeyRes.data);
} else {
logger.warn('TransmissionChain', '桑基图数据加载失败', {
success: sankeyRes.success,
eventId
});
setSankeyData(null);
}
} catch (e) {
logger.error('TransmissionChain', 'fetchData', e, { eventId });
setError('加载传导链数据失败');
} finally {
setLoading(false);
}
}
if (eventId) {
fetchData();
}
}, [eventId]);
// BFS路径查找 - 完全复刻Flask版本
function findPath(nodes, edges, fromId, toId) {
const adj = {};
edges.forEach(e => {
if (!adj[e.source]) adj[e.source] = [];
adj[e.source].push(e.target);
});
const queue = [[fromId, [nodes.find(n => String(n.id) === String(fromId))]]];
const visited = new Set([fromId]);
while (queue.length > 0) {
const [current, path] = queue.shift();
if (String(current) === String(toId)) {
return path;
}
(adj[current] || []).forEach(next => {
if (!visited.has(next)) {
visited.add(next);
const nextNode = nodes.find(n => String(n.id) === String(next));
if (nextNode) {
queue.push([next, [...path, nextNode]]);
}
}
});
}
return [];
}
// 获取节点详情 - 完全复刻Flask版本API调用
async function getChainNodeDetail(nodeId) {
try {
const response = await fetch(`/api/events/${eventId}/chain-node/${nodeId}`);
const result = await response.json();
if (result.success) {
return result.data;
} else {
logger.error('TransmissionChain', 'getChainNodeDetail', new Error(result.message), {
nodeId,
eventId
});
return null;
}
} catch (error) {
logger.error('TransmissionChain', 'getChainNodeDetail', error, {
nodeId,
eventId
});
return null;
}
}
// 力导向图节点点击事件
const handleGraphNodeClick = async (params) => {
logger.debug('TransmissionChain', '图表节点点击', {
dataType: params.dataType,
componentType: params.componentType,
hasData: !!params.data,
nodeId: params.data?.id
});
// 处理节点点击(包括节点本体和标签)
if ((params.dataType === 'node' || params.componentType === 'series') && params.data && params.data.id) {
// 获取基本节点信息
const clickedNode = graphData.nodes.find(n => String(n.id) === String(params.data.id));
if (clickedNode) {
setSelectedNode(clickedNode);
// 计算传导路径
const mainEventNode = graphData?.nodes?.find(n => n.extra?.is_main_event);
if (mainEventNode && String(clickedNode.id) !== String(mainEventNode.id)) {
const path = findPath(graphData.nodes, graphData.edges, mainEventNode.id, clickedNode.id);
setTransmissionPath(path);
} else {
setTransmissionPath([]);
}
// 获取详细节点信息包括parents和children
logger.debug('TransmissionChain', '获取节点详情', {
nodeId: params.data.id,
nodeName: clickedNode.name
});
const detail = await getChainNodeDetail(params.data.id);
setNodeDetail(detail);
// 打开弹窗
setIsModalOpen(true);
}
}
};
// 桑基图节点点击事件
const handleSankeyNodeClick = async (params) => {
if (params.dataType === 'node' && params.data && params.data.name) {
logger.debug('TransmissionChain', '桑基图节点点击', {
nodeName: params.data.name
});
// 通过名称在原始数据中查找对应的节点
if (graphData && graphData.nodes) {
const clickedNode = graphData.nodes.find(n => n.name === params.data.name);
if (clickedNode) {
setSelectedNode(clickedNode);
// 计算传导路径
const mainEventNode = graphData?.nodes?.find(n => n.extra?.is_main_event);
if (mainEventNode && String(clickedNode.id) !== String(mainEventNode.id)) {
const path = findPath(graphData.nodes, graphData.edges, mainEventNode.id, clickedNode.id);
setTransmissionPath(path);
} else {
setTransmissionPath([]);
}
// 获取详细节点信息包括parents和children
logger.debug('TransmissionChain', '获取桑基图节点详情', {
nodeId: clickedNode.id,
nodeName: clickedNode.name
});
const detail = await getChainNodeDetail(clickedNode.id);
setNodeDetail(detail);
// 打开弹窗
setIsModalOpen(true);
} else {
logger.warn('TransmissionChain', '未找到对应的节点数据', {
nodeName: params.data.name
});
// 创建一个临时节点信息用于显示
const tempNode = {
id: params.data.name,
name: params.data.name,
extra: {
node_type: params.data.type,
description: `桑基图节点 - ${params.data.name}`,
importance_score: 'N/A'
}
};
setSelectedNode(tempNode);
setTransmissionPath([]);
setNodeDetail(null);
// 打开弹窗
setIsModalOpen(true);
}
}
}
};
return (
<Box p={6}>
{/* 统计信息条 */}
<Box mb={4}>
<HStack spacing={6} wrap="wrap">
<Stat>
<StatLabel>总节点数</StatLabel>
<StatNumber>{stats.totalNodes}</StatNumber>
</Stat>
<Stat>
<StatLabel>涉及行业</StatLabel>
<StatNumber>{stats.involvedIndustries}</StatNumber>
</Stat>
<Stat>
<StatLabel>相关公司</StatLabel>
<StatNumber>{stats.relatedCompanies}</StatNumber>
</Stat>
<Stat>
<StatLabel>正向影响</StatLabel>
<StatNumber color="green.500">{stats.positiveImpact}</StatNumber>
</Stat>
<Stat>
<StatLabel>负向影响</StatLabel>
<StatNumber color="red.500">{stats.negativeImpact}</StatNumber>
</Stat>
<Stat>
<StatLabel>循环效应</StatLabel>
<StatNumber color="purple.500">{stats.circularEffect}</StatNumber>
</Stat>
</HStack>
</Box>
{/* 自定义图例 */}
<Box mb={4}>
<HStack spacing={4} wrap="wrap">
{Object.entries(NODE_STYLES).map(([type, style]) => (
<Tag key={type} size="md">
<Box w={3} h={3} bg={style.color} borderRadius="sm" mr={2} />
{NODE_TYPE_LABELS[type] || type}
</Tag>
))}
</HStack>
</Box>
{/* 视图切换按钮 */}
<Flex mb={4} gap={2}>
<Button
colorScheme={viewMode === 'graph' ? 'blue' : 'gray'}
onClick={() => setViewMode('graph')}
size="sm"
>
力导向图
</Button>
<Button
colorScheme={viewMode === 'sankey' ? 'blue' : 'gray'}
onClick={() => setViewMode('sankey')}
size="sm"
>
桑基图
</Button>
</Flex>
{loading && (
<Flex justify="center" align="center" h="400px">
<Spinner size="xl" />
</Flex>
)}
{error && (
<Alert status="error" mb={4}>
<AlertIcon />
{error}
</Alert>
)}
{!loading && !error && (
<Box>
{/* 提示信息 */}
<Alert status="info" mb={4} borderRadius="md">
<AlertIcon />
<Text fontSize="sm">
<Icon as={ViewIcon} mr={2} />
点击图表中的节点可以查看详细信息
</Text>
</Alert>
{/* 图表容器 */}
<Box
h={viewMode === 'sankey' ? "600px" : "700px"}
border="1px solid"
borderColor="gray.300"
borderRadius="lg"
boxShadow="md"
bg="white"
p={4}
ref={containerRef}
>
{chartReady && (
<>
{viewMode === 'graph' ? (
<ReactECharts
option={graphData ? getGraphOption(graphData) : {}}
style={{ height: '100%', width: '100%' }}
onEvents={{
click: handleGraphNodeClick
}}
opts={{
renderer: 'canvas',
devicePixelRatio: window.devicePixelRatio || 1
}}
lazyUpdate={true}
notMerge={false}
shouldSetOption={(prevProps, props) => {
// 减少不必要的重新渲染
return JSON.stringify(prevProps.option) !== JSON.stringify(props.option);
}}
/>
) : (
<ReactECharts
option={sankeyData ? getSankeyOption(sankeyData) : {}}
style={{ height: '100%', width: '100%' }}
onEvents={{
click: handleSankeyNodeClick
}}
opts={{
renderer: 'canvas',
devicePixelRatio: window.devicePixelRatio || 1
}}
lazyUpdate={true}
notMerge={false}
shouldSetOption={(prevProps, props) => {
// 减少不必要的重新渲染
return JSON.stringify(prevProps.option) !== JSON.stringify(props.option);
}}
/>
)}
</>
)}
</Box>
</Box>
)}
{/* 节点详情弹窗 */}
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} size="xl">
<ModalOverlay />
<ModalContent maxH="80vh" bg={modalBgColor}>
<ModalHeader borderBottom="1px solid" borderColor={modalBorderColor}>
<HStack justify="space-between">
<Text>{selectedNode ? '节点详情' : '传导链分析'}</Text>
{selectedNode && (
<Badge colorScheme="blue">{NODE_TYPE_LABELS[selectedNode.extra?.node_type] || selectedNode.extra?.node_type}</Badge>
)}
</HStack>
</ModalHeader>
<ModalCloseButton />
<ModalBody overflowY="auto">
{selectedNode ? (
<VStack align="stretch" spacing={4}>
{/* 节点基本信息 */}
<Box>
<Text fontWeight="bold" mb={2} color="blue.600">基本信息</Text>
<VStack align="stretch" spacing={2}>
<HStack align="center">
<Text fontSize="sm"><strong>名称:</strong> {selectedNode.name}</Text>
{selectedNode.extra?.is_main_event && (
<Badge colorScheme="red" variant="solid" size="sm">主事件</Badge>
)}
</HStack>
<Text fontSize="sm"><strong>类型:</strong> {NODE_TYPE_LABELS[selectedNode.extra?.node_type] || selectedNode.extra?.node_type}</Text>
<Text fontSize="sm"><strong>重要性评分:</strong> {selectedNode.extra?.importance_score || 'N/A'}</Text>
{selectedNode.extra?.stock_code && (
<Text fontSize="sm"><strong>股票代码:</strong>
<Badge colorScheme="cyan" ml={2}>{selectedNode.extra.stock_code}</Badge>
</Text>
)}
{nodeDetail ? (
<HStack spacing={4}>
<Badge colorScheme="gray">
总连接: {nodeDetail.node.total_connections || 0}
</Badge>
<Badge colorScheme="green">
来源: {nodeDetail.node.incoming_connections || 0}
</Badge>
<Badge colorScheme="orange">
目标: {nodeDetail.node.outgoing_connections || 0}
</Badge>
</HStack>
) : (
<HStack spacing={4}>
<Badge colorScheme="gray">
总连接: {graphData ? graphData.edges.filter(e =>
String(e.source) === String(selectedNode.id) || String(e.target) === String(selectedNode.id)
).length : 0}
</Badge>
<Badge colorScheme="green">
来源: {graphData ? graphData.edges.filter(e =>
String(e.target) === String(selectedNode.id)
).length : 0}
</Badge>
<Badge colorScheme="orange">
目标: {graphData ? graphData.edges.filter(e =>
String(e.source) === String(selectedNode.id)
).length : 0}
</Badge>
</HStack>
)}
</VStack>
</Box>
{/* 节点描述 */}
{selectedNode.extra?.description && (
<Box>
<Text fontWeight="bold" mb={2} color="blue.600">描述</Text>
<Box
fontSize="sm"
color="gray.600"
borderLeft="3px solid"
borderColor="blue.200"
pl={3}
bg="gray.50"
p={2}
borderRadius="md"
fontStyle="italic"
>
{selectedNode.extra.description}AI合成
</Box>
</Box>
)}
{/* 传导路径 */}
{transmissionPath && transmissionPath.length > 0 && (
<Box>
<Text fontWeight="bold" mb={2} color="blue.600">传导路径</Text>
<Box bg="gray.50" p={3} borderRadius="md" borderLeft="4px solid" borderColor="blue.500">
<List spacing={1}>
{transmissionPath.map((node, index) => (
<ListItem key={index} fontSize="sm">
{index === 0 && '🚀 '}
{index === transmissionPath.length - 1 && '🎯 '}
{index > 0 && index < transmissionPath.length - 1 && '➡️ '}
{node.name}
</ListItem>
))}
</List>
</Box>
</Box>
)}
{/* 影响来源 */}
{(() => {
const sourcesFromAPI = nodeDetail && nodeDetail.parents && nodeDetail.parents.length > 0;
const sourcesFromGraph = graphData && graphData.edges.filter(e =>
String(e.target) === String(selectedNode.id)
).length > 0;
if (sourcesFromAPI) {
return (
<Box>
<Text fontWeight="bold" mb={2} color="blue.600">
影响来源 ({nodeDetail.parents.length})AI合成
</Text>
<List spacing={2}>
{nodeDetail.parents.map((parent, index) => (
<ListItem key={index} p={2} bg="gray.50" borderRadius="md" borderLeft="3px solid" borderColor="green.300">
<HStack justify="space-between" align="flex-start">
<VStack align="stretch" spacing={1} flex={1}>
<Text fontWeight="bold" fontSize="sm">{parent.name}</Text>
{parent.transmission_mechanism_citation?.data ? (
<Box fontSize="xs">
<Text as="span" fontWeight="bold">机制: </Text>
<CitedContent
data={parent.transmission_mechanism_citation.data}
title=""
showAIBadge={false}
containerStyle={{ backgroundColor: 'transparent', padding: 0, display: 'inline' }}
/>
</Box>
) : parent.transmission_mechanism ? (
<Text fontSize="xs" color="gray.600">
机制: {parent.transmission_mechanism}AI合成
</Text>
) : null}
</VStack>
<HStack spacing={2}>
<Badge colorScheme={parent.direction === 'positive' ? 'green' : parent.direction === 'negative' ? 'red' : 'gray'} size="sm">
{parent.direction}
</Badge>
{parent.is_circular && (
<Badge colorScheme="purple" size="sm">🔄 循环</Badge>
)}
</HStack>
</HStack>
</ListItem>
))}
</List>
</Box>
);
}
return null;
})()}
{/* 影响输出 */}
{(() => {
const targetsFromAPI = nodeDetail && nodeDetail.children && nodeDetail.children.length > 0;
if (targetsFromAPI) {
return (
<Box>
<Text fontWeight="bold" mb={2} color="blue.600">
影响输出 ({nodeDetail.children.length})
</Text>
<List spacing={2}>
{nodeDetail.children.map((child, index) => (
<ListItem key={index} p={2} bg="gray.50" borderRadius="md" borderLeft="3px solid" borderColor="orange.300">
<HStack justify="space-between" align="flex-start">
<VStack align="stretch" spacing={1} flex={1}>
<Text fontWeight="bold" fontSize="sm">{child.name}</Text>
{child.transmission_mechanism?.data ? (
<Box fontSize="xs">
<Text as="span" fontWeight="bold">机制: </Text>
<CitedContent
data={child.transmission_mechanism.data}
title=""
showAIBadge={false}
containerStyle={{ backgroundColor: 'transparent', padding: 0, display: 'inline' }}
/>
</Box>
) : child.transmission_mechanism ? (
<Text fontSize="xs" color="gray.600">
机制: {child.transmission_mechanism}AI合成
</Text>
) : null}
</VStack>
<HStack spacing={2}>
<Badge colorScheme={child.direction === 'positive' ? 'green' : child.direction === 'negative' ? 'red' : 'gray'} size="sm">
{child.direction}
</Badge>
</HStack>
</HStack>
</ListItem>
))}
</List>
</Box>
);
}
return null;
})()}
</VStack>
) : (
<VStack align="center" justify="center" h="300px" spacing={4}>
<Text fontSize="lg" color="gray.500">传导链分析</Text>
<Text fontSize="sm" color="gray.400" textAlign="center">
点击图表中的节点查看详细信息
</Text>
</VStack>
)}
</ModalBody>
<ModalFooter borderTop="1px solid" borderColor={modalBorderColor}>
<Button onClick={() => setIsModalOpen(false)}>关闭</Button>
</ModalFooter>
</ModalContent>
</Modal>
</Box>
);
};
export default TransmissionChainAnalysis;