virtualRelation.vue 14 KB

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