「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」。
前言
由于项目需要做到元素之间相互连线,我们选用了jsPlumb来实现。
引用 jsPlumb
框架我使用vue3来搭建,jsPlumb我使用2.x。
我们使用npm install jsplumb来安装。安装完成之后我们就进行初始化。
首先我们看一个简单例子
<template>
<div id="diagramContainer">
<div id="source" class="item">source</div>
<div id="target" class="item" style="margin-left:250px;">target</div>
</div>
</template>
<script setup>
import { onMounted, nextTick } from 'vue'
import { jsPlumb, jsPlumbInstance } from 'jsplumb'
let plumbIns
const jsPlumbInit = () => {
plumbIns.connect({
source: 'source', // 源节点
target: 'target', // 目标节点
endpoint: 'Dot', // 端点的样式,可以设置Dot、Rectangle、image、Blank
connector: ['Bezier'], // 连接线 Bezier(贝塞尔曲线) Straight(直线) Flowchart(垂直或水平线组成的连接) StateMachine
anchor: ['Left', 'Right'], // 锚点位置
endpointStyle:{ fill: "yellow", radius: 5 }
});
}
onMounted(() => {
plumbIns = jsPlumb.getInstance()
jsPlumbInit()
})
</script>
<style>
.item {
width: 150px;
height: 50px
}
#source {
border: 2px solid red;
}
#target {
border: 2px solid blue;
}
</style>
这个例子我们通过简单初始化配置,实现了元素之间的连线,下面我们进行简单的项目实践来深入学习。
jsPlumb在项目中的应用
首先我们写一个简单的页面,左边是一个菜单列表右边是一个画布容器,实现节点拖入到画布以及节点在画布里的拖拽。
<div class="main">
<ul class="main-left">
<li v-for="item in menuList" :key="item.id" :draggable="true" @dragend="handleDragend($event, item)">{{item.label}}</li>
</ul>
<div class="main-right" @dragover.prevent ref="efContainerRef" id="efContainer">
<div v-for="(item, index) in nodeList" :key="item.nodeId" class="node-info flow-node-drag"
:style="item.nodeContainerStyle" :id="item.nodeId" :ref="el => nodeRef[index] = el"
@mouseup="handleMouseup($event, item)">
<div class="node-info-label">{{item.label}}</div>
</div>
</div>
</div>
-
对于左边的菜单列表,我们设置
draggable属性,让其可以进行拖拽;其次,我们添加dragend方法,在它拖拽结束的时候,我们记录下它的位置等信息,并将这些节点放在nodeList中收集起来。 -
对于画布,我们需要设置一个
id和你配置的jsPlumb的容器id是一样的;然后我们需要设置mouseup方法,在画布里面拖拽节点时,也要重新去设置他的位置,最后通过样式来展示节点 所在的位置。
const handleDragend = (ev, node) => {
// 拖拽进来相对于地址栏偏移量
const evClientX = ev.clientX
const evClientY = ev.clientY
const efContainer = efContainerRef.value
const containerRect = efContainer.getBoundingClientRect()
let left = evClientX - efContainer.offsetLeft
let top = evClientY - efContainer.offsetTop
// 居中
left -= 51
top -= 19
const nodeId = `${node.id}_${Date.now()}`
const nodeInfo = {
label: node.label,
id: nodeId,
nodeId,
nodeContainerStyle: {
left: left + 'px',
top: top + 'px'
}
}
nodeList.value.push(nodeInfo)
nextTick(() => {
plumbIns.makeSource(nodeId, deff.jsplumbSourceOptions)
plumbIns.makeTarget(nodeId, deff.jsplumbSourceOptions)
plumbIns.draggable(nodeId)
})
}
const handleMouseup = (ev, data) => { // 在图表中拖拽节点时,设置他的新的位置
nodeRef.value.forEach(node => {
if (node.id === data.nodeId) {
data.nodeContainerStyle.left = node.style.left
data.nodeContainerStyle.top = node.style.top
}
})
}
在这里面我没有去设置拖拽的范围,大家可以自己添加判断去控制在画布内拖拽。
下面我们实现一下元素间的连线~
我们添加一个端点设置class要和配置里面的相同,然后从端点按住鼠标可以进行动态连线~
好了到这里,我们了解了jsPlumb简单用法,实现了简单的节点元素连线功能。
整体实现代码如下:
<template>
<div class="main">
<ul class="main-left">
<li v-for="item in menuList" :key="item.id" :draggable="true" @dragend="handleDragend($event, item)">{{item.label}}</li>
</ul>
<div class="main-right" @dragover.prevent ref="efContainerRef" id="efContainer">
<div v-for="(item, index) in nodeList" :key="item.nodeId" class="node-info flow-node-drag"
:style="item.nodeContainerStyle" :id="item.nodeId" :ref="el => nodeRef[index] = el"
@mouseup="handleMouseup($event, item)">
<div class="node-info-label">{{item.label}}</div>
<span class="node-drag"></span>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, nextTick, ref } from 'vue'
import { jsPlumb, jsPlumbInstance } from 'jsplumb'
const menuList = ref([ { label: 'abc', id: '1' }, { label: 'bcd', id: '2' }, { label: 'cde', id: '3' }, { label: 'def', id: '4' }])
const deff = {
jsplumbSetting: {
// 动态锚点、位置自适应
Anchors: ['Right', 'Left'],
anchor: ['Right', 'Left'],
// 容器ID
Container: 'efContainer',
// 连线的样式,直线或者曲线等,可选值: StateMachine、Flowchart,Bezier、Straight
// Connector: ['Bezier', {curviness: 100}],
// Connector: ['Straight', { stub: 20, gap: 1 }],
Connector: ['Flowchart', { stub: 30, gap: 1, alwaysRespectStubs: false, midpoint: 0.5, cornerRadius: 10 }],
// Connector: ['StateMachine', {margin: 5, curviness: 10, proximityLimit: 80}],
// 鼠标不能拖动删除线
ConnectionsDetachable: false,
// 删除线的时候节点不删除
DeleteEndpointsOnDetach: false,
/**
* 连线的两端端点类型:圆形
* radius: 圆的半径,越大圆越大
*/
// Endpoint: ['Dot', { radius: 5, cssClass: 'ef-dot', hoverClass: 'ef-dot-hover' }],
/**
* 连线的两端端点类型:矩形
* height: 矩形的高
* width: 矩形的宽
*/
// Endpoint: ['Rectangle', {height: 20, width: 20, cssClass: 'ef-rectangle', hoverClass: 'ef-rectangle-hover'}],
/**
* 图像端点
*/
// Endpoint: ['Image', {src: 'https://www.easyicon.net/api/resizeApi.php?id=1181776&size=32', cssClass: 'ef-img', hoverClass: 'ef-img-hover'}],
/**
* 空白端点
*/
Endpoint: ['Blank', { Overlays: '' }],
// Endpoints: [['Dot', {radius: 5, cssClass: 'ef-dot', hoverClass: 'ef-dot-hover'}], ['Rectangle', {height: 20, width: 20, cssClass: 'ef-rectangle', hoverClass: 'ef-rectangle-hover'}]],
/**
* 连线的两端端点样式
* fill: 颜色值,如:#12aabb,为空不显示
* outlineWidth: 外边线宽度
*/
EndpointStyle: { fill: '#1879ffa1', outlineWidth: 1 },
// 是否打开jsPlumb的内部日志记录
LogEnabled: true,
/**
* 连线的样式
*/
PaintStyle: {
// 线的颜色
stroke: '#E0E3E7',
// 线的粗细,值越大线越粗
strokeWidth: 1,
// 设置外边线的颜色,默认设置透明,这样别人就看不见了,点击线的时候可以不用精确点击,参考 https://blog.csdn.net/roymno2/article/details/72717101
outlineStroke: 'transparent',
// 线外边的宽,值越大,线的点击范围越大
outlineWidth: 10,
},
DragOptions: { cursor: 'pointer', zIndex: 2000 },
ConnectionOverlays: [
['Custom', {
create() {
const el = document.createElement('div')
// el.innerHTML = '<select id=\'myDropDown\'><option value=\'foo\'>foo</option><option value=\'bar\'>bar</option></select>'
return el
},
location: 0.7,
id: 'customOverlay',
}],
],
/**
* 叠加 参考: https://www.jianshu.com/p/d9e9918fd928
*/
Overlays: [
// 箭头叠加
['Arrow', {
width: 10, // 箭头尾部的宽度
length: 8, // 从箭头的尾部到头部的距离
location: 1, // 位置,建议使用0~1之间
direction: 1, // 方向,默认值为1(表示向前),可选-1(表示向后)
foldback: 0.623, // 折回,也就是尾翼的角度,默认0.623,当为1时,为正三角
}],
// ['Diamond', { // events: { // dblclick: function (diamondOverlay, originalEvent) { // console.log('double click on diamond overlay for : ' + diamondOverlay.component) // } // } // }],
['Label', { label: '', location: 0.1, cssClass: 'aLabel', }],
],
// 绘制图的模式 svg、canvas
RenderMode: 'canvas',
// 鼠标滑过线的样式
HoverPaintStyle: { stroke: '#b0b2b5', strokeWidth: 1 },
// 滑过锚点效果
// EndpointHoverStyle: {fill: 'red'}
Scope: 'jsPlumb_DefaultScope', // 范围,具有相同scope的点才可连接
},
/**
* 连线参数
*/
jsplumbConnectOptions: {
isSource: true,
isTarget: true,
// 动态锚点、提供了4个方向 Continuous、AutoDefault
// anchor: 'Continuous',
// anchor: ['Continuous', { faces: ['left', 'right'] }],
// 设置连线上面的label样式
labelStyle: {
cssClass: 'flowLabel',
},
// overlays: [
// ['Custom', {
// create(component) {
// const el = document.createElement('div')
// el.innerHTML = '<select id=\'myDropDown\'><option value=\'foo\'>foo</option><option value=\'bar\'>bar</option></select>'
// return el
// },
// location: 0.7,
// id: 'customOverlay',
// }],
// ],
},
/**
* 源点配置参数
*/
jsplumbSourceOptions: {
// 设置可以拖拽的类名,只要鼠标移动到该类名上的DOM,就可以拖拽连线
filter: '.node-drag',
filterExclude: false,
anchor: ['Continuous', { faces: ['right'] }],
// 是否允许自己连接自己
allowLoopback: false,
maxConnections: -1,
},
// 参考 https://www.cnblogs.com/mq0036/p/7942139.html
jsplumbSourceOptions2: {
// 设置可以拖拽的类名,只要鼠标移动到该类名上的DOM,就可以拖拽连线
filter: '.node-drag',
filterExclude: false,
// anchor: 'Continuous',
// 是否允许自己连接自己
allowLoopback: true,
connector: ['Flowchart', { curviness: 50 }],
connectorStyle: {
// 线的颜色
stroke: 'red',
// 线的粗细,值越大线越粗
strokeWidth: 1,
// 设置外边线的颜色,默认设置透明,这样别人就看不见了,点击线的时候可以不用精确点击,参考 https://blog.csdn.net/roymno2/article/details/72717101
outlineStroke: 'transparent',
// 线外边的宽,值越大,线的点击范围越大
outlineWidth: 10,
},
connectorHoverStyle: { stroke: 'red', strokeWidth: 2 },
},
jsplumbTargetOptions: {
// 设置可以拖拽的类名,只要鼠标移动到该类名上的DOM,就可以拖拽连线
filter: '.node-drag',
filterExclude: false,
// 是否允许自己连接自己
anchor: ['Continuous', { faces: ['left'] }],
allowLoopback: false,
dropOptions: { hoverClass: 'ef-drop-hover' },
},
}
const nodeList = ref([])
const efContainerRef = ref()
const nodeRef = ref([])
let plumbIns
const jsPlumbInit = () => {
plumbIns.importDefaults(deff.jsplumbSetting)
}
const handleDragend = (ev, node) => {
// 拖拽进来相对于地址栏偏移量
const evClientX = ev.clientX
const evClientY = ev.clientY
const efContainer = efContainerRef.value
const containerRect = efContainer.getBoundingClientRect()
let left = evClientX - efContainer.offsetLeft
let top = evClientY - efContainer.offsetTop
// 居中
left -= 51
top -= 19
const nodeId = `${node.id}_${Date.now()}`
const nodeInfo = {
label: node.label,
id: nodeId,
nodeId,
nodeContainerStyle: {
left: left + 'px',
top: top + 'px'
}
}
nodeList.value.push(nodeInfo)
nextTick(() => {
plumbIns.makeSource(nodeId, deff.jsplumbSourceOptions)
plumbIns.makeTarget(nodeId, deff.jsplumbTargetOptions)
plumbIns.draggable(nodeId)
})
}
const handleMouseup = (ev, data) => { // 在图表中拖拽节点时,设置他的新的位置
nodeRef.value.forEach(node => {
if (node.id === data.nodeId) {
data.nodeContainerStyle.left = node.style.left
data.nodeContainerStyle.top = node.style.top
}
})
}
onMounted(() => {
plumbIns = jsPlumb.getInstance()
jsPlumbInit()
})
</script>
<style>
body {
margin: 0;
}
.item {
width: 150px;
height: 50px
}
#source {
border: 2px solid red;
}
#target {
border: 2px solid blue;
}
.main {
display: flex;
}
ul {
list-style: none;
padding-left: 0;
width: 120px;
background: #eee;
text-align: center;
}
ul > li {
height: 40px;
line-height: 40px;
}
.main-right {
border: 1px solid #ccc;
flex: 1;
margin-left: 15px;
position: relative;
background: #f4f4f4;
}
.node-info {
position: absolute;
}
.node-info-label {
width: 100px;
height: 36px;
line-height: 36px;
text-align: center;
border: 1px solid #e5e7eb;
background: #fff;
border-radius: 4px;
}
.node-info-label:hover {
cursor: pointer;
background: #f4eded;
}
.node-info-label:hover + .node-drag {
/* background: red; */
display: inline-block;
}
.node-drag {
display: none;
width: 6px;
height: 6px;
border-radius: 6px;
border: 1px solid #ccc;
position: absolute;
right: -7px;
top: 14px;
}
.node-drag:hover {
display: inline-block;
}
</style>