fix: 图谱按顶层分组固定坐标布局,节点不再挤压
This commit is contained in:
@@ -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<string, GraphNode[]>();
|
||||
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<string, { x: number; y: number }>();
|
||||
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<HTMLDivElement>(null);
|
||||
const [graph, setGraph] = useState<Graph | null>(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 <div ref={containerRef} style={{ flex: 1, background: '#111' }} />;
|
||||
return (
|
||||
<div style={{ flex: 1, background: '#111', position: 'relative', overflow: 'hidden' }}>
|
||||
<div ref={containerRef} style={{ position: 'absolute', inset: 0 }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function truncate(s: string, n: number) { return s.length > n ? s.slice(0, n) + '…' : s; }
|
||||
|
||||
Reference in New Issue
Block a user