project.vue 19 KB

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