前端实现连线效果
效果展示
借鉴地址
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/4353c700bd6791eda361ccc276a8aa78.png)
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/f82daef0ce8933c22cfd1be19376d291.png)
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/ba6935de8eb949b4cd880e061f8a82dc.png)
父组件内容
<template>
<div>
<div class="content step_2-content" id="efContainer">
<ul>
<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)
drawingContent.value.setSuspendDrawing(false, true);
loadEasyFlow()
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
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 from = evt.sourceId
let to = evt.targetId
if (from === to) {
console.log('节点不支持连接自己');
return false
}
if (leftids === rightids) {
console.log('节点同节点不能链接');
return false
}
if (hasLine(from, to)) {
console.log('该关系已存在,不允许重复创建');
return false
}
if (hashOppositeLine(from, to)) {
console.log('不支持两个节点之间连线回环');
return false
}
console.log('连接成功');
return true
})
drawingContent.value.bind("beforeDetach", (evt: any) => {
console.log('beforeDetach', evt)
})
})
}
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.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)
}
}
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'],
Container: 'efContainer',
Connector: ['Bezier', {curviness: 50}],
ConnectionsDetachable: false,
DeleteEndpointsOnDetach: false,
Endpoint: ['Blank', {Overlays: ''}],
EndpointStyle: {fill: '#1879ffa1', outlineWidth: 1},
LogEnabled: true,
PaintStyle: {
stroke: '#1E93FF',
strokeWidth: 2,
outlineStroke: 'transparent',
outlineWidth: 10
},
DragOptions: {cursor: 'pointer', zIndex: 2000},
Overlays: [
['Arrow', {
width: 1,
length: 8,
location: 1,
direction: 1,
foldback: 0.623
}],
['Label', {
label: '',
location: 0.1,
cssClass: 'aLabel'
}]
],
RenderMode: 'svg',
HoverPaintStyle: {stroke: 'red', strokeWidth: 3},
Scope: 'jsPlumb_DefaultScope'
}
export const jsplumbConnectOptions={
isSource: true,
isTarget: true,
anchor: 'AutoDefault',
labelStyle: {
cssClass: 'flowLabel'
},
emptyLabelStyle: {
cssClass: 'emptyFlowLabel'
}
}
export const jsplumbSourceOptions= {
filter: '.flow-node-drag',
filterExclude: false,
anchor: 'Continuous',
allowLoopback: true,
maxConnections: -1,
onMaxConnections: function (info, e) {
console.log(`超过了最大值连线: ${info.maxConnections}`)
}
}
export const jsplumbTargetOptions= {
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'
import { accessauthorization } from '@/store/index'
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
let azimuth = data.getAttribute('nodeTypes')
let sum: number = AccessAuthorization.location.length > 0 ? AccessAuthorization.location.reduce((total: number, current: number) => total + current) : 0
if (azimuth === 'left') {
data && (data.style.top = sum + (25 * (data.value + 1)) + 'px')
AccessAuthorization.LocationChange(data.clientHeight)
}
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内容
import { createPinia } from 'pinia'
import { accessauthorization } from './modules/accessauthorization'
const pinia = createPinia()
export { accessauthorization }
export default pinia
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)
}
}
})