custom.vue 15 KB

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