参考了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>