fix: 图谱手动算横向树坐标铺开,修复只显示根节点

This commit is contained in:
hunter-dev
2026-08-24 21:08:47 +08:00
parent 39543a4456
commit 6d8abb95a3
@@ -4,26 +4,55 @@ import { invoke } from '../../ipc-client';
import { IPC, type TreeNode } from '@hunter/shared';
import { useStore } from '../../store';
interface MindNode { id: string; style?: any; data?: any; children?: MindNode[]; }
// 从扁平 TreeNode[] 构建 XMind 风格的树(根=库,文件夹=分支,文档=叶子)
function buildMindTree(nodes: TreeNode[]): MindNode {
const byParent = new Map<string, TreeNode[]>();
nodes.forEach(n => { const a = byParent.get(n.parent) ?? []; a.push(n); byParent.set(n.parent, a); });
const build = (parentPath: string): MindNode[] => (byParent.get(parentPath) ?? []).map(n => {
const isFolder = n.type === 'folder';
return {
id: n.path,
style: { fill: isFolder ? '#2a4a6a' : '#3a3a4a', stroke: isFolder ? '#4a9eff' : '#5a5a6a' },
data: { title: n.name, type: n.type, star: false },
children: isFolder ? build(n.path) : undefined,
};
});
return { id: '__root', style: { fill: '#1a3a5a', stroke: '#4a9eff' }, data: { title: '库', type: 'root' }, children: build('') };
}
interface TNode { id: string; name: string; type: string; parentId: string | null; }
function truncate(s: string, n: number) { return s.length > n ? s.slice(0, n) + '…' : s; }
// 扁平 TreeNode[] → 带 parentId 的节点列表 + edges
function flatten(nodes: TreeNode[]): { nodes: TNode[]; edges: { source: string; target: string }[] } {
const out: TNode[] = []; const edges: { source: string; target: string }[] = [];
out.push({ id: '__root', name: '库', type: 'root', parentId: null });
for (const n of nodes) {
const pid = n.parent === '' ? '__root' : n.parent;
out.push({ id: n.path, name: n.name, type: n.type, parentId: pid });
edges.push({ source: pid, target: n.path });
}
return { nodes: out, edges };
}
// 手动算横向树坐标:深度→x,同父同层→y 均匀分布
function layoutTree(nodes: TNode[], w: number, h: number): Map<string, { x: number; y: number }> {
const childrenOf = new Map<string, TNode[]>();
nodes.forEach(n => { const a = childrenOf.get(n.parentId ?? '__null') ?? []; a.push(n); childrenOf.set(n.parentId ?? '__null', a); });
const pos = new Map<string, { x: number; y: number }>();
const maxDepth = Math.max(1, ...nodes.map(n => depth(n, nodes)));
const xStep = (w - 120) / maxDepth;
// 计算每个节点子树叶子数,用于纵向分配空间
const leafCount = (id: string): number => {
const kids = childrenOf.get(id) ?? [];
if (kids.length === 0) return 1;
return kids.reduce((s, k) => s + leafCount(k.id), 0);
};
const place = (id: string, depthNum: number, yStart: number, ySpan: number) => {
const kids = childrenOf.get(id) ?? [];
pos.set(id, { x: 60 + depthNum * xStep, y: yStart + ySpan / 2 });
let accY = yStart;
for (const k of kids) {
const lc = leafCount(k.id);
place(k.id, depthNum + 1, accY, lc * (ySpan / Math.max(leafCount(id), 1)));
accY += lc * (ySpan / Math.max(leafCount(id), 1));
}
};
place('__root', 0, 40, h - 80);
return pos;
}
function depth(n: TNode, all: TNode[]): number {
if (!n.parentId || n.parentId === '__root') return n.parentId === '__root' ? 1 : 0;
const p = all.find(x => x.id === n.parentId);
return p ? depth(p, all) + 1 : 1;
}
export function GraphView() {
const containerRef = useRef<HTMLDivElement>(null);
const { openDoc } = useStore();
@@ -37,42 +66,39 @@ export function GraphView() {
container: el,
width: 1200, height: 700,
autoResize: true,
// 横向紧凑树(XMind 风格:根在左,向右展开)
layout: {
type: 'compactBox',
direction: 'LR',
getId: (d: any) => d.id,
getHeight: () => 28,
getWidth: () => 120,
getVGap: () => 12,
getHGap: () => 80,
} as any,
node: {
style: {
size: (d: any) => [120, 28] as any,
size: (d: any) => [130, 30] as any,
fill: (d: any) => (d.style && d.style.fill) || '#3a3a4a',
stroke: (d: any) => (d.style && d.style.stroke) || '#5a5a6a',
lineWidth: 1.5,
radius: 6,
lineWidth: 1.5, radius: 6,
labelText: (d: any) => truncate((d.data && d.data.title) || d.id, 14),
labelFill: '#eee', labelFontSize: 12, labelPlacement: 'center',
}
} as any,
edge: { style: { stroke: '#4a5a7a', lineWidth: 1.5 } } as any,
edge: { style: { stroke: '#4a5a7a', lineWidth: 1.5, type: 'cubic' } } as any,
behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],
});
} catch (e) { console.error(e); return; }
invoke(IPC.treeList, undefined as never).then((tree: TreeNode[]) => {
const mindRoot = buildMindTree(tree);
g.setData({ nodes: [mindRoot], edges: [] } as any);
const { nodes: flat, edges } = flatten(tree);
const rect = el.getBoundingClientRect();
const w = rect.width || 1200, h = rect.height || 700;
const pos = layoutTree(flat, w, h);
const gNodes = flat.map(n => ({
id: n.id,
x: pos.get(n.id)?.x ?? w / 2,
y: pos.get(n.id)?.y ?? h / 2,
style: { fill: n.type === 'root' ? '#1a3a5a' : n.type === 'folder' ? '#2a4a6a' : '#3a3a4a', stroke: n.type === 'root' ? '#4a9eff' : n.type === 'folder' ? '#4a9eff' : '#5a5a6a' },
data: { title: n.name, type: n.type },
}));
g.setData({ nodes: gNodes, edges } as any);
g.render();
setTimeout(() => { try { g.fitView(60); } catch {} }, 500);
}).catch(e => console.error(e));
g.on('node:click', (e: any) => {
if (e.target && e.target.id && !e.target.id.startsWith('__')) openDoc(e.target.id);
});
g.on('node:click', (e: any) => { if (e.target && e.target.id && !e.target.id.startsWith('__')) openDoc(e.target.id); });
return () => { try { g.destroy(); } catch {} };
}, []);