Initial commit

This commit is contained in:
2025-10-11 11:55:25 +08:00
parent 467dad8449
commit 8107dee8d3
2879 changed files with 610575 additions and 0 deletions

View File

@@ -0,0 +1,979 @@
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';
// 节点样式配置 - 完全复刻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) {
console.log('开始过滤孤立节点');
console.log('输入节点:', nodes);
console.log('输入边:', edges);
if (!nodes || !edges) {
console.log('节点或边数据为空');
return [];
}
const connectedNodeIds = new Set();
edges.forEach(edge => {
console.log('处理边:', edge, '从', edge.source, '到', edge.target);
connectedNodeIds.add(String(edge.source));
connectedNodeIds.add(String(edge.target));
});
console.log('连接的节点ID集合:', connectedNodeIds);
// 如果图中只有一个节点且是主事件,也显示它
const mainEventNode = nodes.find(n => n.extra?.is_main_event);
console.log('主事件节点:', mainEventNode);
if (mainEventNode) {
connectedNodeIds.add(String(mainEventNode.id));
console.log('添加主事件节点ID:', String(mainEventNode.id));
}
const filteredNodes = nodes.filter(node => {
const shouldKeep = connectedNodeIds.has(String(node.id));
console.log(`节点 ${node.name}(${node.id}[${String(node.id)}]): ${shouldKeep ? '保留' : '过滤'}`);
return shouldKeep;
});
console.log('过滤后的节点:', filteredNodes);
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) {
console.log('getGraphOption 被调用,输入数据:', data);
if (!data || !data.nodes || !data.edges || data.nodes.length === 0) {
console.log('数据为空或无效');
return {
title: { text: '暂无传导链数据', left: 'center', top: 'center' },
graphic: { type: 'text', left: 'center', top: '60%', style: { text: '当前事件暂无传导链分析数据', fontSize: 14 } }
};
}
console.log('原始节点数:', data.nodes.length);
console.log('原始边数:', data.edges.length);
const filteredNodes = filterIsolatedNodes(data.nodes, data.edges);
console.log('过滤后节点数:', filteredNodes.length);
console.log('过滤后的节点:', filteredNodes);
// 进一步过滤:不显示事件类型的节点
const nonEventNodes = filteredNodes.filter(node => node.extra?.node_type !== 'event');
console.log('排除事件节点后:', nonEventNodes.length);
if (nonEventNodes.length === 0) {
console.log('过滤后没有有效节点');
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 }
}));
console.log('节点类别:', categories);
// 构建图表节点数据 - 完全复刻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);
console.log(`节点 ${node.name} (${node.id}): 类型=${nodeType}, 连接数=${connectionCount}`);
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 {
console.log('开始加载传导链数据eventId:', eventId);
const [graphRes, sankeyRes] = await Promise.all([
eventService.getTransmissionChainAnalysis(eventId),
eventService.getSankeyData(eventId)
]);
console.log('传导链数据API响应:', graphRes);
console.log('桑基图数据API响应:', sankeyRes);
if (graphRes.success && graphRes.data) {
console.log('传导链节点数据:', graphRes.data.nodes);
console.log('传导链边数据:', graphRes.data.edges);
setGraphData(graphRes.data);
} else {
console.log('传导链数据加载失败:', graphRes);
setGraphData(null);
}
if (sankeyRes.success && sankeyRes.data) {
console.log('桑基图数据:', sankeyRes.data);
setSankeyData(sankeyRes.data);
} else {
console.log('桑基图数据加载失败:', sankeyRes);
setSankeyData(null);
}
} catch (e) {
console.error('传导链数据加载异常:', e);
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 {
console.error('获取节点详情失败:', result.message);
return null;
}
} catch (error) {
console.error('API调用异常:', error);
return null;
}
}
// 力导向图节点点击事件
const handleGraphNodeClick = async (params) => {
console.log('点击事件详情:', params);
// 处理节点点击(包括节点本体和标签)
if ((params.dataType === 'node' || params.componentType === 'series') && params.data && params.data.id) {
console.log('点击图表节点:', params.data.id, 'dataType:', params.dataType, 'componentType:', params.componentType);
// 获取基本节点信息
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
console.log('开始获取节点详情节点ID:', params.data.id);
const detail = await getChainNodeDetail(params.data.id);
console.log('获取到的节点详情:', detail);
setNodeDetail(detail);
// 打开弹窗
setIsModalOpen(true);
}
}
// 如果点击的是空白区域,也尝试查找最近的节点
else if (params.componentType === 'series' && !params.data) {
console.log('点击了图表空白区域');
// 这里可以添加点击空白区域的处理逻辑
}
};
// 桑基图节点点击事件
const handleSankeyNodeClick = async (params) => {
if (params.dataType === 'node' && params.data && params.data.name) {
console.log('点击桑基图节点:', params.data.name);
// 通过名称在原始数据中查找对应的节点
if (graphData && graphData.nodes) {
const clickedNode = graphData.nodes.find(n => n.name === params.data.name);
if (clickedNode) {
console.log('找到对应节点:', 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
console.log('开始获取桑基图节点详情节点ID:', clickedNode.id);
const detail = await getChainNodeDetail(clickedNode.id);
console.log('获取到的桑基图节点详情:', detail);
setNodeDetail(detail);
// 打开弹窗
setIsModalOpen(true);
} else {
console.log('未找到对应的节点数据');
// 创建一个临时节点信息用于显示
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,
// 添加更多事件以提高点击敏感性
mouseover: (params) => {
console.log('鼠标悬停:', params);
// 可以在这里添加悬停效果
},
mouseout: (params) => {
// 鼠标离开的处理
}
}}
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,
// 添加更多事件以提高点击敏感性
mouseover: (params) => {
console.log('桑基图鼠标悬停:', params);
// 可以在这里添加悬停效果
},
mouseout: (params) => {
// 鼠标离开的处理
}
}}
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 && (
<Text fontSize="xs" color="gray.600">
机制: {parent.transmission_mechanism}AI合成
</Text>
)}
</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 && (
<Text fontSize="xs" color="gray.600">
机制: {child.transmission_mechanism}AI合成
</Text>
)}
</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;