// 等待G6加载完成后再注册自定义节点 function registerCustomNode() { if (typeof window.G6 === 'undefined') { console.log('G6库尚未加载,100ms后重试...'); setTimeout(registerCustomNode, 100); return; } console.log('G6库已加载,开始注册自定义节点...'); G6.registerNode( 'hexagon-card', { draw(cfg, group) { const size = Array.isArray(cfg.size) ? cfg.size[0] : cfg.size || 110; const mastery = Math.max( 0, Math.min(1, cfg.meta?.mastery_level ?? 0) ); const percent = Math.round(mastery * 100); const locked = cfg.locked || false; const recommended = cfg.meta?.recommended; const hexagonPath = this.getHexagonPath(size); const palette = this.getPalette(mastery, locked); const ring = group.addShape('path', { attrs: { path: hexagonPath, stroke: palette.ring, lineWidth: palette.ringWidth, opacity: palette.ringOpacity, }, name: 'ring-shape', }); const hexagon = group.addShape('path', { attrs: { path: hexagonPath, fill: '#ffffff', stroke: palette.stroke, lineWidth: palette.lineWidth, shadowColor: palette.shadow, shadowBlur: palette.shadow ? 12 : 0, opacity: locked ? 0.55 : 1, cursor: locked ? 'not-allowed' : 'pointer', }, name: 'hexagon-shape', draggable: true, }); const cardWidth = size * 0.82; const cardHeight = size * 0.64; group.addShape('rect', { attrs: { x: -cardWidth / 2, y: -cardHeight / 2, width: cardWidth, height: cardHeight, fill: '#ffffff', radius: 6, opacity: locked ? 0.75 : 0.95, shadowColor: 'rgba(15, 23, 42, 0.12)', shadowBlur: 8, }, name: 'card-shape', }); group.addShape('text', { attrs: { text: `${cfg.meta?.code || cfg.id} · ${cfg.label || cfg.meta?.name || cfg.id}`, x: 0, y: -12, fontSize: 24, fontWeight: 700, fill: locked ? '#94a3b8' : '#0f172a', textAlign: 'center', textBaseline: 'middle', }, name: 'title-text', }); group.addShape('text', { attrs: { text: cfg.meta?.name || cfg.label || cfg.id, x: 0, y: 6, fontSize: 16, fontWeight: 700, fill: '#1e293b', textAlign: 'center', textBaseline: 'middle', }, name: 'code-text', }); group.addShape('text', { attrs: { text: `ID: ${cfg.id}`, x: 0, y: 22, fontSize: 13, fontWeight: 700, fill: '#0f172a', textAlign: 'center', textBaseline: 'middle', }, name: 'id-text', }); const barWidth = cardWidth - 16; const barHeight = 8; const barY = cardHeight / 2 - 8; group.addShape('text', { attrs: { text: '掌握度', x: -barWidth / 2, y: barY - 10, fontSize: 12, fontWeight: 700, fill: '#334155', textAlign: 'left', textBaseline: 'middle', }, name: 'mastery-label', }); group.addShape('rect', { attrs: { x: -barWidth / 2, y: barY, width: barWidth, height: barHeight, fill: '#e5e7eb', radius: 3, }, name: 'progress-bg', }); group.addShape('rect', { attrs: { x: -barWidth / 2, y: barY, width: (barWidth * percent) / 100, height: barHeight, fill: palette.progress, radius: 3, }, name: 'progress-fill', }); group.addShape('text', { attrs: { text: `${percent}%`, x: 0, y: barY + barHeight / 2, fontSize: 12, fontWeight: 800, fill: '#0f172a', textAlign: 'center', textBaseline: 'middle', }, name: 'percent-text', }); if (recommended && !locked) { group.addShape('circle', { attrs: { x: cardWidth / 2 - 8, y: -cardHeight / 2 + 10, r: 8, fill: 'rgba(251, 191, 36, 0.15)', stroke: '#f59e0b', }, name: 'recommend-pill', }); group.addShape('text', { attrs: { text: '荐', x: cardWidth / 2 - 8, y: -cardHeight / 2 + 10, fontSize: 12, fontWeight: 700, fill: '#b45309', textAlign: 'center', textBaseline: 'middle', }, name: 'recommend-text', }); } if (locked) { group.addShape('rect', { attrs: { x: -cardWidth / 2, y: -cardHeight / 2, width: cardWidth, height: cardHeight, fill: '#ffffff', opacity: 0.4, }, name: 'lock-mask', }); group.addShape('text', { attrs: { text: '🔒', x: 0, y: 0, fontSize: 16, textAlign: 'center', textBaseline: 'middle', }, name: 'lock-icon', }); } if (percent < 40) { group.addShape('rect', { attrs: { x: -cardWidth / 2, y: -cardHeight / 2, width: cardWidth, height: cardHeight, fill: '#ffffff', opacity: 0.15, }, name: 'weak-mask', }); } return hexagon; }, setState(name, value, item) { const group = item.getContainer(); const hexagon = group.find((e) => e.get('name') === 'hexagon-shape'); if (!hexagon) return; if (name === 'hover') { hexagon.animate( { lineWidth: value ? 4 : 3 }, { duration: 180 } ); group.animate( (ratio) => ({ matrix: [ 1 + (value ? ratio * 0.05 : -ratio * 0.05), 0, 0, 0, 1 + (value ? ratio * 0.05 : -ratio * 0.05), 0, 0, 0, 1, ], }), { duration: 200 } ); } if (name === 'selected') { hexagon.attr('shadowColor', value ? '#fb923c' : undefined); hexagon.attr('shadowBlur', value ? 16 : 0); } if (name === 'weak') { hexagon.attr('opacity', value ? 0.75 : 1); } if (name === 'locked') { hexagon.attr('opacity', value ? 0.5 : 1); hexagon.attr('cursor', value ? 'not-allowed' : 'pointer'); } }, getPalette(mastery, locked) { if (locked) { return { stroke: '#e2e8f0', lineWidth: 2.5, ring: '#e2e8f0', ringWidth: 4, ringOpacity: 0.35, shadow: '', progress: '#cbd5e1', }; } if (mastery >= 0.8) { return { stroke: '#d3b55f', lineWidth: 3, ring: '#f7e4ad', ringWidth: 7, ringOpacity: 0.4, shadow: 'rgba(212, 181, 95, 0.28)', progress: '#22c55e', }; } if (mastery >= 0.6) { return { stroke: '#34d399', lineWidth: 3, ring: '#bbf7d0', ringWidth: 6, ringOpacity: 0.28, shadow: 'rgba(52, 211, 153, 0.25)', progress: '#34d399', }; } if (mastery >= 0.4) { return { stroke: '#f59e0b', lineWidth: 3, ring: '#fde68a', ringWidth: 6, ringOpacity: 0.28, shadow: 'rgba(245, 158, 11, 0.18)', progress: '#f59e0b', }; } return { stroke: '#f87171', lineWidth: 2.5, ring: '#fee2e2', ringWidth: 6, ringOpacity: 0.32, shadow: 'rgba(248, 113, 113, 0.18)', progress: '#f87171', }; }, getHexagonPath(size) { const r = size / 2; const points = []; for (let i = 0; i < 6; i++) { const angle = (Math.PI / 3) * i - Math.PI / 2; points.push([r * Math.cos(angle), r * Math.sin(angle)]); } return [ ['M', points[0][0], points[0][1]], ...points.slice(1).map((p) => ['L', p[0], p[1]]), ['Z'], ]; }, }, 'single-node' ); console.log('自定义节点注册完成'); } // 启动注册流程 registerCustomNode();