g6-custom-node.js 11 KB

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