| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- 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'
- );
|