| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- // 等待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 hasMastery = Boolean(cfg.meta?.has_mastery);
- 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, hasMastery);
- 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: palette.fill,
- stroke: palette.stroke,
- lineWidth: palette.lineWidth,
- shadowColor: palette.shadow,
- shadowBlur: palette.shadow ? 12 : 0,
- opacity: locked ? 0.55 : hasMastery ? 1 : 0.6,
- 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: palette.cardFill,
- radius: 6,
- opacity: locked ? 0.75 : hasMastery ? 0.95 : 0.6,
- shadowColor: 'rgba(15, 23, 42, 0.12)',
- shadowBlur: 8,
- },
- name: 'card-shape',
- });
- group.addShape('text', {
- attrs: {
- text: `${cfg.id} · ${cfg.label || cfg.meta?.name || cfg.meta?.code || cfg.id}`,
- x: 0,
- y: -10,
- fontSize: 22,
- fontWeight: 800,
- fill: locked ? '#94a3b8' : '#0f172a',
- textAlign: 'center',
- textBaseline: 'middle',
- },
- name: 'title-text',
- });
- group.addShape('text', {
- attrs: {
- text: cfg.meta?.code || cfg.id,
- x: 0,
- y: 10,
- fontSize: 15,
- fontWeight: 700,
- fill: '#334155',
- textAlign: 'center',
- textBaseline: 'middle',
- },
- name: 'code-text',
- });
- const barWidth = cardWidth - 16;
- const barHeight = 8;
- const barY = cardHeight / 2 - 4;
- 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, hasMastery) {
- if (!hasMastery) {
- return {
- stroke: '#cbd5e1',
- lineWidth: 3,
- ring: '#e2e8f0',
- ringWidth: 6,
- ringOpacity: 0.35,
- shadow: 'rgba(148, 163, 184, 0.15)',
- progress: '#e5e7eb',
- fill: '#f8fafc',
- cardFill: '#ffffff',
- };
- }
- if (mastery >= 0.8) {
- return {
- stroke: '#d3b55f',
- lineWidth: 4,
- ring: '#f7e4ad',
- ringWidth: 8,
- ringOpacity: 0.55,
- shadow: 'rgba(212, 181, 95, 0.28)',
- progress: '#22c55e',
- fill: '#fffbeb',
- cardFill: '#ffffff',
- };
- }
- if (mastery >= 0.6) {
- return {
- stroke: '#34d399',
- lineWidth: 3.5,
- ring: '#bbf7d0',
- ringWidth: 7,
- ringOpacity: 0.45,
- shadow: 'rgba(52, 211, 153, 0.25)',
- progress: '#34d399',
- fill: '#ecfdf3',
- cardFill: '#ffffff',
- };
- }
- if (mastery >= 0.4) {
- return {
- stroke: '#f59e0b',
- lineWidth: 3.5,
- ring: '#fde68a',
- ringWidth: 7,
- ringOpacity: 0.45,
- shadow: 'rgba(245, 158, 11, 0.18)',
- progress: '#f59e0b',
- fill: '#fffbeb',
- cardFill: '#fff7ed',
- };
- }
- return {
- stroke: '#f87171',
- lineWidth: 3,
- ring: '#fecaca',
- ringWidth: 8,
- ringOpacity: 0.55,
- shadow: 'rgba(248, 113, 113, 0.25)',
- progress: '#f87171',
- fill: '#fef2f2',
- cardFill: '#fff1f2',
- };
- },
- 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'],
- ];
- },
- update(cfg, item) {
- const group = item.getContainer();
- 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 hasMastery = Boolean(cfg.meta?.has_mastery);
- const percent = Math.round(mastery * 100);
- const locked = cfg.locked || false;
- const palette = this.getPalette(mastery, locked, hasMastery);
- const ring = group.find((e) => e.get('name') === 'ring-shape');
- if (ring) {
- ring.attr({
- stroke: palette.ring,
- lineWidth: palette.ringWidth,
- opacity: palette.ringOpacity,
- });
- }
- const hexagon = group.find((e) => e.get('name') === 'hexagon-shape');
- if (hexagon) {
- hexagon.attr({
- stroke: palette.stroke,
- lineWidth: palette.lineWidth,
- shadowColor: palette.shadow,
- shadowBlur: palette.shadow ? 12 : 0,
- opacity: locked ? 0.55 : hasMastery ? 1 : 0.6,
- cursor: locked ? 'not-allowed' : 'pointer',
- fill: palette.fill,
- });
- }
- const title = group.find((e) => e.get('name') === 'title-text');
- if (title) {
- title.attr({
- text: `${cfg.id} · ${cfg.label || cfg.meta?.name || cfg.meta?.code || cfg.id}`,
- fill: locked ? '#94a3b8' : '#0f172a',
- });
- }
- const codeText = group.find((e) => e.get('name') === 'code-text');
- if (codeText) {
- codeText.attr({
- text: cfg.meta?.code || cfg.id,
- });
- }
- const progressFill = group.find((e) => e.get('name') === 'progress-fill');
- const progressBg = group.find((e) => e.get('name') === 'progress-bg');
- if (progressFill && progressBg) {
- const cardWidth = size * 0.82;
- const barWidth = cardWidth - 16;
- progressFill.attr({
- width: (barWidth * percent) / 100,
- fill: palette.progress,
- });
- }
- const card = group.find((e) => e.get('name') === 'card-shape');
- if (card) {
- card.attr({
- fill: palette.cardFill,
- });
- }
- const percentText = group.find((e) => e.get('name') === 'percent-text');
- if (percentText) {
- percentText.attr({
- text: `${percent}%`,
- });
- }
- },
- },
- 'single-node'
- );
- console.log('自定义节点注册完成');
- }
- // 启动注册流程
- registerCustomNode();
|