fix: 图谱按分组铺满画布坐标,不再挤左上角
This commit is contained in:
@@ -44,7 +44,7 @@ export function registerIpc(vault: VaultService) {
|
|||||||
ipcMain.handle(IPC.search, async (_e, p: { query: string }) => vault.searchService?.search(p.query) ?? []);
|
ipcMain.handle(IPC.search, async (_e, p: { query: string }) => vault.searchService?.search(p.query) ?? []);
|
||||||
ipcMain.handle(IPC.tagStats, async () => vault.searchService?.tagStats() ?? []);
|
ipcMain.handle(IPC.tagStats, async () => vault.searchService?.tagStats() ?? []);
|
||||||
ipcMain.handle(IPC.graph, async () => {
|
ipcMain.handle(IPC.graph, async () => {
|
||||||
const fs2 = vault.fileService; console.log('[GRAPH-HANDLER] fs2?', !!fs2);
|
const fs2 = vault.fileService;
|
||||||
if (!fs2) return { nodes: [], edges: [] };
|
if (!fs2) return { nodes: [], edges: [] };
|
||||||
const tree = await fs2.listTree();
|
const tree = await fs2.listTree();
|
||||||
const gd = await buildGraphData(fs2.getRoot(), tree); return gd;
|
const gd = await buildGraphData(fs2.getRoot(), tree); return gd;
|
||||||
|
|||||||
@@ -6,20 +6,20 @@ import { useStore } from '../../store';
|
|||||||
|
|
||||||
function truncate(s: string, n: number) { return s.length > n ? s.slice(0, n) + '…' : s; }
|
function truncate(s: string, n: number) { return s.length > n ? s.slice(0, n) + '…' : s; }
|
||||||
|
|
||||||
|
// 按顶层分组:每组一列,列内纵向均匀排开。坐标基于画布尺寸,保证铺满不挤。
|
||||||
function layoutNodes(nodes: GraphNode[], w: number, h: number) {
|
function layoutNodes(nodes: GraphNode[], w: number, h: number) {
|
||||||
const groups = new Map<string, GraphNode[]>();
|
const groups = new Map<string, GraphNode[]>();
|
||||||
nodes.forEach(n => { const a = groups.get(n.group) ?? []; a.push(n); groups.set(n.group, a); });
|
nodes.forEach(n => { const a = groups.get(n.group) ?? []; a.push(n); groups.set(n.group, a); });
|
||||||
const groupList = [...groups.entries()];
|
const groupList = [...groups.entries()];
|
||||||
const positions = new Map<string, { x: number; y: number }>();
|
const positions = new Map<string, { x: number; y: number }>();
|
||||||
if (groupList.length === 0) return positions;
|
if (groupList.length === 0 || w < 100 || h < 100) return positions;
|
||||||
const margin = 100;
|
const margin = 80;
|
||||||
const usableW = Math.max(200, w - margin * 2);
|
const colWidth = (w - margin * 2) / Math.max(groupList.length, 1);
|
||||||
const colWidth = usableW / groupList.length;
|
groupList.forEach(([, items], gi) => {
|
||||||
groupList.forEach(([group, items], gi) => {
|
|
||||||
const colX = margin + colWidth * gi + colWidth / 2;
|
const colX = margin + colWidth * gi + colWidth / 2;
|
||||||
const rowGap = items.length > 1 ? Math.min(80, (h - margin * 2) / (items.length - 1)) : 0;
|
const usableH = h - margin * 2;
|
||||||
const startY = h / 2 - ((items.length - 1) * rowGap) / 2;
|
const rowGap = items.length > 1 ? usableH / (items.length + 1) : 0;
|
||||||
items.forEach((n, ni) => positions.set(n.id, { x: colX, y: startY + ni * rowGap }));
|
items.forEach((n, ni) => positions.set(n.id, { x: colX, y: margin + rowGap * (ni + 1) }));
|
||||||
});
|
});
|
||||||
return positions;
|
return positions;
|
||||||
}
|
}
|
||||||
@@ -31,26 +31,19 @@ export function GraphView() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
const el = containerRef.current;
|
const el = containerRef.current;
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
const w = rect.width || 800, h = rect.height || 600;
|
|
||||||
let g: Graph;
|
let g: Graph;
|
||||||
try {
|
try {
|
||||||
g = new Graph({
|
g = new Graph({
|
||||||
container: el,
|
container: el,
|
||||||
width: w,
|
width: 1200, height: 700,
|
||||||
height: h,
|
|
||||||
autoResize: true,
|
autoResize: true,
|
||||||
node: {
|
node: {
|
||||||
style: {
|
style: {
|
||||||
size: 30,
|
size: 32,
|
||||||
fill: (d: any) => (d.style && d.style.fill) || '#4a9eff',
|
fill: (d: any) => (d.style && d.style.fill) || '#4a9eff',
|
||||||
stroke: '#6a8cff',
|
stroke: '#6a8cff', lineWidth: 2,
|
||||||
lineWidth: 2,
|
|
||||||
labelText: (d: any) => truncate((d.data && d.data.title) || d.id, 12),
|
labelText: (d: any) => truncate((d.data && d.data.title) || d.id, 12),
|
||||||
labelFill: '#eee',
|
labelFill: '#eee', labelFontSize: 12, labelPlacement: 'bottom', labelOffsetY: 6,
|
||||||
labelFontSize: 12,
|
|
||||||
labelPlacement: 'bottom',
|
|
||||||
labelOffsetY: 6,
|
|
||||||
}
|
}
|
||||||
} as any,
|
} as any,
|
||||||
edge: { style: { stroke: '#666', lineWidth: 1.5, endArrow: true } } as any,
|
edge: { style: { stroke: '#666', lineWidth: 1.5, endArrow: true } } as any,
|
||||||
@@ -59,13 +52,13 @@ export function GraphView() {
|
|||||||
} catch (e) { console.error(e); return; }
|
} catch (e) { console.error(e); return; }
|
||||||
|
|
||||||
invoke(IPC.graph, undefined as never).then((data: GraphData) => {
|
invoke(IPC.graph, undefined as never).then((data: GraphData) => {
|
||||||
const r = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
const cw = r.width || 800, ch = r.height || 600;
|
const w = rect.width || 1200, h = rect.height || 700;
|
||||||
const pos = layoutNodes(data.nodes, cw, ch);
|
const pos = layoutNodes(data.nodes, w, h);
|
||||||
const nodes = data.nodes.map(n => ({
|
const nodes = data.nodes.map(n => ({
|
||||||
id: n.id,
|
id: n.id,
|
||||||
x: pos.get(n.id)?.x ?? cw / 2,
|
x: pos.get(n.id)?.x ?? w / 2,
|
||||||
y: pos.get(n.id)?.y ?? ch / 2,
|
y: pos.get(n.id)?.y ?? h / 2,
|
||||||
style: { fill: n.color, stroke: n.star ? '#ffd54a' : '#6a8cff' },
|
style: { fill: n.color, stroke: n.star ? '#ffd54a' : '#6a8cff' },
|
||||||
data: { title: n.title, star: n.star },
|
data: { title: n.title, star: n.star },
|
||||||
}));
|
}));
|
||||||
@@ -83,4 +76,4 @@ export function GraphView() {
|
|||||||
<div ref={containerRef} style={{ position: 'absolute', inset: 0 }} />
|
<div ref={containerRef} style={{ position: 'absolute', inset: 0 }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user