saler.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. <template>
  2. <div style="background:#f1f2f3" id="full">
  3. <img style="width: 25px; height: 25px;float: right" v-if="!fullscreen" @click="enterFullscreen" src="@/assets/icons/amplify.svg" title="全屏">
  4. <img style="width: 25px; height: 25px;float: right" @click="backFullscreen" v-if="fullscreen" src="@/assets/icons/reduce.svg" title="还原">
  5. <div id="container"></div>
  6. </div>
  7. </template>
  8. <script>
  9. import G6 from '@antv/g6';
  10. import axios from 'axios'
  11. let graph;
  12. const fittingString = (str, maxWidth, fontSize) => {
  13. const ellipsis = "...";
  14. const ellipsisLength = G6.Util.getTextSize(ellipsis, fontSize)[0];
  15. let currentWidth = 0;
  16. let res = str;
  17. const pattern = new RegExp("[\u4E00-\u9FA5]+"); // distinguish the Chinese charactors and letters
  18. str.split("").forEach((letter, i) => {
  19. if (currentWidth > maxWidth - ellipsisLength) return;
  20. if (pattern.test(letter)) {
  21. // Chinese charactors
  22. currentWidth += fontSize;
  23. } else {
  24. // get the width of single letter according to the fontSize
  25. currentWidth += G6.Util.getLetterWidth(letter, fontSize);
  26. }
  27. if (currentWidth > maxWidth - ellipsisLength) {
  28. res = `${str.substr(0, i)}${ellipsis}`;
  29. }
  30. });
  31. return res;
  32. };
  33. const ERROR_COLOR = 'red';
  34. const getNodeConfig = (node) => {
  35. if (node.nodeUrl) {
  36. return {
  37. basicColor: ERROR_COLOR,
  38. fontColor: '#FFF',
  39. borderColor: ERROR_COLOR,
  40. bgColor: '#ff5722',
  41. };
  42. }
  43. let config = {
  44. basicColor: '#fff',
  45. fontColor: '#333',
  46. borderColor: '#fff',
  47. bgColor: '#fff',
  48. };
  49. switch (node.type) {
  50. case 'root': {
  51. config = {
  52. basicColor: '#E3E6E8',
  53. fontColor: 'rgba(0,0,0,0.85)',
  54. borderColor: '#E3E6E8',
  55. bgColor: '#5b8ff9',
  56. };
  57. break;
  58. }
  59. default:
  60. break;
  61. }
  62. return config;
  63. };
  64. const COLLAPSE_ICON = function COLLAPSE_ICON(x, y, r) {
  65. return [
  66. ['M', x - r, y],
  67. ['a', r, r, 0, 1, 0, r * 2, 0],
  68. ['a', r, r, 0, 1, 0, -r * 2, 0],
  69. ['M', x - r + 4, y],
  70. ['L', x - r + 2 * r - 4, y],
  71. ];
  72. };
  73. const EXPAND_ICON = function EXPAND_ICON(x, y, r) {
  74. return [
  75. ['M', x - r, y],
  76. ['a', r, r, 0, 1, 0, r * 2, 0],
  77. ['a', r, r, 0, 1, 0, -r * 2, 0],
  78. ['M', x - r + 4, y],
  79. ['L', x - r + 2 * r - 4, y],
  80. ['M', x - r + r, y - r + 4],
  81. ['L', x, y + r - 4],
  82. ];
  83. };
  84. const nodeBasicMethod = {
  85. createNodeBox: (group, config, w, h, isRoot) => {
  86. /* 最外面的大矩形 */
  87. const container = group.addShape('rect', {
  88. attrs: {
  89. x: 0,
  90. y: 0,
  91. width: w,
  92. height: h,
  93. },
  94. // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
  95. name: 'big-rect-shape',
  96. });
  97. if (!isRoot) {
  98. /* 左边的小圆点 */
  99. group.addShape('circle', {
  100. attrs: {
  101. x: 3,
  102. y: h / 2,
  103. r: 0,
  104. fill: config.basicColor,
  105. },
  106. // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
  107. name: 'left-dot-shape',
  108. });
  109. }
  110. /* 矩形 */
  111. group.addShape('rect', {
  112. attrs: {
  113. x: 3,
  114. y: 0,
  115. width: w - 19,
  116. height: h,
  117. fill: config.bgColor,
  118. stroke: config.borderColor,
  119. radius: 2,
  120. cursor: 'pointer',
  121. },
  122. // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
  123. name: 'rect-shape',
  124. });
  125. /* 左边的粗线 */
  126. group.addShape('rect', {
  127. attrs: {
  128. x: 3,
  129. y: 0,
  130. width: 3,
  131. height: h,
  132. fill: config.basicColor,
  133. radius: 1.5,
  134. },
  135. // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
  136. name: 'left-border-shape',
  137. });
  138. return container;
  139. },
  140. /* 生成树上的 marker */
  141. createNodeMarker: (group, collapsed, x, y) => {
  142. group.addShape('circle', {
  143. attrs: {
  144. x,
  145. y,
  146. r: 13,
  147. fill: 'rgba(47, 84, 235, 0.05)',
  148. opacity: 0,
  149. zIndex: -2,
  150. },
  151. // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
  152. name: 'collapse-icon-bg',
  153. });
  154. group.addShape('marker', {
  155. attrs: {
  156. x,
  157. y,
  158. r: 7,
  159. symbol: collapsed ? EXPAND_ICON : COLLAPSE_ICON,
  160. stroke: 'rgba(0,0,0,0.25)',
  161. fill: 'rgba(0,0,0,0)',
  162. lineWidth: 1,
  163. cursor: 'pointer',
  164. },
  165. // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
  166. name: 'collapse-icon',
  167. });
  168. },
  169. afterDraw: (cfg, group) => {
  170. /* 操作 marker 的背景色显示隐藏 */
  171. const icon = group.find((element) => element.get('id') === 'collapse-icon');
  172. if (icon) {
  173. const bg = group.find((element) => element.get('id') === 'collapse-icon-bg');
  174. icon.on('mouseenter', () => {
  175. bg.attr('opacity', 1);
  176. graph.get('canvas').draw();
  177. });
  178. icon.on('mouseleave', () => {
  179. bg.attr('opacity', 0);
  180. graph.get('canvas').draw();
  181. });
  182. }
  183. /* ip 显示 */
  184. const ipBox = group.find((element) => element.get('id') === 'ip-box');
  185. if (ipBox) {
  186. /* ip 复制的几个元素 */
  187. const ipLine = group.find((element) => element.get('id') === 'ip-cp-line');
  188. const ipBG = group.find((element) => element.get('id') === 'ip-cp-bg');
  189. const ipIcon = group.find((element) => element.get('id') === 'ip-cp-icon');
  190. const ipCPBox = group.find((element) => element.get('id') === 'ip-cp-box');
  191. const onMouseEnter = () => {
  192. ipLine.attr('opacity', 1);
  193. ipBG.attr('opacity', 1);
  194. ipIcon.attr('opacity', 1);
  195. graph.get('canvas').draw();
  196. };
  197. const onMouseLeave = () => {
  198. ipLine.attr('opacity', 0);
  199. ipBG.attr('opacity', 0);
  200. ipIcon.attr('opacity', 0);
  201. graph.get('canvas').draw();
  202. };
  203. ipBox.on('mouseenter', () => {
  204. onMouseEnter();
  205. });
  206. ipBox.on('mouseleave', () => {
  207. onMouseLeave();
  208. });
  209. ipCPBox.on('mouseenter', () => {
  210. onMouseEnter();
  211. });
  212. ipCPBox.on('mouseleave', () => {
  213. onMouseLeave();
  214. });
  215. ipCPBox.on('click', () => { });
  216. }
  217. },
  218. setState: (name, value, item) => {
  219. const hasOpacityClass = [
  220. 'ip-cp-line',
  221. 'ip-cp-bg',
  222. 'ip-cp-icon',
  223. 'ip-cp-box',
  224. 'ip-box',
  225. 'collapse-icon-bg',
  226. ];
  227. const group = item.getContainer();
  228. const childrens = group.get('children');
  229. graph.setAutoPaint(false);
  230. if (name === 'emptiness') {
  231. if (value) {
  232. childrens.forEach((shape) => {
  233. if (hasOpacityClass.indexOf(shape.get('id')) > -1) {
  234. return;
  235. }
  236. shape.attr('opacity', 0.4);
  237. });
  238. } else {
  239. childrens.forEach((shape) => {
  240. if (hasOpacityClass.indexOf(shape.get('id')) > -1) {
  241. return;
  242. }
  243. shape.attr('opacity', 1);
  244. });
  245. }
  246. }
  247. graph.setAutoPaint(true);
  248. },
  249. };
  250. G6.registerNode('card-node', {
  251. options: {
  252. style: {
  253. stroke: '#ccc',
  254. },
  255. },
  256. draw: (cfg, group) => {
  257. const config = getNodeConfig(cfg);
  258. const isRoot = cfg.dataType === 'root';
  259. const nodeUrl = cfg.nodeUrl;
  260. /* the biggest rect */
  261. const container = nodeBasicMethod.createNodeBox(group, config, 243, 64, isRoot);
  262. if (cfg.dataType !== 'root') {
  263. /* the type text */
  264. group.addShape('text', {
  265. attrs: {
  266. text: cfg.dataType,
  267. x: 3,
  268. y: -10,
  269. fontSize: 12,
  270. textAlign: 'left',
  271. textBaseline: 'middle',
  272. fill: 'rgba(0,0,0,0.65)',
  273. },
  274. // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
  275. name: 'type-text-shape',
  276. });
  277. }
  278. if (cfg.ip) {
  279. /* ip start */
  280. /* ipBox */
  281. const ipRect = group.addShape('rect', {
  282. attrs: {
  283. fill: nodeUrl ? null : '#FFF',
  284. stroke: nodeUrl ? 'rgba(255,255,255,0.65)' : null,
  285. radius: 2,
  286. cursor: 'pointer',
  287. },
  288. // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
  289. name: 'ip-container-shape',
  290. });
  291. /* ip */
  292. const ipText = group.addShape('text', {
  293. attrs: {
  294. text: '666',
  295. x: 0,
  296. y: 19,
  297. fontSize: 12,
  298. textAlign: 'left',
  299. textBaseline: 'middle',
  300. fill: nodeUrl ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.65)',
  301. cursor: 'pointer',
  302. },
  303. // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
  304. name: 'ip-text-shape',
  305. });
  306. const ipBBox = ipText.getBBox();
  307. /* the distance from the IP to the right is 12px */
  308. ipText.attr({
  309. x: 224 - 12 - ipBBox.width,
  310. });
  311. /* ipBox */
  312. ipRect.attr({
  313. x: 224 - 12 - ipBBox.width - 4,
  314. y: ipBBox.minY - 5,
  315. width: ipBBox.width + 8,
  316. height: ipBBox.height + 10,
  317. });
  318. /* a transparent shape on the IP for click listener */
  319. group.addShape('rect', {
  320. attrs: {
  321. stroke: '',
  322. cursor: 'pointer',
  323. x: 224 - 12 - ipBBox.width - 4,
  324. y: ipBBox.minY - 5,
  325. width: ipBBox.width + 8,
  326. height: ipBBox.height + 10,
  327. fill: '#fff',
  328. opacity: 0,
  329. },
  330. // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
  331. name: 'ip-box',
  332. });
  333. /* copyIpLine */
  334. group.addShape('rect', {
  335. attrs: {
  336. x: 194,
  337. y: 7,
  338. width: 1,
  339. height: 24,
  340. fill: '#E3E6E8',
  341. opacity: 0,
  342. },
  343. // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
  344. name: 'ip-cp-line',
  345. });
  346. /* copyIpBG */
  347. group.addShape('rect', {
  348. attrs: {
  349. x: 195,
  350. y: 8,
  351. width: 22,
  352. height: 22,
  353. fill: '#FFF',
  354. cursor: 'pointer',
  355. opacity: 0,
  356. },
  357. // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
  358. name: 'ip-cp-bg',
  359. });
  360. /* copyIpIcon */
  361. group.addShape('image', {
  362. attrs: {
  363. x: 200,
  364. y: 13,
  365. height: 12,
  366. width: 10,
  367. img: 'https://os.alipayobjects.com/rmsportal/DFhnQEhHyPjSGYW.png',
  368. cursor: 'pointer',
  369. opacity: 0,
  370. },
  371. // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
  372. name: 'ip-cp-icon',
  373. });
  374. /* a transparent rect on the icon area for click listener */
  375. group.addShape('rect', {
  376. attrs: {
  377. x: 195,
  378. y: 8,
  379. width: 22,
  380. height: 22,
  381. fill: '#FFF',
  382. cursor: 'pointer',
  383. opacity: 0,
  384. },
  385. // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
  386. name: 'ip-cp-box',
  387. tooltip: 'Copy the IP',
  388. });
  389. /* ip end */
  390. }
  391. /* name */
  392. group.addShape('text', {
  393. attrs: {
  394. text: cfg.id,
  395. x: 19,
  396. y: 19,
  397. fontSize: 14,
  398. fontWeight: 700,
  399. textAlign: 'left',
  400. textBaseline: 'middle',
  401. fill: config.fontColor,
  402. cursor: 'pointer',
  403. },
  404. // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
  405. name: 'name-text-shape',
  406. });
  407. /* the description text */
  408. group.addShape('text', {
  409. attrs: {
  410. text: fittingString(cfg.value, 243 - 50, 14),
  411. x: 19,
  412. y: 45,
  413. fontSize: 14,
  414. textAlign: 'left',
  415. textBaseline: 'middle',
  416. fill: config.fontColor,
  417. cursor: 'pointer',
  418. },
  419. // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
  420. name: 'bottom-text-shape',
  421. });
  422. if (nodeUrl) {
  423. group.addShape('text', {
  424. attrs: {
  425. x: 191,
  426. y: 62,
  427. text: '⇢',
  428. fill: '#fff',
  429. fontSize: 18,
  430. },
  431. // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
  432. name: 'error-text-shape',
  433. });
  434. }
  435. const hasChildren = cfg.children && cfg.children.length > 0;
  436. if (hasChildren) {
  437. nodeBasicMethod.createNodeMarker(group, cfg.collapsed, 236, 32);
  438. }
  439. return container;
  440. },
  441. afterDraw: nodeBasicMethod.afterDraw,
  442. setState: nodeBasicMethod.setState,
  443. });
  444. export default {
  445. props:['id'],
  446. data () {
  447. return {
  448. fullscreen:false
  449. }
  450. },
  451. mounted () {
  452. // this.getData()
  453. document.addEventListener('fullscreenchange', this.handleFullscreenChange);
  454. document.addEventListener('mozfullscreenchange', this.handleFullscreenChange);
  455. document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange);
  456. document.addEventListener('MSFullscreenChange',this.handleFullscreenChange)
  457. },
  458. methods:{
  459. createMenu (array) {
  460. var that = this
  461. let arr = []
  462. const HASLINKS = ['有效线索数','有效项目数']
  463. function convertToElementTree(node) {
  464. // 新节点
  465. var elNode = {
  466. id: node['name'],
  467. nodeid:node['id'],
  468. value:node['labor'] ? node['labor'] !== node['name'] ? node['labor']:'-':'-',
  469. children: [],
  470. nodeUrl:HASLINKS.includes(node['name'])?'123':null
  471. }
  472. if (node.children && node.children.length > 0) {
  473. // 如果存在子节点
  474. for (var index = 0; index < node.children.length; index++) {
  475. // 遍历子节点, 把每个子节点看做一颗独立的树, 传入递归构造子树, 并把结果放回到新node的children中
  476. elNode.children.push(convertToElementTree(node.children[index]));
  477. }
  478. }
  479. return elNode;
  480. }
  481. array.forEach((element) => {
  482. arr.push(convertToElementTree(element))
  483. });
  484. return arr
  485. },
  486. async getData () {
  487. const res = await this.$api.requested({
  488. "id": 20230625101004,
  489. "content": {
  490. "hrid": this.id
  491. }
  492. })
  493. const container = document.getElementById('container');
  494. const width = container.scrollWidth;
  495. const height = container.scrollHeight || 500;
  496. const graph = new G6.TreeGraph({
  497. container: 'container',
  498. width,
  499. height,
  500. linkCenter: true,
  501. fitCenter: true,
  502. modes: {
  503. default: ['drag-canvas','zoom-canvas','collapse-expand','drag-node', 'lasso-select'],
  504. },
  505. defaultNode: {
  506. /* node type, the priority is lower than the type in the node data */
  507. type: 'card-node',
  508. },
  509. defaultEdge: {
  510. type: 'cubic-horizontal',
  511. },
  512. layout: {
  513. type: 'dendrogram',
  514. direction: 'RL',
  515. nodeSep: 100,
  516. rankSep: 250,
  517. radial: true,
  518. },
  519. });
  520. graph.node(function (node) {
  521. return {
  522. label: node.id,
  523. };
  524. });
  525. graph.data(this.createMenu([res.data])[0]);
  526. graph.render();
  527. graph.fitView();
  528. graph.on('node:click', evt => {
  529. const item = evt.item;
  530. console.log(item)
  531. })
  532. if (typeof window !== 'undefined')
  533. window.onresize = () => {
  534. if (!graph || graph.get('destroyed')) return;
  535. if (!container || !container.scrollWidth || !container.scrollHeight) return;
  536. graph.changeSize(container.scrollWidth, container.scrollHeight);
  537. };
  538. },
  539. enterFullscreen () {
  540. /* 获取(<html>)元素以全屏显示页面 */
  541. const full = document.getElementById('full')
  542. if (full.RequestFullScreen) {
  543. full.RequestFullScreen()
  544. //兼容Firefox
  545. } else if (full.mozRequestFullScreen) {
  546. full.mozRequestFullScreen()
  547. //兼容Chrome, Safari and Opera等
  548. } else if (full.webkitRequestFullScreen) {
  549. full.webkitRequestFullScreen()
  550. //兼容IE/Edge
  551. } else if (full.msRequestFullscreen) {
  552. full.msRequestFullscreen()
  553. }
  554. },
  555. handleFullscreenChange () {
  556. if (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement) {
  557. // 全屏模式激活
  558. console.log('全屏模式已激活');
  559. this.fullscreen = true
  560. } else {
  561. // 全屏模式退出
  562. this.fullscreen = false
  563. console.log('全屏模式已退出');
  564. }
  565. },
  566. /*全屏还原*/
  567. backFullscreen(){
  568. if (document.exitFullscreen) {
  569. document.exitFullscreen();
  570. } else if (document.webkitCancelFullScreen) {
  571. document.webkitCancelFullScreen();
  572. } else if (document.mozCancelFullScreen) {
  573. document.mozCancelFullScreen();
  574. } else if (document.msExitFullscreen) {
  575. document.msExitFullscreen();
  576. }
  577. },
  578. },
  579. }
  580. </script>
  581. <style>
  582. #container{
  583. height: 100vh;
  584. width: 100vw;
  585. }
  586. </style>