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';
// 节点样式配置 - 完全复刻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 `
` +
`${name}
` +
`类型: ${NODE_TYPE_LABELS[extra?.node_type] || extra?.node_type}
` +
`重要性: ${extra?.importance_score}
` +
`连接数: ${connectionCount}` +
`${extra?.is_main_event ? '
★ 主事件' : ''}` +
`
🖱️ 点击查看详细信息` +
`
`;
}
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 `` +
`${sourceNode.name} → ${targetNode.name}
` +
`类型: ${extra?.transmission_type}
` +
`方向: ${extra?.direction}
` +
`强度: ${extra?.strength}` +
`${extra?.is_circular ? '
🔄 循环效应' : ''}` +
`
`;
}
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 `${params.name}
类型: ${params.data.type || 'N/A'}
层级: ${params.data.level || 'N/A'}
点击查看详情`;
}
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 (
{/* 统计信息条 */}
总节点数
{stats.totalNodes}
涉及行业
{stats.involvedIndustries}
相关公司
{stats.relatedCompanies}
正向影响
{stats.positiveImpact}
负向影响
{stats.negativeImpact}
循环效应
{stats.circularEffect}
{/* 自定义图例 */}
{Object.entries(NODE_STYLES).map(([type, style]) => (
{NODE_TYPE_LABELS[type] || type}
))}
{/* 视图切换按钮 */}
{loading && (
)}
{error && (
{error}
)}
{!loading && !error && (
{/* 提示信息 */}
点击图表中的节点可以查看详细信息
{/* 图表容器 */}
{chartReady && (
<>
{viewMode === 'graph' ? (
{
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);
}}
/>
) : (
{
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);
}}
/>
)}
>
)}
)}
{/* 节点详情弹窗 */}
setIsModalOpen(false)} size="xl">
{selectedNode ? '节点详情' : '传导链分析'}
{selectedNode && (
{NODE_TYPE_LABELS[selectedNode.extra?.node_type] || selectedNode.extra?.node_type}
)}
{selectedNode ? (
{/* 节点基本信息 */}
基本信息
名称: {selectedNode.name}
{selectedNode.extra?.is_main_event && (
主事件
)}
类型: {NODE_TYPE_LABELS[selectedNode.extra?.node_type] || selectedNode.extra?.node_type}
重要性评分: {selectedNode.extra?.importance_score || 'N/A'}
{selectedNode.extra?.stock_code && (
股票代码:
{selectedNode.extra.stock_code}
)}
{nodeDetail ? (
总连接: {nodeDetail.node.total_connections || 0}
来源: {nodeDetail.node.incoming_connections || 0}
目标: {nodeDetail.node.outgoing_connections || 0}
) : (
总连接: {graphData ? graphData.edges.filter(e =>
String(e.source) === String(selectedNode.id) || String(e.target) === String(selectedNode.id)
).length : 0}
来源: {graphData ? graphData.edges.filter(e =>
String(e.target) === String(selectedNode.id)
).length : 0}
目标: {graphData ? graphData.edges.filter(e =>
String(e.source) === String(selectedNode.id)
).length : 0}
)}
{/* 节点描述 */}
{selectedNode.extra?.description && (
描述
{selectedNode.extra.description}(AI合成)
)}
{/* 传导路径 */}
{transmissionPath && transmissionPath.length > 0 && (
传导路径
{transmissionPath.map((node, index) => (
{index === 0 && '🚀 '}
{index === transmissionPath.length - 1 && '🎯 '}
{index > 0 && index < transmissionPath.length - 1 && '➡️ '}
{node.name}
))}
)}
{/* 影响来源 */}
{(() => {
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 (
影响来源 ({nodeDetail.parents.length})(AI合成)
{nodeDetail.parents.map((parent, index) => (
{parent.name}
{parent.transmission_mechanism_citation?.data ? (
机制:
) : parent.transmission_mechanism ? (
机制: {parent.transmission_mechanism}(AI合成)
) : null}
{parent.direction}
{parent.is_circular && (
🔄 循环
)}
))}
);
}
return null;
})()}
{/* 影响输出 */}
{(() => {
const targetsFromAPI = nodeDetail && nodeDetail.children && nodeDetail.children.length > 0;
if (targetsFromAPI) {
return (
影响输出 ({nodeDetail.children.length})
{nodeDetail.children.map((child, index) => (
{child.name}
{child.transmission_mechanism?.data ? (
机制:
) : child.transmission_mechanism ? (
机制: {child.transmission_mechanism}(AI合成)
) : null}
{child.direction}
))}
);
}
return null;
})()}
) : (
传导链分析
点击图表中的节点查看详细信息
)}
);
};
export default TransmissionChainAnalysis;