card-node.html 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <title>自定义卡片节点</title>
  8. </head>
  9. <body>
  10. <div id="mountNode"></div>
  11. <script src="../build/g6.js"></script>
  12. <script>
  13. const ERROR_COLOR = "#F5222D";
  14. const getNodeConfig = node => {
  15. if (node.nodeError) {
  16. return {
  17. basicColor: ERROR_COLOR,
  18. fontColor: "#FFF",
  19. borderColor: ERROR_COLOR,
  20. bgColor: "#E66A6C"
  21. };
  22. }
  23. let config = {
  24. basicColor: "#722ED1",
  25. fontColor: "#722ED1",
  26. borderColor: "#722ED1",
  27. bgColor: "#F6EDFC"
  28. };
  29. switch (node.type) {
  30. case "root": {
  31. config = {
  32. basicColor: "#E3E6E8",
  33. fontColor: "rgba(0,0,0,0.85)",
  34. borderColor: "#E3E6E8",
  35. bgColor: "#F7F9FA"
  36. };
  37. break;
  38. }
  39. default:
  40. break;
  41. }
  42. return config;
  43. };
  44. const COLLAPSE_ICON = function COLLAPSE_ICON(x, y, r) {
  45. return [
  46. ["M", x - r, y],
  47. ["a", r, r, 0, 1, 0, r * 2, 0],
  48. ["a", r, r, 0, 1, 0, -r * 2, 0],
  49. ["M", x - r + 4, y],
  50. ["L", x - r + 2 * r - 4, y]
  51. ];
  52. };
  53. const EXPAND_ICON = function EXPAND_ICON(x, y, r) {
  54. return [
  55. ["M", x - r, y],
  56. ["a", r, r, 0, 1, 0, r * 2, 0],
  57. ["a", r, r, 0, 1, 0, -r * 2, 0],
  58. ["M", x - r + 4, y],
  59. ["L", x - r + 2 * r - 4, y],
  60. ["M", x - r + r, y - r + 4],
  61. ["L", x, y + r - 4]
  62. ];
  63. };
  64. const nodeBasicMethod = {
  65. createNodeBox: (group, config, width, height, isRoot) => {
  66. /* 最外面的大矩形 */
  67. const container = group.addShape("rect", {
  68. attrs: {
  69. x: 0,
  70. y: 0,
  71. width,
  72. height
  73. }
  74. });
  75. if (!isRoot) {
  76. /* 左边的小圆点 */
  77. group.addShape("circle", {
  78. attrs: {
  79. x: 3,
  80. y: height / 2,
  81. r: 6,
  82. fill: config.basicColor
  83. }
  84. });
  85. }
  86. /* 矩形 */
  87. group.addShape("rect", {
  88. attrs: {
  89. x: 3,
  90. y: 0,
  91. width: width - 19,
  92. height,
  93. fill: config.bgColor,
  94. stroke: config.borderColor,
  95. radius: 2,
  96. cursor: "pointer"
  97. }
  98. });
  99. /* 左边的粗线 */
  100. group.addShape("rect", {
  101. attrs: {
  102. x: 3,
  103. y: 0,
  104. width: 3,
  105. height,
  106. fill: config.basicColor,
  107. radius: 1.5
  108. }
  109. });
  110. return container;
  111. },
  112. /* 生成树上的 marker */
  113. createNodeMarker: (group, collapsed, x, y) => {
  114. group.addShape("circle", {
  115. attrs: {
  116. x,
  117. y,
  118. r: 13,
  119. fill: "rgba(47, 84, 235, 0.05)",
  120. opacity: 0,
  121. zIndex: -2
  122. },
  123. className: "collapse-icon-bg"
  124. });
  125. group.addShape("marker", {
  126. attrs: {
  127. x,
  128. y,
  129. radius: 7,
  130. symbol: collapsed ? EXPAND_ICON : COLLAPSE_ICON,
  131. stroke: "rgba(0,0,0,0.25)",
  132. fill: "rgba(0,0,0,0)",
  133. lineWidth: 1,
  134. cursor: "pointer"
  135. },
  136. className: "collapse-icon"
  137. });
  138. },
  139. afterDraw: (cfg, group) => {
  140. /* 操作 marker 的背景色显示隐藏 */
  141. const icon = group.findByClassName("collapse-icon");
  142. if (icon) {
  143. const bg = group.findByClassName("collapse-icon-bg");
  144. icon.on("mouseenter", () => {
  145. bg.attr("opacity", 1);
  146. graph.get("canvas").draw();
  147. });
  148. icon.on("mouseleave", () => {
  149. bg.attr("opacity", 0);
  150. graph.get("canvas").draw();
  151. });
  152. }
  153. /* ip 显示 */
  154. const ipBox = group.findByClassName("ip-box");
  155. if (ipBox) {
  156. /* ip 复制的几个元素 */
  157. const ipLine = group.findByClassName("ip-cp-line");
  158. const ipBG = group.findByClassName("ip-cp-bg");
  159. const ipIcon = group.findByClassName("ip-cp-icon");
  160. const ipCPBox = group.findByClassName("ip-cp-box");
  161. const onMouseEnter = () => {
  162. this.ipHideTimer && clearTimeout(this.ipHideTimer);
  163. ipLine.attr("opacity", 1);
  164. ipBG.attr("opacity", 1);
  165. ipIcon.attr("opacity", 1);
  166. graph.get("canvas").draw();
  167. };
  168. const onMouseLeave = () => {
  169. this.ipHideTimer = setTimeout(() => {
  170. ipLine.attr("opacity", 0);
  171. ipBG.attr("opacity", 0);
  172. ipIcon.attr("opacity", 0);
  173. graph.get("canvas").draw();
  174. }, 100);
  175. };
  176. ipBox.on("mouseenter", () => {
  177. onMouseEnter();
  178. });
  179. ipBox.on("mouseleave", () => {
  180. onMouseLeave();
  181. });
  182. ipCPBox.on("mouseenter", () => {
  183. onMouseEnter();
  184. });
  185. ipCPBox.on("mouseleave", () => {
  186. onMouseLeave();
  187. });
  188. ipCPBox.on("click", () => {});
  189. }
  190. },
  191. setState: (name, value, item) => {
  192. const hasOpacityClass = [
  193. "ip-cp-line",
  194. "ip-cp-bg",
  195. "ip-cp-icon",
  196. "ip-cp-box",
  197. "ip-box",
  198. "collapse-icon-bg"
  199. ];
  200. const group = item.getContainer();
  201. const childrens = group.get("children");
  202. graph.setAutoPaint(false);
  203. if (name === "emptiness") {
  204. if (value) {
  205. childrens.forEach(shape => {
  206. if (hasOpacityClass.indexOf(shape.get("className")) > -1) {
  207. return;
  208. }
  209. shape.attr("opacity", 0.4);
  210. });
  211. } else {
  212. childrens.forEach(shape => {
  213. if (hasOpacityClass.indexOf(shape.get("className")) > -1) {
  214. return;
  215. }
  216. shape.attr("opacity", 1);
  217. });
  218. }
  219. }
  220. graph.setAutoPaint(true);
  221. }
  222. };
  223. /**
  224. * 计算字符串的长度
  225. * @param {string} str 指定的字符串
  226. */
  227. const calcStrLen = (str) => {
  228. let len = 0;
  229. for (let i = 0; i < str.length; i++) {
  230. if (str.charCodeAt(i) > 0 && str.charCodeAt(i) < 128) {
  231. len++;
  232. } else {
  233. len += 2;
  234. }
  235. }
  236. return len;
  237. }
  238. G6.registerNode(
  239. 'card-node',
  240. {
  241. drawShape: (cfg, group) => {
  242. const config = getNodeConfig(cfg);
  243. const isRoot = cfg.type === "root";
  244. const nodeError = cfg.nodeError;
  245. /* 最外面的大矩形 */
  246. const container = nodeBasicMethod.createNodeBox(
  247. group,
  248. config,
  249. 243,
  250. 64,
  251. isRoot
  252. );
  253. if (cfg.type !== "root") {
  254. /* 上边的 type */
  255. group.addShape("text", {
  256. attrs: {
  257. text: cfg.type,
  258. x: 3,
  259. y: -10,
  260. fontSize: 12,
  261. textAlign: "left",
  262. textBaseline: "middle",
  263. fill: "rgba(0,0,0,0.65)"
  264. }
  265. });
  266. }
  267. let ipWidth = 0;
  268. if (cfg.ip) {
  269. /* ip start */
  270. /* ipBox */
  271. const ipRect = group.addShape("rect", {
  272. attrs: {
  273. fill: nodeError ? null : "#FFF",
  274. stroke: nodeError ? "rgba(255,255,255,0.65)" : null,
  275. radius: 2,
  276. cursor: "pointer"
  277. }
  278. });
  279. /* ip */
  280. const ipText = group.addShape("text", {
  281. attrs: {
  282. text: cfg.ip,
  283. x: 0,
  284. y: 19,
  285. fontSize: 12,
  286. textAlign: "left",
  287. textBaseline: "middle",
  288. fill: nodeError ? "rgba(255,255,255,0.85)" : "rgba(0,0,0,0.65)",
  289. cursor: "pointer"
  290. }
  291. });
  292. const ipBBox = ipText.getBBox();
  293. /* ip 的文字总是距离右边 12px */
  294. ipText.attr({
  295. x: 224 - 12 - ipBBox.width
  296. });
  297. /* ipBox */
  298. ipRect.attr({
  299. x: 224 - 12 - ipBBox.width - 4,
  300. y: ipBBox.minY - 5,
  301. width: ipBBox.width + 8,
  302. height: ipBBox.height + 10
  303. });
  304. /* 在 IP 元素上面覆盖一层透明层,方便监听 hover 事件 */
  305. group.addShape("rect", {
  306. attrs: {
  307. stroke: "",
  308. cursor: "pointer",
  309. x: 224 - 12 - ipBBox.width - 4,
  310. y: ipBBox.minY - 5,
  311. width: ipBBox.width + 8,
  312. height: ipBBox.height + 10,
  313. fill: "#fff",
  314. opacity: 0
  315. },
  316. className: "ip-box"
  317. });
  318. /* copyIpLine */
  319. group.addShape("rect", {
  320. attrs: {
  321. x: 194,
  322. y: 7,
  323. width: 1,
  324. height: 24,
  325. fill: "#E3E6E8",
  326. opacity: 0
  327. },
  328. className: "ip-cp-line"
  329. });
  330. /* copyIpBG */
  331. group.addShape("rect", {
  332. attrs: {
  333. x: 195,
  334. y: 8,
  335. width: 22,
  336. height: 22,
  337. fill: "#FFF",
  338. cursor: "pointer",
  339. opacity: 0
  340. },
  341. className: "ip-cp-bg"
  342. });
  343. /* copyIpIcon */
  344. group.addShape("image", {
  345. attrs: {
  346. x: 200,
  347. y: 13,
  348. height: 12,
  349. width: 10,
  350. img: "https://os.alipayobjects.com/rmsportal/DFhnQEhHyPjSGYW.png",
  351. cursor: "pointer",
  352. opacity: 0
  353. },
  354. className: "ip-cp-icon"
  355. });
  356. /* 放一个透明的矩形在 icon 区域上,方便监听点击 */
  357. group.addShape("rect", {
  358. attrs: {
  359. x: 195,
  360. y: 8,
  361. width: 22,
  362. height: 22,
  363. fill: "#FFF",
  364. cursor: "pointer",
  365. opacity: 0
  366. },
  367. className: "ip-cp-box",
  368. tooltip: "复制IP"
  369. });
  370. const ipRectBBox = ipRect.getBBox();
  371. ipWidth = ipRectBBox.width;
  372. /* ip end */
  373. }
  374. /* name */
  375. const nameText = group.addShape("text", {
  376. attrs: {
  377. text: cfg.name,
  378. x: 19,
  379. y: 19,
  380. fontSize: 14,
  381. fontWeight: 700,
  382. textAlign: "left",
  383. textBaseline: "middle",
  384. fill: config.fontColor,
  385. cursor: "pointer"
  386. }
  387. // tooltip: cfg.name,
  388. });
  389. /* 下面的文字 */
  390. const remarkText = group.addShape("text", {
  391. attrs: {
  392. text: cfg.keyInfo,
  393. x: 19,
  394. y: 45,
  395. fontSize: 14,
  396. textAlign: "left",
  397. textBaseline: "middle",
  398. fill: config.fontColor,
  399. cursor: "pointer"
  400. }
  401. });
  402. if (nodeError) {
  403. group.addShape("text", {
  404. attrs: {
  405. x: 191,
  406. y: 62,
  407. text: '⚠️',
  408. fill: '#000',
  409. fontSize: 18
  410. }
  411. });
  412. }
  413. const hasChildren = cfg.children && cfg.children.length > 0;
  414. if (hasChildren) {
  415. nodeBasicMethod.createNodeMarker(group, cfg.collapsed, 236, 32);
  416. }
  417. return container;
  418. },
  419. afterDraw: nodeBasicMethod.afterDraw,
  420. setState: nodeBasicMethod.setState
  421. },
  422. "single-shape"
  423. );
  424. const data = {
  425. nodes: [
  426. {
  427. name: 'cardNodeApp',
  428. ip: '127.0.0.1',
  429. nodeError: true,
  430. type: 'root',
  431. keyInfo: 'this is a card node info',
  432. x: 100,
  433. y: 50
  434. },
  435. {
  436. name: 'cardNodeApp',
  437. ip: '127.0.0.1',
  438. nodeError: false,
  439. type: 'subRoot',
  440. keyInfo: 'this is sub root',
  441. x: 100,
  442. y: 150
  443. },
  444. {
  445. name: 'cardNodeApp',
  446. ip: '127.0.0.1',
  447. nodeError: false,
  448. type: 'subRoot',
  449. keyInfo: 'this is sub root',
  450. x: 100,
  451. y: 250,
  452. children: [
  453. {
  454. name: 'sub'
  455. }
  456. ]
  457. }
  458. ],
  459. edges: []
  460. }
  461. const graph = new G6.Graph({
  462. container: 'mountNode',
  463. width: 800,
  464. height: 600,
  465. defaultNode: {
  466. shape: 'card-node'
  467. }
  468. })
  469. graph.data(data)
  470. graph.render()
  471. </script>
  472. </body>
  473. </html>