irpas技术客

需求:vue+svg实现连线功能_小南瓜地_vue连线

网络投稿 7693

问题描述:如标题所示

参考了svg.js和jquery实现连线功能

需求: 1.右侧两个方框可以合成一个bond,点击bond之后又可以恢复。 2.一对一连接 3.有回显功能

package.json @svgdotjs/svg.js: “^3.1.1”

解决方法: svg_style.css

.draw-container { position: relative; margin-top: 50px; } .data-list { position: absolute; } .question-list { left: 8%; } .answer-list { right: 10%; } .question-list li, .answer-list li { width: 113px; background: #FFFFFF; border-radius: 7px; margin-bottom: 16px; line-height: 56px; text-align: center; box-shadow: 0px 2px 9px 0px rgba(0, 0, 0, 0.14); } .question-list li { border-left: 2px solid #F57474; cursor: crosshair; } .answer-list li { border-left: 2px solid #4F90F0; position: relative; cursor: pointer; } .hover-g { cursor: pointer; opacity: 1; stroke-width: 4; } .bondGray, .bondBlue { width: 116px; height: 56px; box-shadow: 0px 2px 9px 0px rgba(0, 0, 0, 0.14); border-radius: 7px; display: flex; justify-content: center; align-items: center; font-size: 16px; color: #ffffff; margin-bottom: 16px; position: relative; cursor: pointer; } .bondGray { background: #c6c6c6 !important; border-left: 2px solid #c6c6c6 !important; } .bondBlue { background: #4F90F0 !important; } .infoListStyle { /*width: 120px;*/ height: 20px; font-size: 16px; line-height: 20px; text-align: center; border-radius: 2px; position: absolute; left: 113px; top: 5px; cursor: default; margin-right: 10px; display: flex; pointer-events: none; } .netcardStyle { width: 90px; color: #659EF3; background: #D6E6FF; top: 5px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; margin-right: 5px; } .speedStyle { max-width: 100px; height: 20px; font-size: 16px; line-height: 20px; text-align: left; color: #333; margin-right: 10px; cursor: default; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .bondGray > div:last-child { top: 30px } .bondBlue > div:last-child { top: 30px }

link.vue

<template> <div class="xx_info"> <div class="link_box container"> <div class="link_warp"> <div class="draw-container" id="draw" ref="drawDom"> <ul class="left_box question-list data-list"> <li v-for="(item, index) in netList" :key="index" :data-question="item.name" :data-answer="item.value" class="question-li" ref="leftBox" style="user-select: none" > {{ item.name }} </li> </ul> <ul class="right_box answer-list data-list" ref="dataListBox"> <li v-for="(item, index) in dataList" :key="index" :style="item.status == 'No' ? 'border-left:2px solid #C6C6C6' : ''" :data-answer="item.name" class="answer-li" ref="rightBox" draggable="true" @dragstart.stop="startDrag($event, item)" @dragover.stop="overDrop($event)" @drop.stop="endDrop($event, index, item)" > {{ item.name }} </li> </ul> </div> <ul class="info"> <li> <p class="color_red"></p> <p class="title">xx</p> </li> <li> <p class="color_blue"></p> <p class="title">xx(已通)</p> </li> <li> <p class="color_gray"></p> <p class="title">xx未通)</p> </li> </ul> </div> </div> </div> </template> <script> import {SVG} from "@svgdotjs/svg.js"; export default { name: "index", data() { return { // 左侧数据 netList: [ { name: "1" }, { name: "2" }, { name: "3" }, { name: "4" }, { name: "5" } ], // 右侧数据 dataList: [], //svg draw: null, // 保存数据 lineArr: [], currentInfo: {}, //bond 第一个数据element startLi: null, //bond 第一个数据 startData: null, newArr: [], scrollTop: 0 }; }, methods: { /** * 连线操作 */ // 合并开始 startDrag(e, val) { this.startLi = e.target; this.startData = val; }, // 合并中 overDrop(e) { e.preventDefault(); }, // 合并结束 endDrop(ev, index, val) { if (ev.target != this.startLi) { /** * 合并 */ let node = document.createElement("li"); // li标签的值value node.innerHTML = "bond"; // li标签 className node.classList.add("answer-li"); // li标签className if (val.status == "No" && this.startData.status == "No") { node.classList.add("bondGray"); } else { node.classList.add("bondBlue"); } // li标签属性 node.setAttribute("data-answer", `${this.startLi.innerText + "," + val.name}`); // 右侧数据和speed let child = null, self = this, speed = null, infoList = null; for (let i = 0; i < 2; i++) { // 创建div标签 (bond后面的box) infoList = document.createElement("div"); infoList.classList.add("infoListStyle"); // 创建 div标签(bond后面的box的数据) child = document.createElement("div"); // 数据的className child.classList.add("netcardStyle"); // 创建 div标签(bond后面的box的speed) speed = document.createElement("div"); // 速率的className speed.classList.add("speedStyle"); if (i == 0) { // 元素 child.innerHTML = self.startLi.innerText; // 标题 child.title = self.startLi.innerText; // 速率的元素 speed.innerHTML = self.startData.speed; // 速率的标题 speed.title = self.startData.speed; } else { child.innerHTML = val.name; speed.innerHTML = val.speed; } // bond后面的box添加子元素 infoList.appendChild(child); infoList.appendChild(speed); // bond添加子元素 node.appendChild(infoList); } // 右侧列表添加bond this.$refs.dataListBox.appendChild(node); // 移除 拖曳的元素 ev.target.parentNode.removeChild(this.startLi); // 移除 被合并的元素 ev.target.parentNode.removeChild(ev.target); // 清除左侧所选数据和lineArr数据 let value = node.getAttribute("data-answer"); this.clearLine(value); this.bindBtnEvent(); this.itemForEach(true); /** * 解绑 */ let startEle = this.startLi; node.onclick = function () { // 移除bond self.$refs.dataListBox.removeChild(node); // 添加 被拖拽的元素 self.$refs.dataListBox.appendChild(startEle); // 添加 被解绑的元素 self.$refs.dataListBox.appendChild(ev.target); // 清除左侧所选数据和lineArr数据 let value = node.getAttribute("data-answer"); self.clearLine(value); self.bindBtnEvent(); self.itemForEach(true); }; ev.preventDefault(); } }, // 初始化 async init() { this.draw = SVG() .addTo("#draw") .size("100%", "100%"); // 获取右侧数据 await this.getDataList(); // 左侧数据 this.createList(this.netList); // 绑定父亲事件事件 this.bindParentsEvent(); // 绑定按钮事件 this.bindBtnEvent(); }, // 起始点数据 createList(data) { data.forEach(element => { // 定义一个对象 let obj = {}; //左侧起始点判断为元素的名称 obj.beginValue = element.name; // 创建线条 obj.line = this.createLine(); // 保存左侧数据 this.lineArr.push(obj); }); }, // 创建线条 createLine() { let self = this, line = self.draw.line(); // 左侧方块 line.marker("start", 2, 2, function (add) { add.attr({orient: 0}); add.rect(2, 2).attr({fill: "white", stroke: "#F57474"}); }); // 右侧方块 line.marker("end", 2, 2, function (add) { add.attr({orient: 0}); add.rect(2, 2).attr({fill: "white", stroke: "#4F90F0", strokeWidth: 1}); }); // 连线的样式 line.stroke({ color: "#333", width: 2, opacity: 0.6, linecap: "round" }); line.hide(); //点击 删除线 line.click(function () { let current = self.lineArr.find(el => { return el.line == this; }); // 移除开始点的className current.beginElement.classList.remove("selected"); // 移除结束点的className current.endElement.classList.remove("selected"); // 开始点移除所选的值 current.beginElement.setAttribute("data-selected", ""); current.endValue = ""; current.endElement = ""; current.end = ""; this.hide(); }); // 鼠标经过线 line.mouseover(function () { let current = self.lineArr.find(el => { return el.line == this; }); if (current.endValue) { //线条会放大 this.addClass("hover-g"); } }); // 鼠标离开线 line.mouseout(function () { //线条会恢复 this.removeClass("hover-g"); }); return line; }, // 连线开始-连线结束 bindBtnEvent() { let self = this, node = document.getElementById("draw"), parentPosition = this.offset(node); //获取offset // 左侧数据 this.$refs.leftBox.forEach(li => { // 鼠标离开 li.onmousedown = function () { let current = self.lineArr.find(el => { return el.beginValue == li.getAttribute("data-question"); }); // 开始坐标为空 current.begin = {}; // 开始元素为点击的li current.beginElement = this; // 开始x坐标 current.begin.y = self.offset(li).top - parentPosition.top + 25; // 开始y坐标 current.begin.x = self.offset(li).left - parentPosition.left + 130; // 线显示 current.line.show(); // 线颜色 current.line.stroke({ color: "#333" }); // 画线 current.line.plot(current.begin.x, current.begin.y, current.begin.x, current.begin.y); current.end = {}; /* 如果存在结束位置,删除 */ if (current.endElement) { current.endElement.classList.remove("selected"); li.classList.remove("selected"); li.setAttribute('data-answer', ''); } current.endElement = ""; current.endValue = ""; self.currentInfo = current; }; }); // 右侧数据 let allLis = this.$refs.dataListBox.getElementsByTagName("li"); if (allLis) { Array.prototype.slice.call(allLis).forEach(li => { li.onmouseup = function () { let current = self.lineArr.find(el => { return el.beginValue == self.currentInfo.beginValue; }); if (current) { // 结束y坐标 current.end.y = self.offset(li).top - parentPosition.top + 25; // 结束x坐标 current.end.x = self.offset(li).left - parentPosition.left - 20; // 结束元素为点击的li current.endElement = this; //结束值为所选的右侧数据 current.endValue = li.getAttribute("data-answer"); // 画线 current.line.plot(current.begin.x, current.begin.y, current.end.x, current.end.y); // 开始元素添加className current.beginElement.classList.add("selected"); // 开始元素添加所选的值 current.beginElement.setAttribute("data-selected", current.endValue); // 元素添加className li.classList.add("selected"); // 清空数据 self.currentInfo = {}; // dat-selected值与data-answer值一致 self.$refs.leftBox.forEach(left => { if (left.getAttribute("data-selected") == li.getAttribute("data-answer")) { left.setAttribute("data-answer", `${left.getAttribute("data-selected")}`); } }); } }; }); } }, // 获取offset offset(element) { // 滚动的距离 let offest = { top: 0, left: 0 }; let _position = null; getOffset(element, true); return offest; // 递归获取 offset, 可以考虑使用 getBoundingClientRect function getOffset(node, init) { // 非Element 终止递归 if (node && node.nodeType !== 1) { return; } _position = window.getComputedStyle(node)["position"]; // position=static: 继续递归父节点 if (typeof init === "undefined" && _position === "static") { getOffset(node.parentNode); return; } offest.top = node.offsetTop + offest.top - node.scrollTop; offest.left = node.offsetLeft + offest.left - node.scrollLeft; // position = fixed: 获取值后退出递归 if (_position === "fixed") { return; } getOffset(node.parentNode); } }, // 绑定父亲事件 bindParentsEvent() { // 如果结束点不在右侧数据,线条消失 document.addEventListener("mouseup", e => { if (e.target.className && typeof e.target.className != "object") { let name = e.target.className.split(" "); if (name[0] != "answer-li" && this.currentInfo.line) { this.currentInfo.line.hide(); } } }); // draw 组件鼠标移动事件,添加线条 let self = this, btn = document.getElementById("draw"), parentPosition = this.offset(btn); btn.onmousemove = function (e) { if (Object.keys(self.currentInfo).length != 0) { let end = {}; end.x = self.getMousePos(event).x - parentPosition.left; end.y = self.getMousePos(event).y - (parentPosition.top - self.scrollTop); self.currentInfo.line.plot(self.currentInfo.begin.x, self.currentInfo.begin.y, end.x, end.y); } e.stopPropagation(); // 阻止事件冒泡 }; }, // 获取鼠标坐标 getMousePos(event) { var e = event || window.event; var scrollX = document.documentElement.scrollLeft || document.body.scrollLeft; var scrollY = document.documentElement.scrollTop || document.body.scrollTop; var x = e.pageX || e.clientX + scrollX; var y = e.pageY || e.clientY + scrollY; return { x: x, y: y }; }, // 默认值 itemForEach(resize) { let self = this, node = document.getElementById("draw"), parentPosition = this.offset(node); //获取offse if (this.netList.length && this.dataList.length) { // 获取所有li标签 let eles = document.getElementsByTagName("li"); // 移除calssName // Array.prototype.slice.call(eles).forEach(el => { // el.classList.remove("selected"); // }); // 去重 this.$refs.leftBox.forEach(li => { if (this.newArr.indexOf(li.getAttribute("data-answer")) === -1) { this.newArr.push(li.getAttribute("data-answer")); } }); // 浏览器缩放无需调用默认数据 if (!resize) { // 生成bond-解开bond this.bondList(this.newArr); } // 连线 this.$refs.leftBox.forEach(leftLi => { let obj = {}, beginValue = leftLi.getAttribute("data-question"), endValue = leftLi.getAttribute("data-answer"); obj = self.lineArr.find(el => el.beginValue == beginValue); // 开始元素为li obj.beginElement = leftLi; // 开始为空坐标 obj.begin = {}; // 开始的y坐标 obj.begin.y = self.offset(leftLi).top - parentPosition.top + 25; // 开始x坐标 obj.begin.x = self.offset(leftLi).left - parentPosition.left + 130; // 左侧的元素值 leftLi.setAttribute("data-selected", `${endValue}`); // bond存在 if (endValue.indexOf(",") != -1) { //如果存在结束值 if (endValue) { this.$refs.rightBox.forEach(rightLi => { // 右侧数据的值和结束值相等 if ( rightLi.innerText.trim() == endValue.split(",")[0] || rightLi.innerText.trim() == endValue.split(",")[1] ) { let allLis = this.$refs.dataListBox.getElementsByTagName("li"); Array.prototype.slice.call(allLis).forEach(li => { //li 为标签 if (li.getAttribute("data-answer") == endValue) { // 结束为空坐标 obj.end = {}; // 结束的y坐标 obj.end.y = self.offset(li).top - parentPosition.top + 25; // 结束的x坐标 obj.end.x = self.offset(li).left - parentPosition.left - 20; // 结束元素为bond obj.endElement = li; // 结束值为data-answer值 obj.endValue = endValue; // 线的颜色 obj.line.stroke({ color: "#333" }); // 画线 obj.line.plot(obj.begin.x, obj.begin.y, obj.end.x, obj.end.y); // 显示线条 obj.line.show(); // 左侧数据添加className leftLi.classList.add("selected"); // 右侧数据添加className li.classList.add("selected"); } }); } }); } } // 没有bond存在 if (endValue.indexOf(",") == -1) { //如果存在结束值 if (endValue) { this.$refs.rightBox.forEach(rightLi => { // 右侧数据的值和结束值想等 if (rightLi.innerText.trim() == endValue) { // 结束为空坐标 obj.end = {}; //结束的y坐标 obj.end.y = self.offset(rightLi).top - parentPosition.top + 25; //结束的x坐标 obj.end.x = self.offset(rightLi).left - parentPosition.left - 20; // 结束元素为li obj.endElement = rightLi; // 结束值为data-answer值 obj.endValue = endValue; // 线的颜色 obj.line.stroke({ color: "#333" }); // 画线 obj.line.plot(obj.begin.x, obj.begin.y, obj.end.x, obj.end.y); // 显示线条 obj.line.show(); // 左侧数据添加className leftLi.classList.add("selected"); // 右侧数据添加className rightLi.classList.add("selected"); } }); } } }); } }, // 数据回显-生成bond bondList(data) { data.forEach(item => { if (item.indexOf(",") != -1) { /** * 合并 */ let self = this; let node = document.createElement("li"); // li标签的值value node.innerHTML = "bond"; // li标签 className node.classList.add("answer-li"); // li标签className let datas1 = {}, datas2 = {}; this.dataList.forEach(el => { if (item.split(",")[0] == el.name) { datas1 = el; } if (item.split(",")[1] == el.name) { datas2 = el; } }); if (datas1.status == "No" && datas2.status == "No") { node.classList.add("bondGray"); } else { node.classList.add("bondBlue"); } // li标签属性 node.setAttribute("data-answer", `${item.split(",")[0] + "," + item.split(",")[1]}`); // 右侧数据和speed let child = null, speed = null, infoList = null; for (let i = 0; i < 2; i++) { // 创建div标签 (bond后面的box) infoList = document.createElement("div"); infoList.classList.add("infoListStyle"); // 创建 div标签(bond后面的box的数据) child = document.createElement("div"); // 数据的className child.classList.add("netcardStyle"); // 创建 div标签(bond后面的box的speed) speed = document.createElement("div"); // 速率的className speed.classList.add("speedStyle"); if (i == 0) { child.innerHTML = item.split(",")[0]; speed.innerHTML = datas1.speed; } else { child.innerHTML = item.split(",")[1]; speed.innerHTML = datas2.speed; } // bond后面的box添加子元素 infoList.appendChild(child); infoList.appendChild(speed); // bond添加子元素 node.appendChild(infoList); } // 右侧数据列表添加bond this.$refs.dataListBox.appendChild(node); // 清除 已经合并的元素 this.$refs.rightBox.forEach(li => { if (li.innerText == item.split(",")[0]) { this.$refs.dataListBox.removeChild(li); } if (li.innerText == item.split(",")[1]) { this.$refs.dataListBox.removeChild(li); } }); /** * 解绑 */ node.onclick = function () { // 移除bond self.$refs.dataListBox.removeChild(node); // 添加被合并的元素 self.$refs.rightBox.forEach(li => { if (li.innerText.trim() == item.split(",")[0]) { self.$refs.dataListBox.appendChild(li); } if (li.innerText.trim() == item.split(",")[1]) { self.$refs.dataListBox.appendChild(li); } }); // 清除左侧数据所选数据和lineArr数据 let value = node.getAttribute("data-answer"); self.clearLine(value); //bond或unbond线条重构 self.itemForEach(true); }; } }); }, // 解绑:清除左侧数据所选数据和lineArr数据 clearLine(value) { // 左侧数据移除数据 this.$refs.leftBox.forEach(lis => { if (lis.getAttribute("data-answer").indexOf(",") != -1) { if (lis.getAttribute("data-answer") == value) { lis.classList.remove("selected"); lis.setAttribute("data-answer", ''); } } else { if ( lis.getAttribute("data-answer") == value.split(",")[0] || lis.getAttribute("data-answer") == value.split(",")[1] ) { lis.classList.remove("selected"); lis.setAttribute("data-answer", ""); } } }); // 清除lineArr数据 this.lineArr.find(current => { if (current.endValue && current.endValue.indexOf(",") != -1) { if (current.endValue == value) { // 移除开始点的className current.beginElement.classList.remove("selected"); // 移除结束点的className current.endElement.classList.remove("selected"); // 开始点移除所选的值 current.beginElement.setAttribute("data-selected", ""); current.endValue = ""; current.endElement = ""; current.end = ""; current.line.hide(); } } else { if (current.endValue == value.split(",")[0] || current.endValue == value.split(",")[1]) { // 移除开始点的className current.beginElement.classList.remove("selected"); // 移除结束点的className current.endElement.classList.remove("selected"); // 开始点移除所选的值 current.beginElement.setAttribute("data-selected", ""); current.endValue = ""; current.endElement = ""; current.end = ""; current.line.hide(); } } }); }, /** * 接口 */ // 获取右侧数据 async getDataList() { try { // 模拟测试数据 // this.dataList.splice(0); // setTimeout(() => { // this.dataList.push( // { // name: '1', // status: 'Yes', // speed: 'Unknown' // }, { // name: '2', // status: 'No', // speed: 'Unknown' // }, { // name: '3', // status: 'Yes', // speed: 'Unknown' // }, { // name: '4', // status: 'Yes', // speed: 'Unknown' // }, { // name: '5', // status: 'Yes', // speed: 'Unknown' // }, { // name: '6', // status: 'Yes', // speed: 'Unknown' // }, { // name: '7', // status: 'No', // speed: 'Unknown' // }, { // name: '8', // status: 'Yes', // speed: 'Unknown' // }, { // name: '9', // status: 'No', // speed: 'Unknown' // }, { // name: '10', // status: 'Yes', // speed: 'Unknown' // } // ); // const wHeight = document.getElementsByClassName('link_warp')[0]; // wHeight.style.height = `${this.dataList.length * 60 + this.dataList.length * 20}px`; // }, 1000); // 真实数据 const {data} = await this.$http.get("接口"); if (data.result && Array.isArray(data.result)) { this.dataList.splice(0); data.result.forEach(el => { this.dataList.push({ name: el.name, status: el.status, speed: el.speed == "Unknown!" ? "" : el.speed }); }); // 右侧高度,svg需要一个固定高度 const wHeight = document.getElementsByClassName('link_warp')[0]; wHeight.style.height = `${this.dataList.length * 60 + this.dataList.length * 20}px`; } } catch (e) { console.log("错误", e); } }, // 回显信息 async getXXInfo() { // 模拟测试数据 // let flag = true; // if (flag) { // this.netList[0].value = '1'; // this.netList[1].value = '2'; // this.netList[2].value = '3,4'; // this.netList[3].value = '5,6'; // this.netList[4].value = '7'; // this.netList[5].value = '8'; // setTimeout(() => { // this.itemForEach(); // this.bindBtnEvent(); // }, 3000); // } //真实数据 try { const {data} = await this.$http.get("接口"); if (data.result && data.result.xx) {/** * 连线 */ this.netList[0].value = data.result.xx; this.netList[1].value = data.result.xx; this.netList[2].value = data.result.xx; this.netList[3].value = data.result.xx; this.netList[4].value = data.result.xx; setTimeout(() => { this.itemForEach(); this.bindBtnEvent(); }, 3000); } } catch (e) { console.log("错误", e); } }, }, mounted() { // 初始化svg this.init(); let that = this; // 滚动 let dom = document.getElementsByClassName('steps_content')[0]; dom.addEventListener("scroll", e => { that.scrollTop = e.target.scrollTop; }); // 浏览器缩放时 window.onresize = function () { document.documentElement.scrollTop = 0; document.body.scrollTop = 0; let resize = true; that.bindBtnEvent(); that.bindParentsEvent(); that.itemForEach(resize); }; }, created() { // 获取回显信息 this.getXXInfo(); } }; </script> <style scoped lang="stylus"> .xx_info height 100% .link_box border-top: 1px solid #F3F3F3 padding-top 30px .link_warp //height 500px display flex justify-content space-between width 60% margin 0 auto .draw-container display flex justify-content space-around flex 1 .info margin-left 40px li display flex align-items center .color_red, .color_blue, .color_gray width 10px height 10px margin-right 9px .color_blue background: #4F90F0 .color_gray background: #C6C6C6 .color_red background: #F57474 .title font-size 12px font-family MicrosoftYaHei color rgba(0, 0, 0, 0.7) line-height 16px </style>


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #vue连线