diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 23f6fce..d055ce3 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -1,4 +1,4 @@ -// main 进程入口(M1:创建窗口 + 初始化库 + 注册 IPC) +// main 进程入口:创建窗口 + 初始化库 + 注册 IPC + 迁移检测 import { app, BrowserWindow, ipcMain } from 'electron'; import nodePath from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -32,7 +32,7 @@ function createWindow() { app.whenReady().then(async () => { const vault = new VaultService(); - await vault.init(); // 确保有当前库 + await vault.init(); // 旧版数据迁移检测(FR-D1/D2/D3):幂等,无旧数据则跳过 const migrator = new MigrationService(app.getPath('userData')); 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(); }); }); -// 关闭前冲刷:渲染层监听窗口 close 事件自行 flush(FR-S4),M1 先以关闭前 onbeforeunload 兜底 -app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); \ No newline at end of file +app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); diff --git a/apps/desktop/src/main/ipc/handlers.ts b/apps/desktop/src/main/ipc/handlers.ts index 7394ae9..dd14d3c 100644 --- a/apps/desktop/src/main/ipc/handlers.ts +++ b/apps/desktop/src/main/ipc/handlers.ts @@ -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.tagStats, async () => vault.searchService?.tagStats() ?? []); ipcMain.handle(IPC.graph, async () => { - const fs2 = vault.fileService; + const fs2 = vault.fileService; console.log('[GRAPH-HANDLER] fs2?', !!fs2); if (!fs2) return { nodes: [], edges: [] }; 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 }) => { diff --git a/apps/desktop/src/renderer/features/graph/GraphView.tsx b/apps/desktop/src/renderer/features/graph/GraphView.tsx index 0b05072..b427850 100644 --- a/apps/desktop/src/renderer/features/graph/GraphView.tsx +++ b/apps/desktop/src/renderer/features/graph/GraphView.tsx @@ -1,20 +1,24 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef } from 'react'; import { Graph } from '@antv/g6'; import { invoke } from '../../ipc-client'; import { IPC, type GraphData, type GraphNode } from '@hunter/shared'; import { useStore } from '../../store'; -// 按顶层分组固定坐标布局:每组一列,组内纵向排开,间距充足(FR-G2 组内分行排布) -function layoutNodes(nodes: GraphNode[], width: number, height: number) { +function truncate(s: string, n: number) { return s.length > n ? s.slice(0, n) + '…' : s; } + +function layoutNodes(nodes: GraphNode[], w: number, h: 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(); + 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) => { - 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; + const colX = margin + colWidth * gi + colWidth / 2; + const rowGap = items.length > 1 ? Math.min(80, (h - margin * 2) / (items.length - 1)) : 0; + const startY = h / 2 - ((items.length - 1) * rowGap) / 2; items.forEach((n, ni) => positions.set(n.id, { x: colX, y: startY + ni * rowGap })); }); return positions; @@ -22,59 +26,61 @@ function layoutNodes(nodes: GraphNode[], width: number, height: number) { 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: 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 () => { ro.disconnect(); g.destroy(); }; - }, []); + const rect = el.getBoundingClientRect(); + const w = rect.width || 800, h = rect.height || 600; + let g: Graph; + try { + g = new Graph({ + container: el, + width: w, + height: h, + autoResize: true, + node: { + style: { + size: 30, + fill: (d: any) => (d.style && d.style.fill) || '#4a9eff', + stroke: '#6a8cff', + lineWidth: 2, + labelText: (d: any) => truncate((d.data && d.data.title) || d.id, 12), + labelFill: '#eee', + labelFontSize: 12, + labelPlacement: 'bottom', + labelOffsetY: 6, + } + } as any, + edge: { style: { stroke: '#666', lineWidth: 1.5, endArrow: true } } as any, + behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'], + }); + } catch (e) { console.error(e); return; } - useEffect(() => { - if (!graph) return; invoke(IPC.graph, undefined as never).then((data: GraphData) => { - 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]); + const r = el.getBoundingClientRect(); + const cw = r.width || 800, ch = r.height || 600; + const pos = layoutNodes(data.nodes, cw, ch); + const nodes = data.nodes.map(n => ({ + id: n.id, + x: pos.get(n.id)?.x ?? cw / 2, + y: pos.get(n.id)?.y ?? ch / 2, + style: { fill: n.color, stroke: n.star ? '#ffd54a' : '#6a8cff' }, + 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 (
); -} - -function truncate(s: string, n: number) { return s.length > n ? s.slice(0, n) + '…' : s; } +} \ No newline at end of file