g6-custom-node.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. G6.registerNode(
  2. 'hexagon-card',
  3. {
  4. draw(cfg, group) {
  5. const size = Array.isArray(cfg.size)
  6. ? cfg.size[0]
  7. : cfg.size || 110;
  8. const mastery = Math.max(
  9. 0,
  10. Math.min(1, cfg.meta?.mastery_level ?? 0)
  11. );
  12. const percent = Math.round(mastery * 100);
  13. const locked = cfg.locked || false;
  14. const recommended = cfg.meta?.recommended;
  15. const hexagonPath = this.getHexagonPath(size);
  16. const palette = this.getPalette(mastery, locked);
  17. const ring = group.addShape('path', {
  18. attrs: {
  19. path: hexagonPath,
  20. stroke: palette.ring,
  21. lineWidth: palette.ringWidth,
  22. opacity: palette.ringOpacity,
  23. },
  24. name: 'ring-shape',
  25. });
  26. const hexagon = group.addShape('path', {
  27. attrs: {
  28. path: hexagonPath,
  29. fill: '#ffffff',
  30. stroke: palette.stroke,
  31. lineWidth: palette.lineWidth,
  32. shadowColor: palette.shadow,
  33. shadowBlur: palette.shadow ? 12 : 0,
  34. opacity: locked ? 0.55 : 1,
  35. cursor: locked ? 'not-allowed' : 'pointer',
  36. },
  37. name: 'hexagon-shape',
  38. draggable: true,
  39. });
  40. const cardWidth = size * 0.82;
  41. const cardHeight = size * 0.64;
  42. group.addShape('rect', {
  43. attrs: {
  44. x: -cardWidth / 2,
  45. y: -cardHeight / 2,
  46. width: cardWidth,
  47. height: cardHeight,
  48. fill: '#ffffff',
  49. radius: 6,
  50. opacity: locked ? 0.75 : 0.95,
  51. shadowColor: 'rgba(15, 23, 42, 0.12)',
  52. shadowBlur: 8,
  53. },
  54. name: 'card-shape',
  55. });
  56. group.addShape('text', {
  57. attrs: {
  58. text: `${cfg.meta?.code || cfg.id} · ${cfg.label || cfg.meta?.name || cfg.id}`,
  59. x: 0,
  60. y: -12,
  61. fontSize: 24,
  62. fontWeight: 700,
  63. fill: locked ? '#94a3b8' : '#0f172a',
  64. textAlign: 'center',
  65. textBaseline: 'middle',
  66. },
  67. name: 'title-text',
  68. });
  69. group.addShape('text', {
  70. attrs: {
  71. text: cfg.meta?.name || cfg.label || cfg.id,
  72. x: 0,
  73. y: 6,
  74. fontSize: 16,
  75. fontWeight: 700,
  76. fill: '#1e293b',
  77. textAlign: 'center',
  78. textBaseline: 'middle',
  79. },
  80. name: 'code-text',
  81. });
  82. group.addShape('text', {
  83. attrs: {
  84. text: `ID: ${cfg.id}`,
  85. x: 0,
  86. y: 22,
  87. fontSize: 13,
  88. fontWeight: 700,
  89. fill: '#0f172a',
  90. textAlign: 'center',
  91. textBaseline: 'middle',
  92. },
  93. name: 'id-text',
  94. });
  95. const barWidth = cardWidth - 16;
  96. const barHeight = 8;
  97. const barY = cardHeight / 2 - 8;
  98. group.addShape('text', {
  99. attrs: {
  100. text: '掌握度',
  101. x: -barWidth / 2,
  102. y: barY - 10,
  103. fontSize: 12,
  104. fontWeight: 700,
  105. fill: '#334155',
  106. textAlign: 'left',
  107. textBaseline: 'middle',
  108. },
  109. name: 'mastery-label',
  110. });
  111. group.addShape('rect', {
  112. attrs: {
  113. x: -barWidth / 2,
  114. y: barY,
  115. width: barWidth,
  116. height: barHeight,
  117. fill: '#e5e7eb',
  118. radius: 3,
  119. },
  120. name: 'progress-bg',
  121. });
  122. group.addShape('rect', {
  123. attrs: {
  124. x: -barWidth / 2,
  125. y: barY,
  126. width: (barWidth * percent) / 100,
  127. height: barHeight,
  128. fill: palette.progress,
  129. radius: 3,
  130. },
  131. name: 'progress-fill',
  132. });
  133. group.addShape('text', {
  134. attrs: {
  135. text: `${percent}%`,
  136. x: 0,
  137. y: barY + barHeight / 2,
  138. fontSize: 12,
  139. fontWeight: 800,
  140. fill: '#0f172a',
  141. textAlign: 'center',
  142. textBaseline: 'middle',
  143. },
  144. name: 'percent-text',
  145. });
  146. if (recommended && !locked) {
  147. group.addShape('circle', {
  148. attrs: {
  149. x: cardWidth / 2 - 8,
  150. y: -cardHeight / 2 + 10,
  151. r: 8,
  152. fill: 'rgba(251, 191, 36, 0.15)',
  153. stroke: '#f59e0b',
  154. },
  155. name: 'recommend-pill',
  156. });
  157. group.addShape('text', {
  158. attrs: {
  159. text: '荐',
  160. x: cardWidth / 2 - 8,
  161. y: -cardHeight / 2 + 10,
  162. fontSize: 12,
  163. fontWeight: 700,
  164. fill: '#b45309',
  165. textAlign: 'center',
  166. textBaseline: 'middle',
  167. },
  168. name: 'recommend-text',
  169. });
  170. }
  171. if (locked) {
  172. group.addShape('rect', {
  173. attrs: {
  174. x: -cardWidth / 2,
  175. y: -cardHeight / 2,
  176. width: cardWidth,
  177. height: cardHeight,
  178. fill: '#ffffff',
  179. opacity: 0.4,
  180. },
  181. name: 'lock-mask',
  182. });
  183. group.addShape('text', {
  184. attrs: {
  185. text: '🔒',
  186. x: 0,
  187. y: 0,
  188. fontSize: 16,
  189. textAlign: 'center',
  190. textBaseline: 'middle',
  191. },
  192. name: 'lock-icon',
  193. });
  194. }
  195. if (percent < 40) {
  196. group.addShape('rect', {
  197. attrs: {
  198. x: -cardWidth / 2,
  199. y: -cardHeight / 2,
  200. width: cardWidth,
  201. height: cardHeight,
  202. fill: '#ffffff',
  203. opacity: 0.15,
  204. },
  205. name: 'weak-mask',
  206. });
  207. }
  208. return hexagon;
  209. },
  210. setState(name, value, item) {
  211. const group = item.getContainer();
  212. const hexagon = group.find((e) => e.get('name') === 'hexagon-shape');
  213. if (!hexagon) return;
  214. if (name === 'hover') {
  215. hexagon.animate(
  216. { lineWidth: value ? 4 : 3 },
  217. { duration: 180 }
  218. );
  219. group.animate(
  220. (ratio) => ({
  221. matrix: [
  222. 1 + (value ? ratio * 0.05 : -ratio * 0.05),
  223. 0,
  224. 0,
  225. 0,
  226. 1 + (value ? ratio * 0.05 : -ratio * 0.05),
  227. 0,
  228. 0,
  229. 0,
  230. 1,
  231. ],
  232. }),
  233. { duration: 200 }
  234. );
  235. }
  236. if (name === 'selected') {
  237. hexagon.attr('shadowColor', value ? '#fb923c' : undefined);
  238. hexagon.attr('shadowBlur', value ? 16 : 0);
  239. }
  240. if (name === 'weak') {
  241. hexagon.attr('opacity', value ? 0.75 : 1);
  242. }
  243. if (name === 'locked') {
  244. hexagon.attr('opacity', value ? 0.5 : 1);
  245. hexagon.attr('cursor', value ? 'not-allowed' : 'pointer');
  246. }
  247. },
  248. getPalette(mastery, locked) {
  249. if (locked) {
  250. return {
  251. stroke: '#e2e8f0',
  252. lineWidth: 2.5,
  253. ring: '#e2e8f0',
  254. ringWidth: 4,
  255. ringOpacity: 0.35,
  256. shadow: '',
  257. progress: '#cbd5e1',
  258. };
  259. }
  260. if (mastery >= 0.8) {
  261. return {
  262. stroke: '#d3b55f',
  263. lineWidth: 3,
  264. ring: '#f7e4ad',
  265. ringWidth: 7,
  266. ringOpacity: 0.4,
  267. shadow: 'rgba(212, 181, 95, 0.28)',
  268. progress: '#22c55e',
  269. };
  270. }
  271. if (mastery >= 0.6) {
  272. return {
  273. stroke: '#34d399',
  274. lineWidth: 3,
  275. ring: '#bbf7d0',
  276. ringWidth: 6,
  277. ringOpacity: 0.28,
  278. shadow: 'rgba(52, 211, 153, 0.25)',
  279. progress: '#34d399',
  280. };
  281. }
  282. if (mastery >= 0.4) {
  283. return {
  284. stroke: '#f59e0b',
  285. lineWidth: 3,
  286. ring: '#fde68a',
  287. ringWidth: 6,
  288. ringOpacity: 0.28,
  289. shadow: 'rgba(245, 158, 11, 0.18)',
  290. progress: '#f59e0b',
  291. };
  292. }
  293. return {
  294. stroke: '#f87171',
  295. lineWidth: 2.5,
  296. ring: '#fee2e2',
  297. ringWidth: 6,
  298. ringOpacity: 0.32,
  299. shadow: 'rgba(248, 113, 113, 0.18)',
  300. progress: '#f87171',
  301. };
  302. },
  303. getHexagonPath(size) {
  304. const r = size / 2;
  305. const points = [];
  306. for (let i = 0; i < 6; i++) {
  307. const angle = (Math.PI / 3) * i - Math.PI / 2;
  308. points.push([r * Math.cos(angle), r * Math.sin(angle)]);
  309. }
  310. return [
  311. ['M', points[0][0], points[0][1]],
  312. ...points.slice(1).map((p) => ['L', p[0], p[1]]),
  313. ['Z'],
  314. ];
  315. },
  316. },
  317. 'single-node'
  318. );