
vue实现连线效果
vue+jsplumb实现连线效果
·
前端实现连线效果
效果展示
父组件内容
<template>
<div>
//使用jsplumb的容器
<div class="content step_2-content" id="efContainer">
<ul>
//渲染点位必须要绑定id用于连线
//左侧盒子
<template v-for="node in dataContent.nodeList">
<flowNode :node="node" v-if="node.data === '1'" :id="node.domId" :ids="node.id" nodeTypes="left"
:value="node.value" />
</template>
</ul>
<ul>
// 右侧盒子
<template v-for="node in dataContent.nodeList">
<flowNode :node="node" v-if="node.data === '2'" :id="node.domId" :ids="node.id" nodeTypes="right"
:value="node.value" />
</template>
</ul>
</div>
<!-- 连线的数据 -->
{{ dataContent.lineList}}
</div>
</template>
<script setup lang="ts">
import 'jsplumb'
import lodash from 'lodash'
import flowNode from './component/node.vue'
import { ref, toRefs, nextTick, onMounted } from 'vue'
import { jsplumbSetting, jsplumbConnectOptions, jsplumbSourceOptions, jsplumbTargetOptions } from './mixins.js'
import { accessauthorization } from '@/store/index'
type TypeDataArr = {
name: string
nodeList: any[]
lineList: any[]
}
type Typeleft={
id:string
name:string
}
type TypeData = {
drawingContent: any
dataContent: {
nodeList: any[]
lineList: any[]
}
DataArr: TypeDataArr
leftData: Typeleft[]
rightData: Typeleft[]
}
const AccessAuthorization = accessauthorization()
const data = ref<TypeData>({
// 连线内容数据
drawingContent: {},
// 生成连接块的内容
dataContent: {
// 生成连接的盒子
nodeList: [],
// 生成连接的线
lineList: []
},
// 模拟左侧放假数据
leftData: [{
id: '10',
name: '国庆放假',
}, {
id: '11',
name: '元旦放假',
}, {
id: '12',
name: '除夕放假',
}, {
id: '13',
name: '劳动放假',
}],
// 模拟右侧城市数据
rightData: [{
id: '2',
name: '成都',
}, {
id: '3',
name: '武汉',
}, {
id: '4',
name: '青岛',
}, {
id: '5',
name: '重庆',
}, {
id: '6',
name: '西藏',
}, {
id: '7',
name: '上海',
}, {
id: '8',
name: '北京',
}],
// 处理需要展示的数据集
DataArr: {
name: 'processB',
nodeList: [],
lineList: []
},
})
const { drawingContent, DataArr, dataContent, leftData, rightData } = toRefs(data.value)
// 加载流程图
const dataReload = (data: TypeDataArr) => {
dataContent.value.nodeList = []
dataContent.value.lineList = []
nextTick(() => {
data = lodash.cloneDeep(data)
dataContent.value = data
nextTick(() => {
drawingContent.value = jsPlumb.getInstance()
nextTick(() => {
jsPlumbInit()
});
});
});
}
const jsPlumbInit = () => {
drawingContent.value.ready(() => {
// 导入默认配置
drawingContent.value.importDefaults(jsplumbSetting)
// 会使整个jsPlumb立即重绘。
drawingContent.value.setSuspendDrawing(false, true);
// 初始化节点
loadEasyFlow()
// 点击线
// drawingContent.value.bind('click', (conn) => {
// activeElement.value.type = 'line'
// activeElement.value.sourceId = conn.sourceId
// activeElement.value.targetId = conn.targetId
// })
// 双击线
drawingContent.value.bind('dblclick', (conn: any) => {
var conn = drawingContent.value.getConnections({
source: conn.sourceId,
target: conn.targetId
})[0]
drawingContent.value.deleteConnection(conn)
})
// 成功连线
drawingContent.value.bind("connection", (evt: any) => {
let from = evt.source.id
let to = evt.target.id
// nodeTypes
let sourceType = evt.source.getAttribute('nodeTypes') as string
let timeId = evt.source.getAttribute('ids') as string
let areaIds = evt.target.getAttribute('ids') as string
// 添加线的内容
if (sourceType === 'left') {
dataContent.value.lineList.push({ timeId, areaIds, from, to })
} else {
dataContent.value.lineList.push({ timeId: areaIds, areaIds: timeId, from, to })
}
})
// 删除连线回调
drawingContent.value.bind("connectionDetached", (evt: any) => {
deleteLine(evt.sourceId, evt.targetId)
})
// 改变线的连接节点
drawingContent.value.bind("connectionMoved", (evt: any) => {
changeLine(evt.originalSourceId, evt.originalTargetId)
})
// 连线右击
drawingContent.value.bind("contextmenu", (evt: any) => {
console.log('contextmenu', evt)
})
// 连线
drawingContent.value.bind("beforeDrop", (evt: any) => {
let leftType: HTMLElement = document.getElementById(evt.sourceId) as HTMLElement;
let leftids = leftType.getAttribute('nodeTypes')
var rightType: HTMLElement = document.getElementById(evt.targetId) as HTMLElement;
let rightids = rightType.getAttribute('nodeTypes')
// let rightType =evt.source.getAttribute('nodeTypes') as string
let from = evt.sourceId
let to = evt.targetId
if (from === to) {
console.log('节点不支持连接自己');
// this.$message.error('节点不支持连接自己')
return false
}
if (leftids === rightids) {
console.log('节点同节点不能链接');
// this.$message.error('节点不支持连接自己')
return false
}
if (hasLine(from, to)) {
console.log('该关系已存在,不允许重复创建');
// this.$message.error('该关系已存在,不允许重复创建')
return false
}
if (hashOppositeLine(from, to)) {
console.log('不支持两个节点之间连线回环');
// this.$message.error('不支持两个节点之间连线回环');
return false
}
console.log('连接成功');
// this.$message.success('连接成功')
return true
})
// beforeDetach
drawingContent.value.bind("beforeDetach", (evt: any) => {
console.log('beforeDetach', evt)
})
// drawingContent.value.setContainer(this.$refs.efContainer)
})
}
// 是否含有相反的线
const hashOppositeLine = (from: string, to: string) => {
return hasLine(to, from)
}
// 是否具有该线
const hasLine = (from: string, to: string) => {
for (var i = 0; i < dataContent.value.lineList.length; i++) {
var line = dataContent.value.lineList[i]
if (line.from === from && line.to === to) {
return true
}
}
return false
}
// 加载流程图
const loadEasyFlow = () => {
// 初始化节点
for (var i = 0; i < dataContent.value.nodeList.length; i++) {
let node = dataContent.value.nodeList[i]
// 设置源点,可以拖出线连接其他节点
drawingContent.value.makeSource(node.domId, lodash.merge(jsplumbSourceOptions, {}))
// drawingContent.value.makeSource(node.id, lodash.merge(jsplumbSourceOptions, {}))
// // 设置目标点,其他源点拖出的线可以连接该节点
drawingContent.value.makeTarget(node.domId, jsplumbTargetOptions)
}
// 初始化连线
for (var i = 0; i < dataContent.value.lineList.length; i++) {
let line: any = dataContent.value.lineList[i]
var connParam = {
source: line.from,
target: line.to,
label: line.label ? line.label : '',
connector: line.connector ? line.connector : '',
anchors: line.anchors ? line.anchors : undefined,
paintStyle: line.paintStyle ? line.paintStyle : undefined,
}
drawingContent.value.connect(connParam, jsplumbConnectOptions)
}
// this.$nextTick(function () {
// this.loadEasyFlowFinish = true
// })
}
// 删除线
const deleteLine = (from: string, to: string) => {
dataContent.value.lineList = dataContent.value.lineList.filter(function (line) {
if (line.from == from && line.to == to) {
return false
}
return true
})
}
// 改变连线
const changeLine = (oldFrom: string, oldTo: string) => {
deleteLine(oldFrom, oldTo)
}
onMounted(() => {
drawingContent.value = jsPlumb.getInstance() as any
// 处理数据
let left = leftData.value.map((item: any, index: number) => {
return {
...item,
value: index,
domId: 'leftDom' + index,
data: "1",
type: 'task',
}
})
let right = rightData.value.map((item: any, index: number) => {
return {
...item,
value: index,
domId: 'rightDom' + index,
data: "2",
type: 'task',
}
})
DataArr.value.nodeList = [...left, ...right]
dataReload(DataArr.value)
AccessAuthorization.locationEmpty()
})
</script>
<style lang="less" scoped>
.content {
height: 500px;
overflow: auto;
margin-left: 40px;
position: relative;
display: flex;
padding: 16px 0 16px 16px;
border-radius: 4px;
border: 1px solid rgba(220, 224, 231, 1);
}
</style>
mixins.js文件
export const jsplumbSetting= {
// 动态锚点、位置自适应
Anchors: ['Top', 'TopCenter', 'TopRight', 'TopLeft', 'Right', 'RightMiddle', 'Bottom', 'BottomCenter', 'BottomRight', 'BottomLeft', 'Left', 'LeftMiddle'],
// 容器ID
Container: 'efContainer',
// 连线的样式,直线或者曲线等,可选值: StateMachine,Flowchart,Bezier,Straight
Connector: ['Bezier', {curviness: 50}],
// 鼠标不能拖动删除线
ConnectionsDetachable: false,
// 删除线的时候节点不删除
DeleteEndpointsOnDetach: false,
/**
* 空白端点
*/
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: '#1E93FF',
// 线的粗细,值越大线越粗
strokeWidth: 2,
// 设置外边线的颜色,默认设置透明,这样别人就看不见了,点击线的时候可以不用精确点击,参考 https://blog.csdn.net/roymno2/article/details/72717101
outlineStroke: 'transparent',
// 线外边的宽,值越大,线的点击范围越大
outlineWidth: 10
},
DragOptions: {cursor: 'pointer', zIndex: 2000},
/**
* 叠加 参考: https://www.jianshu.com/p/d9e9918fd928
*/
Overlays: [
// 箭头叠加
['Arrow', {
width: 1, // 箭头尾部的宽度
length: 8, // 从箭头的尾部到头部的距离
location: 1, // 位置,建议使用0~1之间
direction: 1, // 方向,默认值为1(表示向前),可选-1(表示向后)
foldback: 0.623 // 折回,也就是尾翼的角度,默认0.623,当为1时,为正三角
}],
['Label', {
label: '',
location: 0.1,
cssClass: 'aLabel'
}]
],
// 绘制图的模式 svg、canvas
RenderMode: 'svg',
// 鼠标滑过线的样式
HoverPaintStyle: {stroke: 'red', strokeWidth: 3},
// 滑过锚点效果
// EndpointHoverStyle: {fill: 'red'}
Scope: 'jsPlumb_DefaultScope' // 范围,具有相同scope的点才可连接
}
/**
* 连线参数
*/
export const jsplumbConnectOptions={
isSource: true,
isTarget: true,
// 动态锚点、提供了4个方向 Continuous、AutoDefault
anchor: 'AutoDefault',
// 设置连线上面的label样式
labelStyle: {
cssClass: 'flowLabel'
},
// 修改了jsplumb 源码,支持label 为空传入自定义style
emptyLabelStyle: {
cssClass: 'emptyFlowLabel'
}
}
/**
* 源点配置参数
*/
export const jsplumbSourceOptions= {
// 设置可以拖拽的类名,只要鼠标移动到该类名上的DOM,就可以拖拽连线
filter: '.flow-node-drag',
filterExclude: false,
anchor: 'Continuous',
// 是否允许自己连接自己
allowLoopback: true,
maxConnections: -1,
onMaxConnections: function (info, e) {
console.log(`超过了最大值连线: ${info.maxConnections}`)
}
}
export const jsplumbTargetOptions= {
// 设置可以拖拽的类名,只要鼠标移动到该类名上的DOM,就可以拖拽连线
filter: '.flow-node-drag',
filterExclude: false,
// 是否允许自己连接自己
anchor: 'Continuous',
allowLoopback: true,
dropOptions: {hoverClass: 'ef-drop-hover'}
}
flowNode组件
<template>
<li class="content-left node-data ef-node-container" v-if="node.data === '1'" ref="location">
<!-- 左侧box内容展示 -->
<div>
<span>{{ node.name }}</span>
</div>
<div class="ef-node-left-ico flow-node-drag left-dot"></div>
</li>
<li class="content-right node-data ef-node-container" v-else ref="location">
<!-- 右侧box内容展示 -->
<div class="ef-node-left-ico flow-node-drag right-dot"></div>
{{ node.name }}
</li>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// accessauthorization 是用pinia存储的数据,也可以用vuex代替
import { accessauthorization } from '@/store/index'
// AccessAuthorization内容为左侧容器的高度
const AccessAuthorization = accessauthorization()
const location = ref<HTMLElement | string>('')
type TypeProps = {
node: any,
}
const props = withDefaults(defineProps<TypeProps>(), {
node: {},
})
const { node } = props
onMounted(() => {
if (location.value) {
let data = location.value as any
// 获取元素 nodeTypes属性 nodeTypes内容为 left或right
let azimuth = data.getAttribute('nodeTypes')
// 判断AccessAuthorization是否数组有值 进行计算总和
let sum: number = AccessAuthorization.location.length > 0 ? AccessAuthorization.location.reduce((total: number, current: number) => total + current) : 0
// 判断是左侧进行定位top赋值
if (azimuth === 'left') {
data && (data.style.top = sum + (25 * (data.value + 1)) + 'px')
// 存储左侧box高度 因为左侧盒子是未知的高度,右侧盒子是已知高度
AccessAuthorization.LocationChange(data.clientHeight)
}
// 判断右侧进行定位top赋值
else {
data && (data.style.top = (data.value * data.clientHeight) + (33 * (data.value + 1)) + 'px')
}
}
})
</script>
<style lang="less" scoped>
.content {
display: flex;
padding: 16px 0 20px 16px;
border-radius: 4px;
border: 1px solid rgba(220, 224, 231, 1);
.left-dot,
.right-dot {
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
right: -3px;
top: 50%;
transform: translateY(-50%);
background-color: rgba(30, 147, 255, 1);
}
.right-dot {
right: 0;
left: -3px;
}
&-left {
padding: 19px 21px 22px 25px;
width: 517px;
background-color: #F5F5F5;
font: 14px Arial-regular;
color: rgba(81, 90, 110, 1);
>div:nth-child(1) {
>span:first-child {
font: 16px Arial-regular;
margin-right: 6px;
}
>span:last-child {
color: rgba(145, 145, 145, 1);
}
}
}
&-right {
left: 605px;
margin: 0 0 33px 73px;
padding-left: 29px;
width: 517px;
height: 74px;
font: 14px / 74px Arial-regular;
background-color: #F5F5F5;
color: rgba(81, 90, 110, 1);
}
.ef-node-container {
position: absolute;
}
}
</style>
store内容
// 记得要在main.ts里面进行注册哦
import { createPinia } from 'pinia'
import { accessauthorization } from './modules/accessauthorization'
const pinia = createPinia()
export { accessauthorization }
export default pinia
//-------accessauthorization需要再次创建文件
import { defineStore } from 'pinia';
type accessauthorizationType = {
location: number[]
}
export const accessauthorization = defineStore({
id: 'accessauthorization',
state: (): accessauthorizationType => ({
//
location: [],
}),
actions: {
locationEmpty(){
this.location=[]
},
LocationChange(num:number) {
this.location.push(num)
}
}
})
更多推荐
所有评论(0)