feat: 激活节点时聚焦节点

This commit is contained in:
2026-04-06 16:38:20 +08:00
parent 59d4618c2e
commit 8846228399
2 changed files with 161 additions and 75 deletions
+156 -71
View File
@@ -1,10 +1,15 @@
import { useEffect, useRef, useMemo } from 'react';
import { Graph } from '@antv/g6';
import type { GraphData, Node, Edge, GraphLayoutConfig } from '../../types/graph';
import { getNodeColors } from '../../config/colors';
import { FORCE_LAYOUT_CONFIG, GRAPH_BEHAVIORS } from '../../config/layout';
import { GRAPH_DEFAULT_HEIGHT } from '../../config/constants';
import { getNodeStyle, getEdgeStyle } from '../../utils/graphStyle';
import { useEffect, useRef, useMemo } from "react";
import { Graph, NodeEvent, CanvasEvent } from "@antv/g6";
import type {
GraphData,
Node,
Edge,
GraphLayoutConfig,
} from "../../types/graph";
import { getNodeColors } from "../../config/colors";
import { FORCE_LAYOUT_CONFIG } from "../../config/layout";
import { GRAPH_DEFAULT_HEIGHT } from "../../config/constants";
import { getNodeStyle, getEdgeStyle } from "../../utils/graphStyle";
interface GraphViewProps {
data: GraphData;
@@ -28,10 +33,34 @@ export const GraphView = ({
const cleanupRef = useRef<(() => void) | null>(null);
const isCancelledRef = useRef(false);
const renderPromiseRef = useRef<Promise<void> | null>(null);
const selectedNodeIdRef = useRef<string | null>(null);
const onNodeClickRef = useRef(onNodeClick);
const nodeMap = useMemo(() => new Map(data.nodes.map((node: Node) => [node.id, node])), [data.nodes]);
const nodeMap = useMemo(
() => new Map(data.nodes.map((node: Node) => [node.id, node])),
[data.nodes],
);
const findConnectedElements = (
nodeId: string,
): {
connectedNodeIds: Set<string>;
connectedEdgeIds: Set<string>;
} => {
const connectedNodeIds = new Set<string>([nodeId]);
const connectedEdgeIds = new Set<string>();
data.edges.forEach((edge: Edge) => {
if (edge.source === nodeId || edge.target === nodeId) {
connectedEdgeIds.add(edge.id);
connectedNodeIds.add(edge.source);
connectedNodeIds.add(edge.target);
}
});
return { connectedNodeIds, connectedEdgeIds };
};
useEffect(() => {
onNodeClickRef.current = onNodeClick;
@@ -65,10 +94,10 @@ export const GraphView = ({
stroke: colors.stroke,
lineWidth: 2,
labelText: node.label,
labelFill: '#2C3E50',
labelFill: "#2C3E50",
labelFontSize: 14,
labelFontWeight: 'bold' as const,
labelPlacement: 'bottom' as const,
labelFontWeight: "bold" as const,
labelPlacement: "bottom" as const,
},
};
}),
@@ -86,9 +115,9 @@ export const GraphView = ({
properties: edge.properties,
},
style: {
stroke: '#95A5A6',
stroke: "#95A5A6",
labelText: edge.label,
labelFill: '#666',
labelFill: "#666",
labelFontSize: 10,
},
};
@@ -101,76 +130,45 @@ export const GraphView = ({
width: containerRef.current.clientWidth,
height: height,
data: processedGraphData,
autoFit: 'view' as const,
autoFit: "view" as const,
node: {
type: 'circle',
type: "circle",
style: {
lineWidth: 2,
...getNodeStyle(),
},
state: {
selected: {
stroke: '#1890FF',
stroke: "#1890FF",
lineWidth: 4,
halo: true,
haloColor: 'rgba(24, 144, 255, 0.3)',
haloColor: "rgba(24, 144, 255, 0.3)",
haloLineWidth: 12,
},
inactive: {
opacity: 1,
},
},
},
edge: {
type: 'line',
type: "line",
style: {
endArrow: true,
...getEdgeStyle(nodeMap),
},
state: {
selected: {
stroke: '#1890FF',
stroke: "#1890FF",
},
inactive: {
opacity: 1,
},
},
},
layout: {
...FORCE_LAYOUT_CONFIG,
},
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,
},
],
behaviors: ["drag-canvas", "zoom-canvas", "drag-element-force"],
};
const graph = new Graph(graphConfig);
@@ -183,29 +181,109 @@ export const GraphView = ({
renderPromiseRef.current = graph.render().catch((renderError) => {
if (!isCancelledRef.current) {
console.error('Graph render error:', renderError);
console.error("Graph render error:", renderError);
}
});
graph.on(NodeEvent.CLICK, (event: any) => {
console.log("[GraphView] Node click event", event);
if (event.target && event.target.id && event.targetType === "node") {
const nodeId = event.target.id;
const node = data.nodes.find((n: Node) => n.id === nodeId);
if (node && graph) {
const wasSelected = selectedNodeIdRef.current === nodeId;
if (wasSelected) {
selectedNodeIdRef.current = null;
const stateUpdates: Record<string, string[]> = {};
data.nodes.forEach((n: Node) => {
stateUpdates[n.id] = [];
});
data.edges.forEach((edge: Edge) => {
stateUpdates[edge.id] = [];
});
console.log("[GraphView] Clearing all states:", stateUpdates);
graph.setElementState(stateUpdates).then(() => {
console.log("[GraphView] States cleared");
});
if (onNodeClickRef.current) {
onNodeClickRef.current(null);
}
} else {
selectedNodeIdRef.current = nodeId;
const { connectedNodeIds, connectedEdgeIds } =
findConnectedElements(nodeId);
const stateUpdates: Record<string, string[]> = {};
data.nodes.forEach((n: Node) => {
if (n.id === nodeId) {
stateUpdates[n.id] = ["selected"];
} else if (connectedNodeIds.has(n.id)) {
stateUpdates[n.id] = [];
} else {
stateUpdates[n.id] = ["inactive"];
}
});
data.edges.forEach((edge: Edge) => {
if (connectedEdgeIds.has(edge.id)) {
stateUpdates[edge.id] = [];
} else {
stateUpdates[edge.id] = ["inactive"];
}
});
graph.setElementState(stateUpdates);
if (onNodeClickRef.current) {
onNodeClickRef.current(node);
}
}
}
}
});
graph.on(CanvasEvent.CLICK, (event: any) => {
if (event.targetType !== "canvas") {
console.log(
"[GraphView] Canvas click ignored - clicked on",
event.targetType,
);
return;
}
console.log("[GraphView] Canvas click - deselecting node");
selectedNodeIdRef.current = null;
const stateUpdates: Record<string, string[]> = {};
data.nodes.forEach((node: Node) => {
stateUpdates[node.id] = [];
});
data.edges.forEach((edge: Edge) => {
stateUpdates[edge.id] = [];
});
console.log("[GraphView] Clearing all states:", stateUpdates);
graph.setElementState(stateUpdates).then(() => {
console.log("[GraphView] States cleared");
});
// 画布点击事件(取消选择)
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);
}
});
// 清理函数
return () => {
isCancelledRef.current = true;
selectedNodeIdRef.current = null;
if (renderPromiseRef.current) {
renderPromiseRef.current.catch(() => {});
@@ -213,7 +291,8 @@ export const GraphView = ({
}
if (graphRef.current) {
graphRef.current.off('canvas:click');
graphRef.current.off(NodeEvent.CLICK);
graphRef.current.off(CanvasEvent.CLICK);
graphRef.current.destroy();
graphRef.current = null;
}
@@ -228,19 +307,25 @@ export const GraphView = ({
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [height]);
return (
<div className="relative w-full h-full">
<div ref={containerRef} className="w-full h-full" style={{ height: `${height}px` }} />
<div
ref={containerRef}
className="w-full h-full"
style={{ height: `${height}px` }}
/>
<div className="absolute bottom-4 right-4 bg-white/90 backdrop-blur-sm rounded-lg shadow-lg p-3">
<div className="text-xs text-gray-600 mb-2">布局: {layout.type}</div>
<div className="text-xs text-gray-600">节点: {data.nodes.length} | 边: {data.edges.length}</div>
<div className="text-xs text-gray-600">
节点: {data.nodes.length} | 边: {data.edges.length}
</div>
</div>
</div>
);
};
export default GraphView;
export default GraphView;
+5 -4
View File
@@ -1,5 +1,6 @@
// API 相关常量
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3001/api';
export const API_BASE_URL =
import.meta.env.VITE_API_BASE_URL || "http://localhost:3001/api";
export const API_TIMEOUT = 10000;
// 图谱渲染配置
@@ -11,9 +12,9 @@ export const NODE_SIZE_MIN = 10;
export const NODE_SIZE_MAX = 60;
// 边宽度配置
export const DEFAULT_EDGE_WIDTH = 1.5;
export const EDGE_WIDTH_MIN = 1;
export const EDGE_WIDTH_MAX = 2;
export const DEFAULT_EDGE_WIDTH = 0.8;
export const EDGE_WIDTH_MIN = 0.3;
export const EDGE_WIDTH_MAX = 2.0;
// Importance 配置
export const IMPORTANCE_DEFAULT = 0.8;