g6-custom-node.js 14 KB

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