diff --git a/frontend/src/components/GraphView/index.tsx b/frontend/src/components/GraphView/index.tsx index f30b3c2..5332411 100644 --- a/frontend/src/components/GraphView/index.tsx +++ b/frontend/src/components/GraphView/index.tsx @@ -8,9 +8,8 @@ import { GRAPH_DEFAULT_HEIGHT } from '../../config/constants'; interface GraphViewProps { data: GraphData; layout?: GraphLayoutConfig; - onNodeClick?: (node: Node) => void; + onNodeClick?: (node: Node | null) => void; onEdgeClick?: (edge: Edge) => void; - onNodeHover?: (node: Node | null) => void; height?: number; } @@ -23,14 +22,25 @@ export const GraphView = ({ layout = FORCE_LAYOUT_CONFIG, onNodeClick, onEdgeClick, - onNodeHover, height = GRAPH_DEFAULT_HEIGHT, }: GraphViewProps) => { const containerRef = useRef(null); const graphRef = useRef(null); + const cleanupRef = useRef<(() => void) | null>(null); + const isCancelledRef = useRef(false); + const renderPromiseRef = useRef | null>(null); + + const onNodeClickRef = useRef(onNodeClick); + const onEdgeClickRef = useRef(onEdgeClick); + + useEffect(() => { + onNodeClickRef.current = onNodeClick; + onEdgeClickRef.current = onEdgeClick; + }, [onNodeClick, onEdgeClick]); useEffect(() => { if (!containerRef.current) return; + if (!data.id || data.nodes.length === 0) return; // 清理现有图谱实例 if (graphRef.current) { @@ -98,6 +108,15 @@ export const GraphView = ({ size: 50, lineWidth: 2, }, + state: { + selected: { + stroke: '#1890FF', + lineWidth: 4, + halo: true, + haloColor: 'rgba(24, 144, 255, 0.3)', + haloLineWidth: 12, + }, + }, }, edge: { type: 'line', @@ -105,65 +124,102 @@ export const GraphView = ({ lineWidth: 1, endArrow: true, }, + state: { + selected: { + stroke: '#1890FF', + lineWidth: 3, + }, + }, }, layout: { ...FORCE_LAYOUT_CONFIG, }, - behaviors: GRAPH_BEHAVIORS, + behaviors: [ + ...GRAPH_BEHAVIORS, + { + type: 'click-select', + key: 'click-select', + degree: 0, + state: 'selected', + multiple: false, + animation: true, + onClick: (event: any) => { + console.log('[GraphView] Node click event triggered', event); + console.log('[GraphView] event.target:', event.target); + console.log('[GraphView] onNodeClickRef.current:', onNodeClickRef.current); + + if (event.target && event.target.id) { + const nodeId = event.target.id; + console.log('[GraphView] Looking for node with id:', nodeId); + console.log('[GraphView] Available nodes:', data.nodes.map((n: Node) => n.id)); + + const node = data.nodes.find((n: Node) => n.id === nodeId); + console.log('[GraphView] Found node:', nodeId, node); + + if (node && onNodeClickRef.current) { + console.log('[GraphView] Calling onNodeClick callback with node:', node); + onNodeClickRef.current(node); + } else { + console.log('[GraphView] Failed: node not found or callback not available'); + } + } + }, + }, + { + type: 'drag-element-force', + key: 'drag-element-force', + state: 'selected', + fixed: false, + }, + ], }; const graph = new Graph(graphConfig); graphRef.current = graph; - - graph.render().catch((renderError) => { - console.error('Graph render error:', renderError); - }); - // 节点点击事件 - graph.on('node:click', (evt: any) => { - const nodeId = evt.target.id; - if (nodeId && onNodeClick) { - const nodeData = data.nodes.find((n: Node) => n.id === nodeId); - if (nodeData) { - onNodeClick(nodeData); - } + isCancelledRef.current = false; + cleanupRef.current = () => { + isCancelledRef.current = true; + }; + + renderPromiseRef.current = graph.render().catch((renderError) => { + if (!isCancelledRef.current) { + console.error('Graph render error:', renderError); } }); - // 边点击事件 - graph.on('edge:click', (evt: any) => { - if (evt.target.id && onEdgeClick) { - const edgeData = data.edges.find((e: Edge) => e.id === evt.target.id); - if (edgeData) { - onEdgeClick(edgeData); - } + + + + + // 画布点击事件(取消选择) + graph.on('canvas:click', () => { + console.log('[GraphView] Canvas click - deselecting node'); + console.log('[GraphView] onNodeClickRef.current:', onNodeClickRef.current); + if (onNodeClickRef.current) { + console.log('[GraphView] Calling onNodeClick with null'); + onNodeClickRef.current(null); } }); - // 节点悬停事件 - graph.on('node:mouseenter', (evt: any) => { - if (evt.target.id && onNodeHover) { - const nodeData = data.nodes.find((n: Node) => n.id === evt.target.id); - if (nodeData) { - onNodeHover(nodeData); - } - } - }); - graph.on('node:mouseleave', () => { - if (onNodeHover) { - onNodeHover(null); - } - }); // 清理函数 return () => { + isCancelledRef.current = true; + + if (renderPromiseRef.current) { + renderPromiseRef.current.catch(() => {}); + renderPromiseRef.current = null; + } + if (graphRef.current) { + graphRef.current.off('canvas:click'); graphRef.current.destroy(); graphRef.current = null; } }; - }, [height, layout.type, onNodeClick, onEdgeClick, onNodeHover, data]); + }, [height, layout.type, data.id, onNodeClickRef, onEdgeClickRef]); // 窗口大小调整响应 useEffect(() => { diff --git a/frontend/src/components/NodePanel/index.tsx b/frontend/src/components/NodePanel/index.tsx index ddb972f..1e495ee 100644 --- a/frontend/src/components/NodePanel/index.tsx +++ b/frontend/src/components/NodePanel/index.tsx @@ -16,7 +16,7 @@ export const NodePanel = ({ node, onClose }: NodePanelProps) => { const hasProperties = node.properties && Object.keys(node.properties).length > 0; return ( -
+

节点详情

{onClose && ( diff --git a/frontend/src/components/NodeTooltip/index.tsx b/frontend/src/components/NodeTooltip/index.tsx new file mode 100644 index 0000000..4dc1655 --- /dev/null +++ b/frontend/src/components/NodeTooltip/index.tsx @@ -0,0 +1,102 @@ +import { useState, useEffect, useMemo } from 'react'; +import type { Node } from '../../types/graph'; + +interface NodeTooltipProps { + node: Node | null; +} + +export const NodeTooltip = ({ node }: NodeTooltipProps) => { + const [isExpanded, setIsExpanded] = useState(false); + + useEffect(() => { + setIsExpanded(false); + }, [node?.id]); + + const propertiesList = useMemo(() => { + if (!node?.properties) return []; + return Object.entries(node.properties); + }, [node?.properties]); + + const hasProperties = propertiesList.length > 0; + + if (!node) return null; + + const handleExpand = () => setIsExpanded(true); + const handleCollapse = () => setIsExpanded(false); + + return ( +
+ {!isExpanded ? ( +
+
+
{node.label}
+
+ {node.type && ( +
+ {node.type} +
+ )} + {hasProperties && ( +
+ {propertiesList.slice(0, 2).map(([key, value]) => ( +
+ {key}: + + {typeof value === 'object' ? JSON.stringify(value) : String(value)} + +
+ ))} + {propertiesList.length > 2 && ( +
还有 {propertiesList.length - 2} 项属性...
+ )} +
+ )} + {hasProperties && ( + + )} +
+ ) : ( +
+
+
{node.label}
+ +
+ {node.type && ( +
+ {node.type} +
+ )} + {hasProperties && ( +
+
+ 属性 ({propertiesList.length}) +
+
+ {propertiesList.map(([key, value]) => ( +
+ {key}: + + {typeof value === 'object' ? JSON.stringify(value) : String(value)} + +
+ ))} +
+
+ )} +
+ )} +
+ ); +}; + +export default NodeTooltip; \ No newline at end of file diff --git a/frontend/src/hooks/useGraphData.ts b/frontend/src/hooks/useGraphData.ts index b626d92..b7bfc0f 100644 --- a/frontend/src/hooks/useGraphData.ts +++ b/frontend/src/hooks/useGraphData.ts @@ -26,16 +26,16 @@ export const useGraphData = () => { // 如果后端返回空数据,使用示例数据 if (data.nodes.length === 0) { - setGraphData(SAMPLE_GRAPH_DATA); + setGraphData({ ...SAMPLE_GRAPH_DATA, id: `sample-${Date.now()}` }); } else { - setGraphData(data); + setGraphData({ ...data, id: `data-${Date.now()}` }); } } catch (error) { console.error('Failed to load graph data:', error); // 重置颜色分配缓存 resetColorAssignment(); // API 失败时使用示例数据作为降级方案 - setGraphData(SAMPLE_GRAPH_DATA); + setGraphData({ ...SAMPLE_GRAPH_DATA, id: `fallback-${Date.now()}` }); } finally { setLoading(false); } @@ -59,17 +59,36 @@ export const useGraphData = () => { // 通过 ID 选择节点(用于搜索结果点击) const handleNodeSelectById = useCallback((nodeId: string) => { + console.log('[useGraphData] handleNodeSelectById called with id:', nodeId); const node = graphData.nodes.find((n: Node) => n.id === nodeId); + console.log('[useGraphData] Found node:', node); if (node) { + console.log('[useGraphData] Setting selectedNode:', node); setSelectedNode(node); + } else { + console.log('[useGraphData] Node not found with id:', nodeId); } }, [graphData.nodes]); // 选择节点(用于图谱点击) - const handleNodeSelect = useCallback((node: Node) => { + const handleNodeSelect = useCallback((node: Node | null) => { + console.log('[useGraphData] handleNodeSelect called with node:', node); + console.log('[useGraphData] About to call setSelectedNode'); setSelectedNode(node); + console.log('[useGraphData] setSelectedNode called'); }, []); + // 悬停节点(用于图谱悬停) + const handleNodeHover = useCallback((node: Node | null) => { + setHoveredNode(node); + }, []); + + // 监听 selectedNode 变化,用于调试 + useEffect(() => { + console.log('[useGraphData] selectedNode state changed:', selectedNode); + console.log('[useGraphData] selectedNode truthiness:', !!selectedNode); + }, [selectedNode]); + // 组件挂载时加载数据 useEffect(() => { loadGraphData(); @@ -81,11 +100,10 @@ export const useGraphData = () => { hoveredNode, searchResults, loading, - setHoveredNode, setSelectedNode, - setSearchResults, handleSearch, handleNodeSelectById, handleNodeSelect, + handleNodeHover, }; }; diff --git a/frontend/src/pages/KnowledgeGraph.tsx b/frontend/src/pages/KnowledgeGraph.tsx index 9df7c00..4625f7b 100644 --- a/frontend/src/pages/KnowledgeGraph.tsx +++ b/frontend/src/pages/KnowledgeGraph.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useMemo } from 'react'; import GraphView from '../components/GraphView/index'; import SearchBar from '../components/SearchBar/index'; import NodePanel from '../components/NodePanel/index'; @@ -18,17 +18,15 @@ export const KnowledgeGraph = () => { const { graphData, selectedNode, - hoveredNode, searchResults, loading, - setHoveredNode, setSelectedNode, handleSearch, handleNodeSelectById, + handleNodeSelect, } = useGraphData(); const { layout } = useGraphLayout(); - const [isExpanded, setIsExpanded] = useState(false); const legendData = useMemo(() => { if (!graphData || graphData.nodes.length === 0) { @@ -37,11 +35,6 @@ export const KnowledgeGraph = () => { return generateLegendData(graphData); }, [graphData]); - const propertiesList = useMemo(() => { - if (!hoveredNode?.properties) return []; - return Object.entries(hoveredNode.properties); - }, [hoveredNode]); - return (
{loading ? ( @@ -73,94 +66,19 @@ export const KnowledgeGraph = () => { - - {hoveredNode && ( -
- {!isExpanded ? ( -
-
-
{hoveredNode.label}
-
- {hoveredNode.type && ( -
- {hoveredNode.type} -
- )} - {propertiesList.length > 0 && ( -
- {propertiesList.slice(0, 2).map(([key, value]) => ( -
- {key}: - - {typeof value === 'object' ? JSON.stringify(value) : String(value)} - -
- ))} - {propertiesList.length > 2 && ( -
还有 {propertiesList.length - 2} 项属性...
- )} -
- )} - {propertiesList.length > 0 && ( - - )} -
- ) : ( -
-
-
{hoveredNode.label}
- -
- {hoveredNode.type && ( -
- {hoveredNode.type} -
- )} - {propertiesList.length > 0 && ( -
-
- 属性 ({propertiesList.length}) -
-
- {propertiesList.map(([key, value]) => ( -
- {key}: - - {typeof value === 'object' ? JSON.stringify(value) : String(value)} - -
- ))} -
-
- )} -
- )} -
- )} - - {selectedNode && ( - - )}
+ + {selectedNode && ( +
+ setSelectedNode(null)} + /> +
+ )} )}
diff --git a/frontend/src/types/graph.ts b/frontend/src/types/graph.ts index 220b99d..6730ab9 100644 --- a/frontend/src/types/graph.ts +++ b/frontend/src/types/graph.ts @@ -46,6 +46,7 @@ export interface Edge { export interface GraphData { nodes: Node[]; edges: Edge[]; + id?: string; } /**