diff --git a/apps/desktop/src/main/services/graph-builder.ts b/apps/desktop/src/main/services/graph-builder.ts index 0a68c41..ffc58bd 100644 --- a/apps/desktop/src/main/services/graph-builder.ts +++ b/apps/desktop/src/main/services/graph-builder.ts @@ -8,8 +8,8 @@ export interface GraphNode { id: string; title: string; basePath: string; star: export interface GraphEdge { source: string; target: string; } export interface GraphData { nodes: GraphNode[]; edges: GraphEdge[]; } -// 12 色轮换 -const COLORS = ['#e6194b','#3cb44b','#ffe119','#4363d8','#f58231','#911eb4','#46f0f0','#f032e6','#bcf60c','#fabebe','#008080','#e6beff']; +// 分组着色——克制的墨灰阶(契合黑白灰玻璃设计体系,避免彩虹色破坏高级感) +const COLORS = ['#9aa1ad','#6d7480','#b6bcc6','#3c414b','#c8ccd4','#5c616b','#7c8491','#4a4f58','#aab1bd','#565b66','#8b929d','#626a75']; export async function buildGraphData(root: string, tree: TreeNode[]): Promise { const docs = tree.filter(n => n.type === 'doc'); diff --git a/apps/desktop/src/renderer/features/doc-info/DocInfoPanel.tsx b/apps/desktop/src/renderer/features/doc-info/DocInfoPanel.tsx index 757e717..9bd83f1 100644 --- a/apps/desktop/src/renderer/features/doc-info/DocInfoPanel.tsx +++ b/apps/desktop/src/renderer/features/doc-info/DocInfoPanel.tsx @@ -73,10 +73,12 @@ export function DocInfoPanel() { {links.length === 0 ? (
暂无关联,可在下方快速关联或批量关联
) : ( -
+
{links.map(l => ( -
- {linkNames.get(l) ?? l} +
{ e.currentTarget.style.borderColor = 'var(--line-strong)'; e.currentTarget.style.boxShadow = 'var(--shadow-lift)'; }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--control-border)'; e.currentTarget.style.boxShadow = 'var(--shadow-soft)'; }}> + + {linkNames.get(l) ?? l} + 双向
diff --git a/apps/desktop/src/renderer/features/graph/LinkGraph.tsx b/apps/desktop/src/renderer/features/graph/LinkGraph.tsx index 56e6d48..01723b4 100644 --- a/apps/desktop/src/renderer/features/graph/LinkGraph.tsx +++ b/apps/desktop/src/renderer/features/graph/LinkGraph.tsx @@ -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(); + 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(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; }');