virtualnformation.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. <!-- 虚端子关系 -->
  2. <template>
  3. <div class="main">
  4. <!-- 关联图 -->
  5. <div class="main-cont" id="treedom3">
  6. <div class="main-left">
  7. <div v-for="(item, index) in leftList" :key="index" class="conts">
  8. <div class="cont-title">
  9. <img :src="devicePng" alt="" class="img-item" />
  10. <div class="cont-item">
  11. <div>{{ item.descName }}</div>
  12. <div>{{ item.ied_name }}</div>
  13. </div>
  14. </div>
  15. <div
  16. v-for="(cItem, index2) in item.titleItems"
  17. :key="index2"
  18. :ref="(el) => setdomLeftChild3(el, cItem)"
  19. >
  20. <div class="text-midle">
  21. <div>
  22. {{
  23. `${cItem.attr_ld_inst}/${cItem.attr_prefix}${cItem.attr_ln_class}.${cItem.attr_do_name}.${cItem.attr_da_name}`
  24. }}
  25. </div>
  26. <div>{{ cItem.do_source_desc }}</div>
  27. </div>
  28. </div>
  29. </div>
  30. </div>
  31. <!-- 中间部分 -->
  32. <div class="main-middle" ref="middleHeight" id="end">
  33. <div class="cont-title">
  34. <img :src="devicePng" alt="" class="img-item" />
  35. <div class="middle-title" v-if="tagList">
  36. <div>{{ middleTitelDesc }}</div>
  37. <div>{{ tagList.ied_name }}</div>
  38. </div>
  39. </div>
  40. <div class="middle-item">
  41. <div
  42. class="midle-cont"
  43. v-for="(item, index) in svInfo"
  44. :key="index"
  45. :ref="(el) => setdomMiddle3(el, item)"
  46. >
  47. <div
  48. style="margin: 0 4px"
  49. v-if="!item.isDecollate"
  50. :class="{
  51. 'del-middle': item.opt == 'd',
  52. 'upt-middle': item.opt == 'u',
  53. }"
  54. >
  55. {{ item.no }}
  56. </div>
  57. <div
  58. class="midlestyle"
  59. v-if="!item.isDecollate"
  60. :class="{
  61. 'add-middle': item.opt == 'i',
  62. 'del-middle': item.opt == 'd',
  63. 'upt-middle': item.opt == 'u',
  64. }"
  65. >
  66. <div class="middle-cont">
  67. <div v-if="item.inout_type == 'out'">
  68. <span v-if="item.attr_da_name">
  69. {{
  70. `${item.attr_ld_inst}/${item.attr_prefix}${item.attr_ln_class}${item.attr_ln_inst}.${item.attr_do_name}.${item.attr_da_name}`
  71. }}
  72. </span>
  73. <span v-else>
  74. {{
  75. `${item.attr_ld_inst}/${item.attr_prefix}${item.attr_ln_class}${item.attr_ln_inst}.${item.attr_do_name}`
  76. }}
  77. </span>
  78. </div>
  79. <div v-else-if="item.inout_type == 'in'">
  80. {{ item.attr_int_addr }}
  81. </div>
  82. <div v-if="item.inout_type == 'out'">
  83. {{ item.do_source_desc }}
  84. </div>
  85. <div v-else-if="item.inout_type == 'in'">
  86. {{ item.do_target_desc }}
  87. </div>
  88. </div>
  89. <div style="flex: 1">
  90. <img
  91. :src="addMiddlePng"
  92. alt=""
  93. class="img-item-middle"
  94. v-if="item.opt == 'i'"
  95. />
  96. <img
  97. :src="delMiddlePng"
  98. alt=""
  99. class="img-item-middle"
  100. v-else-if="item.opt == 'd'"
  101. />
  102. <img
  103. :src="uptMiddlePng"
  104. alt=""
  105. class="img-item-middle"
  106. v-else-if="item.opt == 'u'"
  107. />
  108. </div>
  109. </div>
  110. <!-- 省略号 -->
  111. <div class="midlestyle omit" v-if="item.isDecollate">
  112. {{ item.do_source_desc }}
  113. </div>
  114. </div>
  115. </div>
  116. </div>
  117. <!-- 右侧 -->
  118. <div class="main-right">
  119. <div v-for="(item, index) in rightList" :key="index" class="conts">
  120. <div class="cont-title">
  121. <img :src="devicePng" alt="" class="img-item" />
  122. <div class="cont-item">
  123. <div>{{ item.descName }}</div>
  124. <div>{{ item.ied_name }}</div>
  125. </div>
  126. </div>
  127. <div
  128. v-for="(cItem, index2) in item.titleItems"
  129. :key="index2"
  130. :ref="(el) => setdomRightChild3(el, cItem)"
  131. >
  132. <div class="text-midle">
  133. <div>{{ `${cItem.attr_int_addr}` }}</div>
  134. <div>{{ cItem.do_target_desc }}</div>
  135. </div>
  136. </div>
  137. </div>
  138. </div>
  139. <div id="wrapper"></div>
  140. </div>
  141. </div>
  142. </template>
  143. <script setup>
  144. import { onMounted, watch, ref, nextTick, defineEmits, inject } from "vue";
  145. import devicePng from "@/assets/image/instruct/device.png";
  146. import addMiddlePng from "@/assets/image/CID/add_middle.png";
  147. import delMiddlePng from "@/assets/image/CID/del_middle.png";
  148. import uptMiddlePng from "@/assets/image/CID/upt_middle.png";
  149. import LeaderLine from "../../../../public/leader-line.min.js";
  150. import AnimEvent from "../../../../public/anim-event.min.js";
  151. import { compResult } from "@/api/scdCheck/scdCheck2";
  152. import { scdIedRelation } from "@/api/iedNetwork";
  153. import { useRoute } from "vue-router";
  154. const route = useRoute();
  155. const props = defineProps({
  156. //对比结果scd所传参数
  157. clickRowDatas: {
  158. type: Object,
  159. default: () => {},
  160. },
  161. clickList: {
  162. type: Object,
  163. default: () => {},
  164. },
  165. clickCodeValue: {
  166. type: String,
  167. default: "",
  168. },
  169. OpensclTrue: {
  170. type: Boolean,
  171. default: false,
  172. },
  173. recordDelIedVer: {
  174. //删除的列表数据,从sclUpdate中获取的
  175. type: Array,
  176. default: () => [],
  177. },
  178. });
  179. const svInfo = ref(null);
  180. //处理两边的数据
  181. const processBoth = (list, svResInfo, inoutType) => {
  182. const iedNames = {};
  183. if(!list||!svResInfo.data) return;
  184. list.forEach(async (item, index) => {
  185. item.titleItems = [];
  186. svResInfo.data.forEach((key) => {
  187. if (key.ied_name == item.ied_name) {
  188. if (key.inout_type == inoutType) {
  189. item.titleItems.push(key);
  190. }
  191. }
  192. });
  193. let iedNames = {};
  194. //为了得到每个小板块的标题描述
  195. if (!iedNames[item.ied_name]) {
  196. iedNames[item.ied_name] = true;
  197. if (!item.isdel) {
  198. const iedRes = await scdIedRelation({
  199. scd_id: scdIdValue,
  200. ied_name: item.ied_name,
  201. reset: 1,
  202. });
  203. if(!item.ied_name||!iedRes.data[item.ied_name]) return;
  204. item.descName = iedRes.data[item.ied_name].desc;
  205. } else {
  206. const iedRes = await scdIedRelation({
  207. scd_id: props.clickRowDatas.target_id,
  208. ied_name: item.ied_name,
  209. reset: 1,
  210. });
  211. item.descName = iedRes.data[item.ied_name].desc;
  212. }
  213. }
  214. });
  215. };
  216. let leaderLines3 = ref([]); //控制线条显示
  217. const leftList = ref([]);
  218. const rightList = ref([]);
  219. const domListMiddle3 = ref(new Map()); //获取中间所有的ref
  220. const domListRightChild3 = ref(new Map()); //获取右侧所有子的ref
  221. const domListLeftChild3 = ref(new Map()); //获取中间所有子的ref
  222. const emit = defineEmits(["result"]); //如果不加这个再次点击左侧会没有反应
  223. const setdomMiddle3 = (el, item) => {
  224. // 中间dom
  225. if (el) {
  226. domListMiddle3.value.set(item, el);
  227. }
  228. };
  229. //左侧子Dom
  230. const setdomLeftChild3 = (el, item) => {
  231. if (el) {
  232. domListLeftChild3.value.set(item, el);
  233. }
  234. };
  235. //右侧子Dom
  236. const setdomRightChild3 = (el, item) => {
  237. if (el) {
  238. domListRightChild3.value.set(item, el);
  239. }
  240. };
  241. let tagList = ref(props.clickList); //左侧更改的设备列表
  242. // //scd一致性对比===
  243. const clickRow = ref(props.clickRowDatas);
  244. // //对比文件:头部对比的单个数据
  245. watch(
  246. () => props.clickRowDatas,
  247. (newValue) => {
  248. if (newValue) {
  249. clickRow.value = newValue;
  250. }
  251. }
  252. );
  253. //点击装置列表的数据变化
  254. watch(
  255. () => props.clickList,
  256. (newValue) => {
  257. if (newValue) {
  258. tagList.value = newValue;
  259. clickResetLine3()
  260. getNetworkInfo3();
  261. }
  262. }
  263. );
  264. const cClickCode = ref(props.clickCodeValue); //点击侧边栏差异项的code
  265. watch(
  266. () => props.clickCodeValue,
  267. (newValue) => {
  268. if (newValue) {
  269. cClickCode.value = newValue;
  270. clickResetLine3();
  271. getNetworkInfo3();
  272. }
  273. }
  274. );
  275. // //scd一致性对比====
  276. const bothdataList = ref([]); //处理得到总的两边的数据
  277. const middleTitelDesc = ref('');//中间数据的标题
  278. //得到中间的子版块数据
  279. const getNetworkInfo3 = async () => {
  280. bothdataList.value = [];
  281. leftList.value = [];
  282. rightList.value = [];
  283. let svResInfo;
  284. //scd一致性对比====
  285. const ids = clickRow.value ? clickRow.value.id : "";
  286. const names = tagList.value ? tagList.value.ied_name : "";
  287. svResInfo = await compResult({
  288. comp_id: ids,
  289. ied_name: names,
  290. comptype: "u",
  291. itemcode: cClickCode.value,
  292. });
  293. const iedRes = await scdIedRelation({
  294. scd_id: scdIdValue,
  295. ied_name: names,
  296. reset: 1,
  297. });
  298. middleTitelDesc.value = iedRes.data?iedRes.data[names].desc:''
  299. //省略号===
  300. const data = {
  301. attr_ld_inst: "",
  302. attr_ln_class: "",
  303. attr_ln_inst: "",
  304. attr_do_name: "",
  305. attr_da_name: "",
  306. do_source_desc: "...",
  307. no: "",
  308. isDecollate: true,
  309. };
  310. //省略号===
  311. if(!svResInfo.data) return;
  312. svResInfo.data.sort(function (a, b) {
  313. return a.no - b.no; // 从小到大排序
  314. });
  315. //得到两边总的每个版块的ied_name名称
  316. const maps = new Map();
  317. svResInfo.data.forEach((item) => {
  318. const key = item.ied_name + item.inout_type; //输入输出都是需要的
  319. if (!maps.has(key)) {
  320. maps.set(key, true);
  321. if (item.inout_type == "in") {
  322. leftList.value.push(item);
  323. } else if (item.inout_type == "out") {
  324. rightList.value.push(item);
  325. }
  326. }
  327. });
  328. if(!props.recordDelIedVer) return;
  329. // recordDelIedVer 删除的全部数据
  330. props.recordDelIedVer.forEach((itemdel) => {
  331. svResInfo.data.forEach((item) => {
  332. if (itemdel.ied_name == item.ied_name) {
  333. item.isdel = true;
  334. }
  335. });
  336. });
  337. //处理两边的数据
  338. if (svResInfo.data.length > 0) {
  339. processBoth(leftList.value, svResInfo, "in");
  340. processBoth(rightList.value, svResInfo, "out");
  341. }
  342. //处理中间的数据有省略号的
  343. let newData = [];
  344. for (let i = 0; i < svResInfo.data.length; i++) {
  345. newData.push(svResInfo.data[i]);
  346. if (
  347. i < svResInfo.data.length - 1 &&
  348. svResInfo.data[i].ied_name != svResInfo.data[i + 1].ied_name
  349. ) {
  350. newData.push(data);
  351. }
  352. }
  353. svInfo.value = newData;
  354. };
  355. //点击后重置数据和线条
  356. const clickResetLine3 = () => {
  357. domListMiddle3.value.clear();
  358. domListLeftChild3.value.clear();
  359. domListRightChild3.value.clear();
  360. leaderLines3.value = [];
  361. removeLine3();
  362. setLine();
  363. };
  364. const setLeaderline = () => {
  365. //左侧子组件
  366. for (let [key, value] of domListMiddle3.value) {
  367. for (const [key2, value2] of domListLeftChild3.value) {
  368. const endDom = value2;
  369. LeaderLine.positionByWindowResize = false;
  370. if (key.node_id == key2.node_id) {
  371. const line = new LeaderLine(endDom, value, {
  372. color: "#7484AB",
  373. size: 2,
  374. path: "straight",
  375. startSocket: "right",
  376. endSocket: "left",
  377. y: 50,
  378. startPlug: "disc",
  379. endPlug: "arrow1",
  380. });
  381. leaderLines3.value.push(line);
  382. }
  383. }
  384. }
  385. for (let [key, value] of domListMiddle3.value) {
  386. //右侧子组件
  387. for (const [key2, value2] of domListRightChild3.value) {
  388. const endDom = value2;
  389. if (key.node_id == key2.node_id) {
  390. const line2 = new LeaderLine(value, endDom, {
  391. color: "#7484AB",
  392. size: 2,
  393. path: "straight",
  394. startSocket: "right",
  395. endSocket: "left",
  396. startPlug: "disc",
  397. endPlug: "arrow1",
  398. });
  399. leaderLines3.value.push(line2);
  400. }
  401. }
  402. }
  403. hiddenLine();
  404. };
  405. //滚动时重定位线条
  406. const newPositionLine = () => {
  407. document.getElementById("treedom3").addEventListener(
  408. "scroll",
  409. AnimEvent.add(() => {
  410. if(!leaderLines3.value) return;
  411. leaderLines3.value.forEach((line) => {
  412. if (line) {
  413. hiddenLine();
  414. line.position();
  415. line.positionByWindowResize = false;
  416. }
  417. });
  418. //中间展示图片的
  419. }),
  420. false
  421. );
  422. document.getElementById("treedom3").addEventListener(
  423. "resize",
  424. AnimEvent.add(function () {
  425. if(!diffline) return;
  426. diffline.forEach((line) => {
  427. hiddenLine();
  428. line.position();
  429. line.positionByWindowResize = false;
  430. });
  431. }),
  432. false
  433. );
  434. };
  435. //弹窗打开后使得线条在指定区域中
  436. const hiddenLine = () => {
  437. const elmWrapper = document.getElementById("wrapper");
  438. if(!elmWrapper) return;
  439. // 移动 line
  440. document.body.querySelectorAll("body .leader-line").forEach((node) => {
  441. elmWrapper.appendChild(node);
  442. });
  443. elmWrapper.style.transform = "none";
  444. var rectWrapper = elmWrapper.getBoundingClientRect();
  445. // Move to the origin of coordinates as the document
  446. elmWrapper.style.transform = `translate(${
  447. (rectWrapper.left + window.scrollY) * -1
  448. }px, ${(rectWrapper.top + window.scrollX) * -1}px)`;
  449. };
  450. const setLine = () => {
  451. setTimeout(() => {
  452. setLeaderline();
  453. newPositionLine();
  454. }, 500);
  455. };
  456. const removeLine3 = () => {
  457. leaderLines3.value = [];
  458. const elmWrapper = document.getElementById("wrapper");
  459. if (elmWrapper) {
  460. document.body.querySelectorAll("#wrapper .leader-line").forEach((node) => {
  461. elmWrapper.removeChild(node);
  462. });
  463. }
  464. };
  465. let scdIdValue = "";
  466. onMounted(() => {
  467. scdIdValue = route.query.id;
  468. getNetworkInfo3();
  469. //不加条件切换下方tab时会出现bug
  470. nextTick(() => {
  471. setLine();
  472. });
  473. });
  474. watch(
  475. () => props.OpensclTrue,
  476. (newValue) => {
  477. if (newValue) {
  478. domListMiddle3.value.clear();
  479. domListLeftChild3.value.clear();
  480. domListRightChild3.value.clear();
  481. leaderLines3.value = [];
  482. }
  483. nextTick(() => {
  484. removeLine3();
  485. });
  486. }
  487. );
  488. </script>
  489. <style lang="scss" scoped>
  490. @mixin img-size {
  491. width: 48px;
  492. height: 48px;
  493. }
  494. @mixin left-and-right {
  495. display: flex;
  496. flex-direction: column;
  497. }
  498. .main-cont {
  499. display: flex;
  500. justify-content: space-evenly;
  501. margin-top: 60px;
  502. overflow-y: auto;
  503. }
  504. .leader-line {
  505. z-index: 3000;
  506. }
  507. .main-left {
  508. display: flex;
  509. @include left-and-right;
  510. }
  511. .main-middle {
  512. box-sizing: border-box;
  513. border: 2px dashed #98a8ff;
  514. background: #edf3ff;
  515. margin: 0 60px;
  516. img {
  517. @include img-size;
  518. }
  519. .middle-item {
  520. @include left-and-right;
  521. align-items: center;
  522. cursor: pointer;
  523. }
  524. .cont-title {
  525. display: flex;
  526. align-items: center;
  527. margin: 12px 14px 5px 14px;
  528. border-bottom: 1px solid #a3ade0;
  529. }
  530. .middle-title {
  531. color: #ffcb11;
  532. margin-left: 8px;
  533. }
  534. }
  535. .main-right,
  536. .main-left {
  537. display: flex;
  538. @include left-and-right;
  539. .img-item {
  540. @include img-size;
  541. }
  542. }
  543. .conts {
  544. @include left-and-right;
  545. margin-bottom: 24px;
  546. border: 2px dashed #98a8ff;
  547. cursor: pointer;
  548. background: #f7f8fb;
  549. padding: 12px;
  550. .cont-title {
  551. display: flex;
  552. align-items: center;
  553. margin: 12px 14px 5px 14px;
  554. padding: 12px;
  555. border-bottom: 1px solid #a3ade0;
  556. }
  557. .cont-item {
  558. color: #1a2447;
  559. margin-left: 6px;
  560. vertical-align: middle;
  561. }
  562. .ied-desc {
  563. color: #255ce7;
  564. }
  565. .ied-desc-title {
  566. color: #134bea;
  567. }
  568. .ied-desc-child-title {
  569. color: #5182ff;
  570. margin-left: 14px;
  571. display: block;
  572. }
  573. }
  574. .midle-cont {
  575. display: flex;
  576. border: 1px solid #7484ab;
  577. align-items: center;
  578. width: 94%;
  579. margin-bottom: 8px;
  580. color: #1a2447;
  581. }
  582. .ied-desc-child,
  583. .midlestyle {
  584. @include left-and-right;
  585. flex-direction: row;
  586. justify-content: center;
  587. align-items: center;
  588. border-radius: 2px;
  589. padding: 5px;
  590. color: #1a2447;
  591. flex: 1;
  592. .img-item-middle {
  593. width: 22px;
  594. height: 22px;
  595. }
  596. .middle-cont {
  597. display: flex;
  598. flex-direction: column;
  599. align-items: center;
  600. width: 90%;
  601. }
  602. }
  603. .del-middle {
  604. color: #e50505;
  605. }
  606. .upt-middle {
  607. color: #ffa011;
  608. }
  609. .omit {
  610. font-weight: bold;
  611. letter-spacing: 8px;
  612. font-size: 15px;
  613. }
  614. #wrapper {
  615. width: 0;
  616. height: 0;
  617. position: relative; /* Origin of coordinates for lines, and scrolled content (i.e. not `absolute`) */
  618. }
  619. .text-midle {
  620. text-align: center;
  621. width: 94%;
  622. border: 1px solid #7484ab;
  623. margin-bottom: 8px;
  624. border-radius: 2px;
  625. padding: 5px;
  626. }
  627. </style>