From 88462283992eb35efaf5756ffd5d1ad504090a3d Mon Sep 17 00:00:00 2001 From: wonder Date: Mon, 6 Apr 2026 16:38:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=BF=80=E6=B4=BB=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E6=97=B6=E8=81=9A=E7=84=A6=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/GraphView/index.tsx | 227 ++++++++++++++------ frontend/src/config/constants.ts | 9 +- 2 files changed, 161 insertions(+), 75 deletions(-) diff --git a/frontend/src/components/GraphView/index.tsx b/frontend/src/components/GraphView/index.tsx index 816df5a..0ab5453 100644 --- a/frontend/src/components/GraphView/index.tsx +++ b/frontend/src/components/GraphView/index.tsx @@ -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 | null>(null); + const selectedNodeIdRef = useRef(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; + connectedEdgeIds: Set; + } => { + const connectedNodeIds = new Set([nodeId]); + const connectedEdgeIds = new Set(); + + 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 = {}; + 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 = {}; + + 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 = {}; + 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 (
-
+
布局: {layout.type}
-
节点: {data.nodes.length} | 边: {data.edges.length}
+
+ 节点: {data.nodes.length} | 边: {data.edges.length} +
); }; -export default GraphView; \ No newline at end of file +export default GraphView; diff --git a/frontend/src/config/constants.ts b/frontend/src/config/constants.ts index 87caa35..bce7da2 100644 --- a/frontend/src/config/constants.ts +++ b/frontend/src/config/constants.ts @@ -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;