fix: 图谱视图简化为单useEffect,修复节点不渲染;清理诊断代码

This commit is contained in:
hunter-dev
2026-08-24 20:55:33 +08:00
parent 2062eeceed
commit 13a80c7dc5
3 changed files with 61 additions and 56 deletions
+2 -3
View File
@@ -1,4 +1,4 @@
// main 进程入口(M1:创建窗口 + 初始化库 + 注册 IPC) // main 进程入口:创建窗口 + 初始化库 + 注册 IPC + 迁移检测
import { app, BrowserWindow, ipcMain } from 'electron'; import { app, BrowserWindow, ipcMain } from 'electron';
import nodePath from 'node:path'; import nodePath from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -32,7 +32,7 @@ function createWindow() {
app.whenReady().then(async () => { app.whenReady().then(async () => {
const vault = new VaultService(); const vault = new VaultService();
await vault.init(); // 确保有当前库 await vault.init();
// 旧版数据迁移检测(FR-D1/D2/D3):幂等,无旧数据则跳过 // 旧版数据迁移检测(FR-D1/D2/D3):幂等,无旧数据则跳过
const migrator = new MigrationService(app.getPath('userData')); const migrator = new MigrationService(app.getPath('userData'));
const oldDataDir = nodePath.join(app.getPath('userData'), 'legacy-data'); const oldDataDir = nodePath.join(app.getPath('userData'), 'legacy-data');
@@ -43,5 +43,4 @@ app.whenReady().then(async () => {
app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });
}); });
// 关闭前冲刷:渲染层监听窗口 close 事件自行 flush(FR-S4),M1 先以关闭前 onbeforeunload 兜底
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
+2 -2
View File
@@ -44,10 +44,10 @@ 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; const fs2 = vault.fileService; console.log('[GRAPH-HANDLER] fs2?', !!fs2);
if (!fs2) return { nodes: [], edges: [] }; if (!fs2) return { nodes: [], edges: [] };
const tree = await fs2.listTree(); const tree = await fs2.listTree();
return buildGraphData(fs2.getRoot(), tree); const gd = await buildGraphData(fs2.getRoot(), tree); return gd;
}); });
ipcMain.handle(IPC.copyMd, async (_e, p: { path: string }) => { ipcMain.handle(IPC.copyMd, async (_e, p: { path: string }) => {
@@ -1,20 +1,24 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef } from 'react';
import { Graph } from '@antv/g6'; import { Graph } from '@antv/g6';
import { invoke } from '../../ipc-client'; import { invoke } from '../../ipc-client';
import { IPC, type GraphData, type GraphNode } from '@hunter/shared'; import { IPC, type GraphData, type GraphNode } from '@hunter/shared';
import { useStore } from '../../store'; import { useStore } from '../../store';
// 按顶层分组固定坐标布局:每组一列,组内纵向排开,间距充足(FR-G2 组内分行排布) function truncate(s: string, n: number) { return s.length > n ? s.slice(0, n) + '…' : s; }
function layoutNodes(nodes: GraphNode[], width: number, height: 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 colWidth = groupList.length > 0 ? width / groupList.length : width;
const positions = new Map<string, { x: number; y: number }>(); const positions = new Map<string, { x: number; y: number }>();
if (groupList.length === 0) return positions;
const margin = 100;
const usableW = Math.max(200, w - margin * 2);
const colWidth = usableW / groupList.length;
groupList.forEach(([group, items], gi) => { groupList.forEach(([group, items], gi) => {
const colX = colWidth * gi + colWidth / 2; const colX = margin + colWidth * gi + colWidth / 2;
const rowGap = Math.min(80, height / Math.max(items.length, 1)); const rowGap = items.length > 1 ? Math.min(80, (h - margin * 2) / (items.length - 1)) : 0;
const startY = height / 2 - (items.length * rowGap) / 2 + rowGap / 2; const startY = h / 2 - ((items.length - 1) * rowGap) / 2;
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: startY + ni * rowGap }));
}); });
return positions; return positions;
@@ -22,53 +26,57 @@ function layoutNodes(nodes: GraphNode[], width: number, height: number) {
export function GraphView() { export function GraphView() {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [graph, setGraph] = useState<Graph | null>(null);
const { openDoc } = useStore(); const { openDoc } = useStore();
const sizeRef = useRef({ w: 800, h: 600 });
useEffect(() => { useEffect(() => {
if (!containerRef.current) return; if (!containerRef.current) return;
const el = containerRef.current; const el = containerRef.current;
const updateSize = () => { sizeRef.current = { w: el.clientWidth || 800, h: el.clientHeight || 600 }; }; const rect = el.getBoundingClientRect();
updateSize(); const w = rect.width || 800, h = rect.height || 600;
const ro = new ResizeObserver(updateSize); let g: Graph;
ro.observe(el); try {
const g = new Graph({ g = new Graph({
container: el, container: el,
width: sizeRef.current.w, width: w,
height: sizeRef.current.h, height: h,
autoResize: true, autoResize: true,
layout: { type: 'grid' } as any, node: {
node: { style: {
style: { size: 30,
size: 28, fill: (d: any) => (d.style && d.style.fill) || '#4a9eff',
fill: (d: any) => d.color || '#4a9eff', stroke: '#6a8cff',
labelText: (d: any) => truncate(d.title, 10), lineWidth: 2,
labelFill: '#ddd', labelText: (d: any) => truncate((d.data && d.data.title) || d.id, 12),
labelFontSize: 11, labelFill: '#eee',
labelPlacement: 'bottom', labelFontSize: 12,
labelOffsetY: 4, labelPlacement: 'bottom',
} labelOffsetY: 6,
} as any, }
edge: { style: { stroke: '#555', lineWidth: 1, endArrow: true } } as any, } as any,
behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'], edge: { style: { stroke: '#666', lineWidth: 1.5, endArrow: true } } as any,
}); behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],
setGraph(g); });
return () => { ro.disconnect(); g.destroy(); }; } catch (e) { console.error(e); return; }
}, []);
useEffect(() => {
if (!graph) return;
invoke(IPC.graph, undefined as never).then((data: GraphData) => { invoke(IPC.graph, undefined as never).then((data: GraphData) => {
const pos = layoutNodes(data.nodes, sizeRef.current.w, sizeRef.current.h); const r = el.getBoundingClientRect();
graph.setData({ const cw = r.width || 800, ch = r.height || 600;
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 } })), const pos = layoutNodes(data.nodes, cw, ch);
edges: data.edges.map(e => ({ source: e.source, target: e.target })), const nodes = data.nodes.map(n => ({
} as any); id: n.id,
graph.render(); x: pos.get(n.id)?.x ?? cw / 2,
}); y: pos.get(n.id)?.y ?? ch / 2,
graph.on('node:click', (e: any) => { openDoc(e.target.id); }); style: { fill: n.color, stroke: n.star ? '#ffd54a' : '#6a8cff' },
}, [graph]); data: { title: n.title, star: n.star },
}));
const edges = data.edges.map(e => ({ source: e.source, target: e.target }));
g.setData({ nodes, edges } as any);
g.render();
}).catch(e => console.error(e));
g.on('node:click', (e: any) => { if (e.target && e.target.id) openDoc(e.target.id); });
return () => { try { g.destroy(); } catch {} };
}, []);
return ( return (
<div style={{ flex: 1, background: '#111', position: 'relative', overflow: 'hidden' }}> <div style={{ flex: 1, background: '#111', position: 'relative', overflow: 'hidden' }}>
@@ -76,5 +84,3 @@ export function GraphView() {
</div> </div>
); );
} }
function truncate(s: string, n: number) { return s.length > n ? s.slice(0, n) + '…' : s; }