- {/* 控制按钮 */}
+ {/* p5-style particle background */}
+
+
+ {/* Force graph layer */}
+
+ node._fullLabel || node.name}
+ nodeVal={(node: any) => node.val || 8}
+ nodeRelSize={6}
+ d3AlphaDecay={0.02}
+ d3AlphaMin={0.005}
+ cooldownTicks={200}
+ onNodeClick={(node: any) => handleNodeClick(node)}
+ onNodeDragEnd={(node: any) => {
+ if (node.id !== 'root') {
+ node.fx = node.x;
+ node.fy = node.y;
+ }
+ }}
+ onNodeHover={(node: any) => {
+ if (typeof document !== 'undefined') {
+ document.body.style.cursor = node ? 'pointer' : 'default';
+ }
+ }}
+ onEngineStop={() => {
+ if (graphRef.current) {
+ graphRef.current.centerAt(0, 0, 1000);
+ graphRef.current.zoomToFit(400, 30);
+ }
+ }}
+ nodeCanvasObject={(node: any, ctx: CanvasRenderingContext2D, globalScale: number) => {
+ if (!isFinite(node.x) || !isFinite(node.y)) return;
+ const label = node.name;
+ const color = node.color || '#a78bfa';
+ const nodeRadius = Math.max(3, (node.val || 8) * 0.6);
+
+ // 1. outer glow halo
+ const glowR = nodeRadius * 8;
+ const grd = ctx.createRadialGradient(
+ node.x, node.y, 0,
+ node.x, node.y, glowR
+ );
+ grd.addColorStop(0, color + '25');
+ grd.addColorStop(0.4, color + '0c');
+ grd.addColorStop(1, color + '00');
+ ctx.fillStyle = grd;
+ ctx.beginPath();
+ ctx.arc(node.x, node.y, glowR, 0, Math.PI * 2);
+ ctx.fill();
+
+ // 2. inner glow
+ const innerR = nodeRadius * 3;
+ const grd2 = ctx.createRadialGradient(
+ node.x, node.y, 0,
+ node.x, node.y, innerR
+ );
+ grd2.addColorStop(0, color + '70');
+ grd2.addColorStop(1, color + '00');
+ ctx.fillStyle = grd2;
+ ctx.beginPath();
+ ctx.arc(node.x, node.y, innerR, 0, Math.PI * 2);
+ ctx.fill();
+
+ // 3. core
+ ctx.beginPath();
+ ctx.arc(node.x, node.y, nodeRadius, 0, Math.PI * 2);
+ ctx.fillStyle = color;
+ ctx.fill();
+
+ // 4. bright center spot
+ ctx.beginPath();
+ ctx.arc(node.x, node.y, nodeRadius * 0.3, 0, Math.PI * 2);
+ ctx.fillStyle = 'rgba(255,255,255,0.75)';
+ ctx.fill();
+
+ // 5. label
+ const fontSize = Math.max(9, 13 / globalScale);
+ ctx.font = `600 ${fontSize}px "Noto Serif SC", Georgia, serif`;
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+
+ const tw = ctx.measureText(label).width;
+ const pad = 4 / globalScale;
+ const labelY = node.y + nodeRadius + fontSize / 2 + pad * 3;
+
+ // label background pill
+ const bw = tw + pad * 3;
+ const bh = fontSize + pad * 2;
+ const r = Math.max(2, 3 / globalScale);
+ const lx = node.x - bw / 2;
+ const ly = labelY - bh / 2;
+
+ ctx.fillStyle = 'rgba(10, 10, 26, 0.85)';
+ ctx.strokeStyle = color + '40';
+ ctx.lineWidth = Math.max(0.5, 0.8 / globalScale);
+
+ ctx.beginPath();
+ ctx.moveTo(lx + r, ly);
+ ctx.lineTo(lx + bw - r, ly);
+ ctx.quadraticCurveTo(lx + bw, ly, lx + bw, ly + r);
+ ctx.lineTo(lx + bw, ly + bh - r);
+ ctx.quadraticCurveTo(lx + bw, ly + bh, lx + bw - r, ly + bh);
+ ctx.lineTo(lx + r, ly + bh);
+ ctx.quadraticCurveTo(lx, ly + bh, lx, ly + bh - r);
+ ctx.lineTo(lx, ly + r);
+ ctx.quadraticCurveTo(lx, ly, lx + r, ly);
+ ctx.closePath();
+ ctx.fill();
+ ctx.stroke();
+
+ // label text
+ ctx.fillStyle = '#e2e8f0';
+ ctx.fillText(label, node.x, labelY);
+ }}
+ linkCanvasObjectMode={() => 'replace'}
+ linkCanvasObject={(link: any, ctx: CanvasRenderingContext2D, globalScale: number) => {
+ const src = link.source;
+ const tgt = link.target;
+ if (!isFinite(src.x) || !isFinite(src.y) || !isFinite(tgt.x) || !isFinite(tgt.y)) return;
+
+ const srcColor = src.color || '#a78bfa';
+ const tgtColor = tgt.color || '#a78bfa';
+
+ const dx = tgt.x - src.x;
+ const dy = tgt.y - src.y;
+ const len = Math.sqrt(dx * dx + dy * dy) || 1;
+
+ const curvature = 0.12;
+ const cpX = (src.x + tgt.x) / 2 + (-dy / len) * len * curvature;
+ const cpY = (src.y + tgt.y) / 2 + (dx / len) * len * curvature;
+
+ const gradient = ctx.createLinearGradient(src.x, src.y, tgt.x, tgt.y);
+ gradient.addColorStop(0, srcColor + '45');
+ gradient.addColorStop(1, tgtColor + '45');
+
+ ctx.beginPath();
+ ctx.moveTo(src.x, src.y);
+ ctx.quadraticCurveTo(cpX, cpY, tgt.x, tgt.y);
+ ctx.strokeStyle = gradient;
+ ctx.lineWidth = Math.max(
+ 0.5,
+ (link.type === 'root-chapter' ? 1.8 : 0.8) / Math.max(0.3, globalScale)
+ );
+ ctx.stroke();
+
+ // subtle glow line
+ ctx.beginPath();
+ ctx.moveTo(src.x, src.y);
+ ctx.quadraticCurveTo(cpX, cpY, tgt.x, tgt.y);
+ const glowGradient = ctx.createLinearGradient(src.x, src.y, tgt.x, tgt.y);
+ glowGradient.addColorStop(0, srcColor + '12');
+ glowGradient.addColorStop(1, tgtColor + '12');
+ ctx.strokeStyle = glowGradient;
+ ctx.lineWidth = Math.max(
+ 1.5,
+ (link.type === 'root-chapter' ? 5 : 3) / Math.max(0.3, globalScale)
+ );
+ ctx.stroke();
+ }}
+ width={dimensions.width}
+ height={dimensions.height}
+ />
+
+
+ {/* Controls */}
- {/* 知识图谱 */}
-
node._fullLabel || node.name}
- nodeColor={(node: any) => node.color || '#3b82f6'}
- nodeVal={(node: any) => node.val || 8}
- nodeRelSize={6}
- // 碰撞检测和力衰减优化
- d3AlphaDecay={0.02}
- d3AlphaMin={0.005}
- cooldownTicks={200}
- linkColor={(link: any) => {
- if (link.type === 'root-chapter') {
- return '#64748b'; // 灰色
- }
- return link.source.color || '#94a3b8'; // 使用章节颜色
- }}
- linkWidth={(link: any) => {
- if (link.type === 'root-chapter') {
- return 3;
- }
- return 2;
- }}
- linkDirectionalArrowLength={6}
- linkDirectionalArrowRelPos={1}
- linkCurvature={0.15}
- onNodeClick={(node: any) => handleNodeClick(node)}
- onNodeDragEnd={(node: any) => {
- // 保持根节点固定
- if (node.id !== 'root') {
- node.fx = node.x;
- node.fy = node.y;
- }
- }}
- onNodeHover={(node: any) => {
- if (typeof document !== 'undefined') {
- if (node) {
- document.body.style.cursor = 'pointer';
- } else {
- document.body.style.cursor = 'default';
- }
- }
- }}
- onEngineStop={() => {
- if (graphRef.current) {
- // 确保根节点在中心
- graphRef.current.centerAt(0, 0, 1000);
- graphRef.current.zoomToFit(400, 30);
- }
- }}
- nodeCanvasObject={(node: any, ctx: CanvasRenderingContext2D, globalScale: number) => {
- const label = node.name;
-
- // 获取分级标签配置(只使用字体大小和padding,不限制宽度)
- const config = LABEL_CONFIG[node.type as 'root' | 'chapter' | 'section'] || LABEL_CONFIG.section;
- const fontSize = Math.max(9, config.fontSize / globalScale);
- const padding = config.padding / globalScale;
-
- // 设置字体
- ctx.font = `bold ${fontSize}px "Microsoft YaHei", "SimHei", "Arial", sans-serif`;
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
-
- // 测量完整文本尺寸(不截断)
- const textWidth = ctx.measureText(label).width;
- const textHeight = fontSize;
-
- // 计算标签位置(在节点下方,根据节点大小调整间距)
- const nodeRadius = node.val || 8;
- const labelY = node.y + nodeRadius + textHeight / 2 + padding * 2;
- const labelX = node.x;
-
- // 计算背景尺寸(完全根据实际文本宽度,不限制)
- const bgWidth = textWidth + padding * 2;
- const bgHeight = textHeight + padding * 2;
- const radius = Math.max(2, 4 / globalScale);
-
- // 背景色(根据节点类型调整透明度)
- const bgAlpha = node.type === 'knowledge' ? 0.92 : 0.95;
- ctx.fillStyle = `rgba(255, 255, 255, ${bgAlpha})`;
- ctx.strokeStyle = node.color || '#3b82f6';
- ctx.lineWidth = Math.max(1, 1.5 / globalScale);
-
- // 绘制圆角矩形背景
- const x = labelX - bgWidth / 2;
- const y = labelY - textHeight / 2 - padding;
- ctx.beginPath();
- ctx.moveTo(x + radius, y);
- ctx.lineTo(x + bgWidth - radius, y);
- ctx.quadraticCurveTo(x + bgWidth, y, x + bgWidth, y + radius);
- ctx.lineTo(x + bgWidth, y + bgHeight - radius);
- ctx.quadraticCurveTo(x + bgWidth, y + bgHeight, x + bgWidth - radius, y + bgHeight);
- ctx.lineTo(x + radius, y + bgHeight);
- ctx.quadraticCurveTo(x, y + bgHeight, x, y + bgHeight - radius);
- ctx.lineTo(x, y + radius);
- ctx.quadraticCurveTo(x, y, x + radius, y);
- ctx.closePath();
- ctx.fill();
- ctx.stroke();
-
- // 绘制完整文本(使用节点颜色,不截断)
- ctx.fillStyle = node.color || '#3b82f6';
- ctx.fillText(label, labelX, labelY);
- }}
- width={dimensions.width}
- height={dimensions.height}
- />
-
- {/* 节点详情对话框 */}
- {selectedNode && selectedNode.nodeId && selectedNode.nodeType && (
-
- )}
+ {/* Node detail dialog */}
+ {selectedNode && selectedNode.nodeId && selectedNode.nodeType && (
+
+ )}
);
}
-