fix: 图谱手动算横向树坐标铺开,修复只显示根节点
This commit is contained in:
@@ -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 {} };
|
||||
}, []);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user