From 2062eeceedc51816a64d384e9bca3c5340e200dc Mon Sep 17 00:00:00 2001 From: hunter-dev Date: Mon, 24 Aug 2026 20:44:54 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=9B=BE=E8=B0=B1=E6=8C=89=E9=A1=B6?= =?UTF-8?q?=E5=B1=82=E5=88=86=E7=BB=84=E5=9B=BA=E5=AE=9A=E5=9D=90=E6=A0=87?= =?UTF-8?q?=E5=B8=83=E5=B1=80=EF=BC=8C=E8=8A=82=E7=82=B9=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E6=8C=A4=E5=8E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/renderer/features/graph/GraphView.tsx | 64 +++++++++++++++---- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/renderer/features/graph/GraphView.tsx b/apps/desktop/src/renderer/features/graph/GraphView.tsx index 99de569..0b05072 100644 --- a/apps/desktop/src/renderer/features/graph/GraphView.tsx +++ b/apps/desktop/src/renderer/features/graph/GraphView.tsx @@ -1,38 +1,80 @@ import { useEffect, useRef, useState } from 'react'; import { Graph } from '@antv/g6'; import { invoke } from '../../ipc-client'; -import { IPC } from '@hunter/shared'; -import type { GraphData } from '@hunter/shared'; +import { IPC, type GraphData, type GraphNode } from '@hunter/shared'; import { useStore } from '../../store'; +// 按顶层分组固定坐标布局:每组一列,组内纵向排开,间距充足(FR-G2 组内分行排布) +function layoutNodes(nodes: GraphNode[], width: number, height: number) { + const groups = new Map(); + nodes.forEach(n => { const a = groups.get(n.group) ?? []; a.push(n); groups.set(n.group, a); }); + const groupList = [...groups.entries()]; + const colWidth = groupList.length > 0 ? width / groupList.length : width; + const positions = new Map(); + groupList.forEach(([group, items], gi) => { + const colX = colWidth * gi + colWidth / 2; + const rowGap = Math.min(80, height / Math.max(items.length, 1)); + const startY = height / 2 - (items.length * rowGap) / 2 + rowGap / 2; + items.forEach((n, ni) => positions.set(n.id, { x: colX, y: startY + ni * rowGap })); + }); + return positions; +} + export function GraphView() { const containerRef = useRef(null); const [graph, setGraph] = useState(null); const { openDoc } = useStore(); + const sizeRef = useRef({ w: 800, h: 600 }); useEffect(() => { if (!containerRef.current) return; + const el = containerRef.current; + const updateSize = () => { sizeRef.current = { w: el.clientWidth || 800, h: el.clientHeight || 600 }; }; + updateSize(); + const ro = new ResizeObserver(updateSize); + ro.observe(el); const g = new Graph({ - container: containerRef.current, - width: containerRef.current.clientWidth, - height: containerRef.current.clientHeight, - layout: { type: 'force', preventOverlap: true, nodeStrength: -50, linkDistance: 120 } as any, - node: { style: { size: 20, fill: (d: any) => d.color || '#4a9eff', labelText: (d: any) => d.title, labelFill: '#ddd' } } as any, - edge: { style: { stroke: '#555' } } as any, + container: el, + width: sizeRef.current.w, + height: sizeRef.current.h, + autoResize: true, + layout: { type: 'grid' } as any, + node: { + style: { + size: 28, + fill: (d: any) => d.color || '#4a9eff', + labelText: (d: any) => truncate(d.title, 10), + labelFill: '#ddd', + labelFontSize: 11, + labelPlacement: 'bottom', + labelOffsetY: 4, + } + } as any, + edge: { style: { stroke: '#555', lineWidth: 1, endArrow: true } } as any, behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'], }); setGraph(g); - return () => { g.destroy(); }; + return () => { ro.disconnect(); g.destroy(); }; }, []); useEffect(() => { if (!graph) return; invoke(IPC.graph, undefined as never).then((data: GraphData) => { - graph.setData({ nodes: data.nodes.map(n => ({ id: n.id, ...n })), edges: data.edges.map(e => ({ source: e.source, target: e.target })) } as any); + const pos = layoutNodes(data.nodes, sizeRef.current.w, sizeRef.current.h); + graph.setData({ + nodes: data.nodes.map(n => ({ id: n.id, x: pos.get(n.id)!.x, y: pos.get(n.id)!.y, style: { fill: n.color }, data: { title: n.title, star: n.star } })), + edges: data.edges.map(e => ({ source: e.source, target: e.target })), + } as any); graph.render(); }); graph.on('node:click', (e: any) => { openDoc(e.target.id); }); }, [graph]); - return
; + return ( +
+
+
+ ); } + +function truncate(s: string, n: number) { return s.length > n ? s.slice(0, n) + '…' : s; }