saler.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  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 + Number(25*cfg.infoSize) , 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. /* name */
  279. group.addShape('text', {
  280. attrs: {
  281. text: cfg.id,
  282. x: 19,
  283. y: 19,
  284. fontSize: 14,
  285. fontWeight: 700,
  286. textAlign: 'left',
  287. textBaseline: 'middle',
  288. fill: config.fontColor,
  289. cursor: 'pointer',
  290. },
  291. // 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
  292. name: 'name-text-shape',
  293. });
  294. /* the description text */
  295. group.addShape('text', {
  296. attrs: {
  297. text: fittingString(cfg.value, 243 - 50, 14),
  298. x: 19,
  299. y: 45,
  300. fontSize: 14,
  301. textAlign: 'left',
  302. textBaseline: 'middle',
  303. fill: config.fontColor,
  304. cursor: 'pointer',
  305. },
  306. // 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
  307. name: 'bottom-text-shape',
  308. });
  309. if (cfg.infoSize > 0) {
  310. Object.keys(cfg.info).forEach((key,index) => {
  311. console.log(key, cfg.info[key],index);
  312. group.addShape('text', {
  313. attrs: {
  314. text: fittingString(String(cfg.info[key]), 243 - 50, 14),
  315. x: 19,
  316. y: 65 + (25*index) ,
  317. fontSize: 12,
  318. textAlign: 'left',
  319. textBaseline: 'middle',
  320. fill: config.fontColor,
  321. cursor: 'pointer',
  322. },
  323. // 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
  324. name: 'bottom-text-shape',
  325. });
  326. });
  327. }
  328. if (nodeUrl) {
  329. group.addShape('text', {
  330. attrs: {
  331. x: 191,
  332. y: 62,
  333. text: '⇢',
  334. fill: '#fff',
  335. fontSize: 18,
  336. },
  337. // 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
  338. name: 'error-text-shape',
  339. });
  340. }
  341. const hasChildren = cfg.children && cfg.children.length > 0;
  342. if (hasChildren) {
  343. nodeBasicMethod.createNodeMarker(group, cfg.collapsed, 236, 32);
  344. }
  345. return container;
  346. },
  347. afterDraw: nodeBasicMethod.afterDraw,
  348. setState: nodeBasicMethod.setState,
  349. });
  350. export default {
  351. props:['id'],
  352. data () {
  353. return {
  354. fullscreen:false
  355. }
  356. },
  357. mounted () {
  358. // this.getData()
  359. document.addEventListener('fullscreenchange', this.handleFullscreenChange);
  360. document.addEventListener('mozfullscreenchange', this.handleFullscreenChange);
  361. document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange);
  362. document.addEventListener('MSFullscreenChange',this.handleFullscreenChange)
  363. },
  364. methods:{
  365. createMenu (array) {
  366. var that = this
  367. let arr = []
  368. const HASLINKS = ['有效线索数','有效项目数']
  369. function convertToElementTree(node) {
  370. // 新节点
  371. var elNode = {
  372. id: node['name'],
  373. nodeid:node['id'],
  374. info:node['info']?node['info']:{},
  375. infoSize:node['infoSize']?node['infoSize']:1,
  376. value:node['labor']?node['labor'] !== node['name']?node['labor']:'-':'-',
  377. children: [],
  378. nodeUrl:HASLINKS.includes(node['name'])?'123':null
  379. }
  380. if (node.children && node.children.length > 0) {
  381. // 如果存在子节点
  382. for (var index = 0; index < node.children.length; index++) {
  383. // 遍历子节点, 把每个子节点看做一颗独立的树, 传入递归构造子树, 并把结果放回到新node的children中
  384. elNode.children.push(convertToElementTree(node.children[index]));
  385. }
  386. }
  387. return elNode;
  388. }
  389. array.forEach((element) => {
  390. arr.push(convertToElementTree(element))
  391. });
  392. return arr
  393. },
  394. async getData () {
  395. const res = await this.$api.requested({
  396. "id": 20230625101004,
  397. "content": {
  398. "hrid": this.id
  399. }
  400. })
  401. const container = document.getElementById('container');
  402. const width = container.scrollWidth;
  403. const height = container.scrollHeight || 500;
  404. const graph = new G6.TreeGraph({
  405. container: 'container',
  406. width,
  407. height,
  408. linkCenter: true,
  409. fitCenter: true,
  410. modes: {
  411. default: ['drag-canvas','zoom-canvas','collapse-expand','drag-node', 'lasso-select'],
  412. },
  413. defaultNode: {
  414. /* node type, the priority is lower than the type in the node data */
  415. type: 'card-node',
  416. },
  417. defaultEdge: {
  418. type: 'cubic-horizontal',
  419. },
  420. layout: {
  421. type: 'dendrogram',
  422. direction: 'RL',
  423. nodeSep: 100,
  424. rankSep: 250,
  425. radial: true,
  426. },
  427. });
  428. graph.node(function (node) {
  429. return {
  430. label: node.id,
  431. };
  432. });
  433. graph.data(this.createMenu([res.data])[0]);
  434. graph.render();
  435. graph.fitView();
  436. graph.on('node:click', evt => {
  437. const item = evt.item;
  438. console.log(item)
  439. })
  440. if (typeof window !== 'undefined')
  441. window.onresize = () => {
  442. if (!graph || graph.get('destroyed')) return;
  443. if (!container || !container.scrollWidth || !container.scrollHeight) return;
  444. graph.changeSize(container.scrollWidth, container.scrollHeight);
  445. };
  446. },
  447. enterFullscreen () {
  448. /* 获取(<html>)元素以全屏显示页面 */
  449. const full = document.getElementById('full')
  450. if (full.RequestFullScreen) {
  451. full.RequestFullScreen()
  452. //兼容Firefox
  453. } else if (full.mozRequestFullScreen) {
  454. full.mozRequestFullScreen()
  455. //兼容Chrome, Safari and Opera等
  456. } else if (full.webkitRequestFullScreen) {
  457. full.webkitRequestFullScreen()
  458. //兼容IE/Edge
  459. } else if (full.msRequestFullscreen) {
  460. full.msRequestFullscreen()
  461. }
  462. },
  463. handleFullscreenChange () {
  464. if (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement) {
  465. // 全屏模式激活
  466. console.log('全屏模式已激活');
  467. this.fullscreen = true
  468. } else {
  469. // 全屏模式退出
  470. this.fullscreen = false
  471. console.log('全屏模式已退出');
  472. }
  473. },
  474. /*全屏还原*/
  475. backFullscreen(){
  476. if (document.exitFullscreen) {
  477. document.exitFullscreen();
  478. } else if (document.webkitCancelFullScreen) {
  479. document.webkitCancelFullScreen();
  480. } else if (document.mozCancelFullScreen) {
  481. document.mozCancelFullScreen();
  482. } else if (document.msExitFullscreen) {
  483. document.msExitFullscreen();
  484. }
  485. },
  486. },
  487. }
  488. </script>
  489. <style>
  490. #container{
  491. height: 100vh;
  492. width: 100vw;
  493. }
  494. </style>