feat(graph+info): 重新设计文档关联板块(玻璃卡片+双向提示)与知识图谱显示(关联高亮+分组色点+克制配色)

This commit is contained in:
hunter-dev
2026-09-26 00:23:45 +08:00
parent 3f2f6a4e37
commit 3f80e3270f
3 changed files with 36 additions and 18 deletions
@@ -91,8 +91,18 @@ export function LinkGraph({ onSwitchToEditor }: { onSwitchToEditor?: () => void
.map((e) => ({ source: idx.get(e.source)!, target: idx.get(e.target)! }));
// 曲线连线(贝塞尔弧):比直线更优雅、更符合"图谱"观感
const linkSel = g.append('g').attr('stroke', 'var(--line)').attr('stroke-opacity', 0.6)
.selectAll('path').data(links).join('path').attr('fill', 'none').attr('stroke-width', 1.1);
// 与当前文档直接相连的节点集合——用于高亮"关联"关系
const curPath = useStore.getState().currentPath;
const linkedIds = new Set<string>();
for (const e of data.edges) {
if (e.source === curPath) linkedIds.add(e.target);
if (e.target === curPath) linkedIds.add(e.source);
}
const linkSel = g.append('g')
.selectAll('path').data(links).join('path').attr('fill', 'none')
.attr('stroke', (d: any) => (d.source.id === curPath || d.target.id === curPath) ? 'var(--ink-2)' : 'var(--line)')
.attr('stroke-opacity', (d: any) => (d.source.id === curPath || d.target.id === curPath) ? 0.9 : 0.5)
.attr('stroke-width', (d: any) => (d.source.id === curPath || d.target.id === curPath) ? 2 : 1.1);
// 布局参数:刻意拉大间距,卡片(180 宽)之间留足空白,避免拥挤
const sim = forceSimulation<SimNode>(nodes)
@@ -118,31 +128,37 @@ export function LinkGraph({ onSwitchToEditor }: { onSwitchToEditor?: () => void
const len = Math.max(1, Math.hypot(dx, dy));
return 'M' + sx + ',' + sy + ' Q' + (mx + (nx / len) * bend * dir) + ',' + (my + (ny / len) * bend * dir) + ' ' + tx + ',' + ty;
};
// 当前打开的文档作为深墨焦点节点
const curPath = useStore.getState().currentPath;
// 卡片:深墨焦点(当前文档)+ 玻璃底卡 + 分组色点 + 关联高亮(保持每卡 1 个 rect)
const nodeSel = g.append('g').selectAll('g').data(nodes).join('g').attr('cursor', 'pointer')
// 阴影 filter 加在 g 层:若加在卡片 rect 上会导致该 rect 的 pointer-events 命中失效(点击穿透)
.attr('filter', (d: any) => d.id === curPath ? 'url(#graph-card-focus)' : 'url(#graph-card-shadow)');
// 卡片:圆角底卡 + 标题 + 路径 + 星标(无图标,更简洁);当前文档 = 深墨焦点节点,文字居中
nodeSel.append('rect').attr('class', 'gcard').attr('x', -CARD_W / 2).attr('y', -CARD_H / 2).attr('width', CARD_W).attr('height', CARD_H).attr('rx', 13)
nodeSel.append('rect').attr('class', 'gcard').attr('x', -CARD_W / 2).attr('y', -CARD_H / 2).attr('width', CARD_W).attr('height', CARD_H).attr('rx', 14)
.attr('fill', (d: any) => d.id === curPath ? 'var(--ink)' : 'var(--float)')
.attr('stroke', (d: any) => d.id === curPath ? 'var(--ink)' : 'var(--line)')
.attr('stroke-width', 1.2)
.attr('stroke', (d: any) => d.id === curPath ? 'var(--ink)' : (linkedIds.has(d.id) ? 'var(--ink-2)' : 'var(--line)'))
.attr('stroke-width', (d: any) => (d.id === curPath || linkedIds.has(d.id)) ? 1.6 : 1.1)
.attr('pointer-events', 'all');
// 分组色点(左上角,circle 不增加卡片 rect 计数)
nodeSel.append('circle').attr('cx', -CARD_W / 2 + 10).attr('cy', -CARD_H / 2 + 10).attr('r', 3.4)
.attr('fill', (d: any) => d.id === curPath ? 'var(--space)' : (d.color || 'var(--ink-3)'))
.attr('opacity', 0.9);
// 标题(水平居中)
nodeSel.append('text').attr('x', 0).attr('y', -2).attr('text-anchor', 'middle')
nodeSel.append('text').attr('x', 0).attr('y', -3).attr('text-anchor', 'middle')
.attr('fill', (d: any) => d.id === curPath ? 'var(--space)' : 'var(--ink)')
.attr('font-size', 13).attr('font-weight', (d: any) => d.id === curPath ? 600 : 550).attr('font-family', 'sans-serif')
.attr('font-size', 13).attr('font-weight', (d: any) => d.id === curPath ? 650 : (linkedIds.has(d.id) ? 600 : 550)).attr('font-family', 'sans-serif')
.text((d) => truncate(d.title, 16));
// 路径副文本(水平居中)
nodeSel.append('text').attr('x', 0).attr('y', 13).attr('text-anchor', 'middle')
.attr('fill', (d: any) => d.id === curPath ? 'rgba(255,255,255,0.7)' : 'var(--ink-4)')
nodeSel.append('text').attr('x', 0).attr('y', 14).attr('text-anchor', 'middle')
.attr('fill', (d: any) => d.id === curPath ? 'rgba(255,255,255,0.72)' : 'var(--ink-4)')
.attr('font-size', 10).attr('font-family', 'sans-serif')
.text((d) => truncate(d.basePath || d.id, 18));
// 星标(右上角)
nodeSel.filter((d) => d.star).append('text').attr('x', CARD_W / 2 - 14).attr('y', 4)
nodeSel.filter((d) => d.star).append('text').attr('x', CARD_W / 2 - 14).attr('y', 5)
.attr('fill', (d: any) => d.id === curPath ? 'var(--space)' : 'var(--ink-3)').attr('font-size', 14).text('★');
// 与当前文档关联的卡片右上角加一个小圆点提示(circle,不影响 rect 计数)
nodeSel.filter((d: any) => linkedIds.has(d.id) && d.id !== curPath).append('circle')
.attr('cx', CARD_W / 2 - 12).attr('cy', -CARD_H / 2 + 10).attr('r', 3)
.attr('fill', 'var(--ink-2)').attr('opacity', 0.85);
// 悬停高亮(CSS,不增加 rect)
svg.append('style').text('.gcard { transition: stroke-width .16s var(--ease), stroke .16s var(--ease); } g[data-hover] .gcard { stroke: var(--line-strong); stroke-width: 1.7; }');