业务逻辑,类似与navcat中的视图可视化的操作 拖动列表 生成表格并支持连线
展示效果
<el-row>
<el-col :span="4">
<draggable
v-model="tableData"
@end="onEnd"
:group="{ name: 'test', pull: 'clone', put: 'false' }"
:sort="false"
@start="startDra"
>
<div v-for="(element, index) in tableData" :key="index">
{{ element.name }}
</div>
</draggable></el-col
>
<el-col :span="12">
<draggable
draggable
style="height: 100vh; position: relative"
v-model="tableData2"
:group="{ name: 'test', pull: '' }"
@add="addList"
id="container"
>
<div v-for="(element, index) in tableData2" :key="index">
<div
class="chatBox fx-d-c"
v-drag
:id="element.name + '_' + element.node"
>
<div class="topBar fx-a-c">
<div class="flex1 fx-a-c drayView" style="width: 100%">
<span class="chatName">{{ element.name }}</span>
</div>
<i
class="el-icon-edit"
style="
margin-right: 10px;
height: 55px;
width: 30px;
line-height: 55px;
cursor: pointer;
"
@click="closeChatBox($event)"
></i>
</div>
<ul>
<li
v-for="(item, index) in element.list"
:key="'left' + index"
:id="item.id"
name="source"
class="lines"
>
{{ item.name }}
</li>
</ul>
</div>
</div>
</draggable></el-col
>
</el-row>
vue 自定义的可拖拽组件
import Vue from 'vue';
Vue.directive('drag', function (el, binding, vnode, oldVnode) {
const dialogHeaderEl = el.querySelector('.drayView');
// 监听 当前绑定指令元素的鼠标点下去的事件
dialogHeaderEl.onmousedown = function (e) {
const { ox, oy } = {
ox: e.clientX - el.offsetLeft,
oy: e.clientY - el.offsetTop,
};
let fit = false;
let clear = true;
// 监听鼠标来回移动的事件
document.onmousemove = function (em) {
const { left, top } = {
left: em.clientX - ox,
top: em.clientY - oy,
};
if ((left != 0 || top != 0) && clear) {
vnode.context.clearLine();
clear = false;
}
// ;
// 将鼠标 换成小手
el.style.cursor = 'pointer';
// 和盒子的定位 改变left 和 top的值
el.style.left = left + 'px';
el.style.top = top + 'px';
};
// 监听鼠标抬起的事件
document.onmouseup = function (eu) {
document.onmousemove = null;
if (!fit) {
fit = true;
vnode.context.ConnectLine();
}
el.style.cursor = 'default';
};
};
});
数据源的准备工作
tableData: [
{
date: '2016-05-02',
name: '王涛',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
zip: 200333,
node: '1',
tag: '家',
list: [
{ id: 1, name: 123 },
{ id: 7, name: 'wt' },
],
},
{
date: '2016-05-04',
name: '王涛2',
province: '上海',
node: '2',
city: '普陀区',
address: '上海市普陀区金沙江路 1517 弄',
zip: 200333,
list: [{ id: 2, name: 23 }],
tag: '公司',
},
{
date: '2016-05-01',
name: '王涛3',
province: '上海',
node: '3',
city: '普陀区',
address: '上海市普陀区金沙江路 1519 弄',
zip: 200333,
tag: '家',
list: [{ id: 3, name: 1023 }],
},
{
date: '2016-05-03',
name: '王涛4',
province: '上海',
node: '4',
city: '普陀区',
address: '上海市普陀区金沙江路 1516 弄',
zip: 200333,
tag: '公司',
list: [{ id: 4, name: 1034 }],
},
],
readyList: [],
tableData2: [
{
date: '2016-05-02',
name: '王涛',
province: '上海',
node: '10',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
zip: 200333,
list: [
{ id: 5, name: '男' },
{ id: 6, name: '我是老六' },
],
tag: '家',
},
],
初始化方法以及一些连线的方法
showPlumb() {
this.jsPlumb = this.$jsPlumb.getInstance({
Container: 'container', // 选择器id
EndpointStyle: { radius: 0.11, fill: '#999' }, // 端点样式
PaintStyle: { stroke: '#999', strokeWidth: 2 }, // 绘画样式,默认8px线宽 #456
HoverPaintStyle: { stroke: '#994B0A', strokeWidth: 3 }, // 默认悬停样式 默认为null
ConnectionOverlays: [
// 此处可以设置所有箭头的样式
[
'Arrow',
{
// 设置参数可以参考中文文档
location: 1,
length: 12,
paintStyle: {
stroke: '#999',
fill: '#999',
},
},
],
],
Connector: ['Straight'], // 要使用的默认连接器的类型:直线,折线,曲线等
DrapOptions: { cursor: 'crosshair', zIndex: 2000 },
});
this.jsPlumb.batch(() => {
this.tableData2.forEach((element, index) => {
element.list.forEach((list, i) => {
this.initLeaf('ids' + this.tableData2[index].list[i].id, 'source');
this.initLeaf('ids' + this.tableData2[index].list[i].id, 'target');
});
});
});
this.setjsPlumb(true, true);
this.ConnectLine();
//点击连线
this.jsPlumb.bind('click', (conn, originalEvent) => {
this.LinkData.forEach((element, index) => {
if (
element.sourceId == conn.sourceId &&
element.targetId == conn.targetId
) {
this.LinkData.splice(index, 1);
}
});
this.jsPlumb.deleteConnection(conn);
});
//连线时触发
this.jsPlumb.bind('connection', (conn, originalEvent) => {
let obj = {
sourceId: conn.sourceId,
targetId: conn.targetId,
};
this.LinkData.push(obj);
this.readyList = JSON.parse(JSON.stringify(this.LinkData));
this.LinkData = this.readyList.reduce((curr, next) => {
/*判断对象中是否已经有该属性 没有的话 push 到 curr数组*/
obj[next.sourceId + next.targetId]
? ''
: (obj[next.sourceId + next.targetId] = curr.push(next));
return curr;
}, []);
});
//右键触发
this.jsPlumb.bind('contextmenu', (conn, originalEvent) => {});
},
startDra() {
this.jsPlumb.deleteEveryConnection();
this.jsPlumb.deleteEveryEndpoint();
},
initLeaf(id, type) {
const ins = this.jsPlumb;
const elem = document.getElementById(id);
if (type === 'source') {
ins.makeSource(elem, {
anchor: [1, 0.5, 0, 0], // 左 上 右 下
allowLoopback: false, //允许回连
maxConnections: -1, //最大连接数(-1表示不限制)
});
} else {
ins.makeTarget(elem, {
anchor: [0, 0.5, 0, 0],
allowLoopback: false,
maxConnections: -1,
});
}
},
//初始化连接线
ConnectLine() {
let allArr = [];
for (var i = 0; i < this.LinkData.length; i++) {
var flag = true;
for (var j = 0; j < allArr.length; j++) {
if (
this.LinkData[i].targetId == allArr[j].targetId &&
this.LinkData[i].sourceId == allArr[j].sourceId
) {
flag = false;
}
}
if (flag) {
allArr.push(this.LinkData[i]);
}
}
setTimeout(() => {
for (const item of allArr) {
this.jsPlumb.connect({
source: item.sourceId,
target: item.targetId,
});
}
}, 0);
},
完整代码
<template>
<div>
<el-row>
<el-col :span="4">
<draggable
v-model="tableData"
@end="onEnd"
:group="{ name: 'test', pull: 'clone', put: 'false' }"
:sort="false"
@start="startDra"
>
<div v-for="(element, index) in tableData" :key="index">
{{ element.name }}
</div>
</draggable></el-col
>
<el-col :span="12">
<draggable
draggable
style="height: 100vh; position: relative"
v-model="tableData2"
:group="{ name: 'test', pull: '' }"
@add="addList"
id="container"
>
<div v-for="(element, index) in tableData2" :key="index">
<div class="chatBox fx-d-c" v-drag :id="'element_' + element.node">
<div class="topBar fx-a-c">
<div class="flex1 fx-a-c drayView" style="width: 100%">
<span class="chatName">{{ element.name }}</span>
</div>
<i
class="el-icon-edit"
style="
margin-right: 10px;
height: 55px;
width: 30px;
line-height: 55px;
cursor: pointer;
"
@click="closeChatBox($event, element)"
></i>
</div>
<ul>
<li
v-for="(item, index) in element.list"
:key="'left' + index"
:id="'ids' + item.id"
name="source"
class="lines"
>
{{ item.name }}
</li>
</ul>
</div>
</div>
</draggable></el-col
>
</el-row>
</div>
</template>
<script>
import Vue from 'vue';
Vue.directive('drag', function (el, binding, vnode, oldVnode) {
const dialogHeaderEl = el.querySelector('.drayView');
// 监听 当前绑定指令元素的鼠标点下去的事件
dialogHeaderEl.onmousedown = function (e) {
const { ox, oy } = {
ox: e.clientX - el.offsetLeft,
oy: e.clientY - el.offsetTop,
};
let fit = false;
let clear = true;
// 监听鼠标来回移动的事件
document.onmousemove = function (em) {
const { left, top } = {
left: em.clientX - ox,
top: em.clientY - oy,
};
if ((left != 0 || top != 0) && clear) {
vnode.context.clearLine();
clear = false;
}
// ;
// 将鼠标 换成小手
el.style.cursor = 'pointer';
// 和盒子的定位 改变left 和 top的值
el.style.left = left + 'px';
el.style.top = top + 'px';
};
// 监听鼠标抬起的事件
document.onmouseup = function (eu) {
document.onmousemove = null;
if (!fit) {
fit = true;
vnode.context.ConnectLine();
}
el.style.cursor = 'default';
};
};
});
export default {
data() {
return {
jsPlumb: null,
//初始的id
LinkData: [],
tableData: [
{
date: '2016-05-02',
name: '王涛',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
zip: 200333,
node: '1',
tag: '家',
list: [
{ id: 1, name: 123 },
{ id: 7, name: 'wt' },
],
},
{
date: '2016-05-04',
name: '王涛2',
province: '上海',
node: '2',
city: '普陀区',
address: '上海市普陀区金沙江路 1517 弄',
zip: 200333,
list: [{ id: 2, name: 23 }],
tag: '公司',
},
{
date: '2016-05-01',
name: '王涛3',
province: '上海',
node: '3',
city: '普陀区',
address: '上海市普陀区金沙江路 1519 弄',
zip: 200333,
tag: '家',
list: [{ id: 3, name: 1023 }],
},
{
date: '2016-05-03',
name: '王涛4',
province: '上海',
node: '4',
city: '普陀区',
address: '上海市普陀区金沙江路 1516 弄',
zip: 200333,
tag: '公司',
list: [{ id: 4, name: 1034 }],
},
],
readyList: [],
tableData2: [
{
date: '2016-05-02',
name: '王涛',
province: '上海',
node: '10',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
zip: 200333,
list: [
{ id: 5, name: '男' },
{ id: 6, name: '我是老六' },
],
tag: '家',
},
],
};
},
mounted() {
this.$nextTick(() => {
this.showPlumb();
});
},
methods: {
showPlumb() {
this.jsPlumb = this.$jsPlumb.getInstance({
Container: 'container', // 选择器id
EndpointStyle: { radius: 0.11, fill: '#999' }, // 端点样式
PaintStyle: { stroke: '#999', strokeWidth: 2 }, // 绘画样式,默认8px线宽 #456
HoverPaintStyle: { stroke: '#994B0A', strokeWidth: 3 }, // 默认悬停样式 默认为null
ConnectionOverlays: [
// 此处可以设置所有箭头的样式
[
'Arrow',
{
// 设置参数可以参考中文文档
location: 1,
length: 12,
paintStyle: {
stroke: '#999',
fill: '#999',
},
},
],
],
Connector: ['Straight'], // 要使用的默认连接器的类型:直线,折线,曲线等
DrapOptions: { cursor: 'crosshair', zIndex: 2000 },
});
this.jsPlumb.batch(() => {
this.tableData2.forEach((element, index) => {
element.list.forEach((list, i) => {
this.initLeaf('ids' + this.tableData2[index].list[i].id, 'source');
this.initLeaf('ids' + this.tableData2[index].list[i].id, 'target');
});
});
});
this.setjsPlumb(true, true);
this.ConnectLine();
//点击连线
this.jsPlumb.bind('click', (conn, originalEvent) => {
this.LinkData.forEach((element, index) => {
if (
element.sourceId == conn.sourceId &&
element.targetId == conn.targetId
) {
this.LinkData.splice(index, 1);
}
});
this.jsPlumb.deleteConnection(conn);
});
//连线时触发
this.jsPlumb.bind('connection', (conn, originalEvent) => {
let obj = {
sourceId: conn.sourceId,
targetId: conn.targetId,
};
this.LinkData.push(obj);
this.readyList = JSON.parse(JSON.stringify(this.LinkData));
this.LinkData = this.readyList.reduce((curr, next) => {
/*判断对象中是否已经有该属性 没有的话 push 到 curr数组*/
obj[next.sourceId + next.targetId]
? ''
: (obj[next.sourceId + next.targetId] = curr.push(next));
return curr;
}, []);
});
//右键触发
this.jsPlumb.bind('contextmenu', (conn, originalEvent) => {});
},
startDra() {
this.jsPlumb.deleteEveryConnection();
this.jsPlumb.deleteEveryEndpoint();
},
initLeaf(id, type) {
const ins = this.jsPlumb;
const elem = document.getElementById(id);
if (type === 'source') {
ins.makeSource(elem, {
anchor: [1, 0.5, 0, 0], // 左 上 右 下
allowLoopback: false, //允许回连
maxConnections: -1, //最大连接数(-1表示不限制)
});
} else {
ins.makeTarget(elem, {
anchor: [0, 0.5, 0, 0],
allowLoopback: false,
maxConnections: -1,
});
}
},
//初始化连接线
ConnectLine() {
let allArr = [];
for (var i = 0; i < this.LinkData.length; i++) {
var flag = true;
for (var j = 0; j < allArr.length; j++) {
if (
this.LinkData[i].targetId == allArr[j].targetId &&
this.LinkData[i].sourceId == allArr[j].sourceId
) {
flag = false;
}
}
if (flag) {
allArr.push(this.LinkData[i]);
}
}
setTimeout(() => {
for (const item of allArr) {
this.jsPlumb.connect({
source: item.sourceId,
target: item.targetId,
});
}
}, 0);
},
clearLine() {
this.jsPlumb.deleteEveryConnection();
this.jsPlumb.deleteEveryEndpoint();
this.jsPlumb.setSuspendDrawing(false, true);
},
setjsPlumb(sourceFlag, targetFlag) {
const source = document.getElementsByName('source');
const target = document.getElementsByName('target');
this.jsPlumb.setSourceEnabled(source, sourceFlag);
this.jsPlumb.setTargetEnabled(target, targetFlag);
this.jsPlumb.setDraggable(source, false); // 是否支持拖拽
this.jsPlumb.setDraggable(target, false); // 是否支持拖拽
},
closeChatBox(e, row) {
let node = e.currentTarget.parentNode.parentNode;
let b = e.currentTarget.parentNode.parentNode.getElementsByTagName('li');
//深拷贝防止数组变化
const allLines = [...this.jsPlumb.getConnections()];
const LinkData = [...this.LinkData];
for (const iterator of b) {
allLines.forEach((element, index) => {
if (
iterator.id == element.sourceId ||
iterator.id == element.targetId
)
this.jsPlumb.deleteConnection(element);
});
this.LinkData = LinkData.filter(item => {
return iterator.id !== item.sourceId && iterator.id !== item.targetId;
});
}
node.remove();
row.isDel = 1;
this.$forceUpdate();
this.ConnectLine();
},
onEnd(e) {
// console.log(e, '11111111');
},
addList(e) {
e.item._underlying_vm_.list.forEach((list, i) => {
this.initLeaf('ids' + list.id, 'source');
this.initLeaf('ids' + list.id, 'target');
});
this.ConnectLine();
},
},
};
</script>
<style lang="less" scoped>
.chatBox {
//不能用fixed 不然会出现错乱
position: absolute;
width: 150px;
height: 300px;
right: 130px;
top: 384px;
// z-index: 1;
background: white;
box-shadow: 0px 1px 6px 0px rgba(0, 21, 41, 0.12);
.topBar {
width: 150px;
height: 56px;
display: flex;
line-height: 56px;
box-shadow: 0px 1px 6px 0px rgba(0, 21, 41, 0.12);
z-index: 10;
cursor: move;
}
.chatAvatar {
width: 36px;
height: 36px;
overflow: hidden;
margin-left: 10px;
object-fit: cover;
}
.chatName {
width: 50px;
margin-left: 12px;
font-size: 16px;
font-weight: 600;
color: #333333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chatContent {
padding: 8px 0;
overflow-y: auto;
overflow-x: hidden;
}
}
.flex1 {
display: flex;
}
.el-icon-edit {
z-index: 100;
}
</style>