feat: 激活节点时聚焦节点
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user