一、jsplumbs的基本使用,例子在vue2项目中实现的
- 初始化
mounted() { this.container = document.getElementById('container') this.jsPlumbInstance = jsPlumb.getInstance({ //特别注意container容器需要设置绝对定位,不要设置宽高,否则会出现画线位置不准确和滚 //动条滚动后位置错乱问题 Container: 'container' // 线条挂载区域,不设置会渲染在 body 里 }) },
- 画线
this.jsPlumbInstance.connect( { source: id1, //需要连线的dom元素或元素id target: id2, //需要连线的dom元素或元素id endpoint: 'Blank', anchor: 'Continuous', connector: 'Flowchart', // 连接线的曲线 anchors:[[0.5, 1, 0, 1],[0, 0.5, -1, 0]] // 锚点位置 } ) //锚点位置[x,y,x1,y1],x,y代表锚点的位置,x1,y1表示锚点的方向 //例[0.5, 1, 0, 1]表示下边框中点位置,方向向下 //[0, 0.5, -1, 0]表示左边框中点位置,方向向左
- 删除连接线
//删除单个节点线 delNodeLine(node) { //node为元素节点或元素id this.jsPlumbInstance.deleteConnectionsForElement(node) },
- 删除所有连接线
//删除所有节点和画线 delAllLine() { // 这个方法删除所有连线,不需要传入参数 this.jsPlumbInstance.deleteEveryConnection() // 这个方法删除所有节点,不需要传入参数 this.jsPlumbInstance.deleteEveryEndpoint() },
案例:
二、父组件
<template>
<div class="wrap">
<div id="container">
<div class="serviceNavigation" id="serviceNavigation">
<div class="item_wrap" :style="{ 'margin-right': item.children.length ? 0 : '80px' }" v-for="(item, index) in dataList" :key="item.myId">
<div>
<div class="item" :id="item.myId" :class="{ active: curFocus === item.myId }">
<div class="main" @click="handleItemClick(item)">
<el-input @change="changeValue($event, item)" v-model="item.name"></el-input>
</div>
<div class="plusIcon" v-if="curFocus === item.myId && index === dataList.length - 1">
<i style="color: #008ad6" @click="pushItem(item)" class="el-icon-circle-plus"></i>
<i style="color: #ff4545" v-if="index" @click="removeItem(index)" class="el-icon-remove"></i>
<i style="width: 16px; display: inline-block" v-else></i>
</div>
<div class="pushChildIcon" v-if="curFocus === item.myId && index && !item.children.length">
<i style="color: #008ad6" @click="pushChildItem(item)" class="el-icon-circle-plus"></i>
</div>
</div>
<ChildList
:item="item"
:curFocus.sync="curFocus"
:menuList="menuList"
@getNavigationList="getNavigationList"
@handleItemClick="handleItemClick"
@pushChildItem="pushChildItem"
@removeChildItem="removeChildItem" />
</div>
</div>
</div>
</div>
<div class="save_btn">
<el-button type="primary" :loading="loading" @click="saveBtn">保存</el-button>
</div>
</div>
</template>
<script>
import { jsPlumb } from 'jsplumb'
import ChildList from './components/ChildList.vue'
import { getOrgMenuList, getNavigationList, delNavigation, addNavigation, editNavigation } from './api'
export default {
name: 'ServiceNavigation',
components: {
ChildList
},
data() {
return {
curFocus: '',
jsPlumbInstance: null,
defaultNavigation: { name: '业务导航', children: [], myId: String(Date.now()), level: 1 },
dataList: [this.defaultNavigation],
menuList: [],
container: null,
loading: false
}
},
deactivated() {
this.delAllLine()
},
destroyed() {
this.delAllLine()
},
activated() {
console.log('actived')
this.initAllLine(this.dataList)
},
mounted() {
this.container = document.getElementById('container')
this.jsPlumbInstance = jsPlumb.getInstance({
Container: 'container' // 线条挂载区域,不设置会渲染在 body 里
})
// this.jsPlumbInstance.setContainer('contanin')
this.getOrgMenuList()
this.getNavigationList()
},
methods: {
//初始化所有连接线
initAllLine(list) {
console.log(2222)
list.forEach((item) => {
item.children?.forEach((child) => {
if (item.children?.length) {
this.initAllLine(item.children)
}
this.canvasLine(item.myId, child.myId)
})
})
list.reduce((cur, pre) => {
if (cur.level === 1 && pre.level === 1) {
console.log('levellevellevel')
this.canvasLine(cur.myId, pre.myId, [
[1, 0.5, 1, 0],
[0, 0.5, -1, 0]
])
}
return pre
})
},
//删除所有节点和画线
delAllLine() {
// 这个方法删除所有连线,不需要传入参数
this.jsPlumbInstance.deleteEveryConnection()
// 这个方法删除所有节点,不需要传入参数
this.jsPlumbInstance.deleteEveryEndpoint()
},
//画线
canvasLine(
id1,
id2,
anchors = [
[0.5, 1, 0, 1],
[0, 0.5, -1, 0]
]
) {
console.log(id1, id2)
// 确保 jsPlumb 初始化后再进行操作
this.jsPlumbInstance.ready(() => {
this.jsPlumbInstance.connect(
{
source: id1,
target: id2,
// options: {},
endpoint: 'Blank',
anchor: 'Continuous',
connector: 'Flowchart', // 连接线的曲线
anchors // 锚点位置
}
)
})
},
//删除单个节点线
delNodeLine(node) {
this.jsPlumbInstance.deleteConnectionsForElement(node)
},
handleItemClick(item, flag = true) {
if (item.myId === this.curFocus && flag) {
this.curFocus = ''
} else {
this.curFocus = item.myId
}
},
pushItem(item) {
const item2 = { name: '', myId: String(Date.now()), children: [], level: 1 }
this.dataList.push(item2)
console.log(item.myId, item2.myId)
this.$nextTick(() => {
this.canvasLine(item.myId, item2.myId, [
[1, 0.5, 1, 0],
[0, 0.5, -1, 0]
])
})
},
removeItem(index) {
const item = this.dataList.splice(index, 1)[0]
console.log(item, 'delItem')
this.delNodeLine(item.myId)
},
pushChildItem(item) {
item.children.push({ name: '', myId: String(Date.now()), children: [] })
this.jsPlumbInstance.reset() //清除之前的连接关系 否则无法重新绘制
this.$nextTick(() => {
this.initAllLine(this.dataList)
})
},
removeChildItem(item, index) {
item.children.forEach((child) => {
this.delNodeLine(child.myId)
})
item.children.splice(index, 1)
this.jsPlumbInstance.reset() //清除之前的连接关系 否则无法重新绘制
this.$nextTick(() => {
this.initAllLine(this.dataList)
})
},
//获取机构服务下所有菜单
getOrgMenuList() {
getOrgMenuList({ status: 0, visible: 0, parentName: '机构服务' }).then((res) => {
this.menuList = res.data || []
console.log(this.menuList, 'this.menuList000')
})
},
//获取业务导航列表
getNavigationList() {
this.delAllLine()
getNavigationList({}).then((res) => {
res.data.forEach((item) => {
item.level = 1
})
this.dataList = [this.defaultNavigation, ...res.data]
this.mapDataList(this.dataList)
this.$nextTick(() => {
this.initAllLine(this.dataList)
console.log(this.dataList, 'datalist')
})
})
},
mapDataList(list) {
let key = Date.now()
list.forEach((item) => {
if (!item.children || !item.children?.length) {
item.children = []
}
item.myId = String(key + 1 + Math.random())
item.name = item.menuId ? item.menuId.split(',').map((i) => Number(i)) : item.name
if (item.children.length) {
this.mapDataList(item.children)
}
})
},
saveBtn() {
this.loading = true
function mapDataList(list) {
return list.map((item) => {
if (item.children?.length) item.children = mapDataList(item.children)
if (!item.level) {
return {
menuId: item.menuId,
id: item.id,
parentId: item.parentId,
children: item.children,
navigateUrl: item.navigateUrl
}
} else {
return {
name: item.name,
id: item.id,
children: item.children
}
}
})
}
const children = mapDataList(JSON.parse(JSON.stringify(this.dataList.slice(1))))
console.log(children, 'children')
addNavigation({ children })
.then((res) => {
this.getNavigationList()
this.$notify({
type: 'success',
message: '保存成功'
})
})
.finally(() => {
this.loading = false
})
}
}
}
</script>
<style lang='scss' scoped>
#container {
position: relative;
}
::-webkit-scrollbar {
display: none;
}
.wrap {
position: relative;
width: 100%;
height: 100%;
overflow: auto;
scrollbar-width: none;
.save_btn {
position: fixed;
bottom: 20px;
right: 20px;
}
}
::v-deep .el-input__inner {
position: relative;
border: 0;
border-radius: 0;
// // vertical-align: bottom;
height: 20px;
padding-left: 0;
text-align: center;
}
::v-deep .el-input__icon {
height: 20px;
}
::v-deep .el-icon-arrow-up {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 0px;
transition: all 0s !important;
}
::v-deep .el-select__caret.is-reverse {
transform: rotate(360deg) !important;
transform: translateY(-100%) !important;
// right: 0px;
}
.serviceNavigation {
display: flex;
height: 100%;
.active {
border: 2px solid #008ad6 !important;
}
.item_wrap {
display: flex;
flex-direction: column;
// min-width: 300px;
.item,
.childItem {
position: relative;
display: inline-block;
padding: 5px;
width: 150px;
// height: 52px;
box-sizing: border-box;
border: 2px solid transparent;
// margin-right: 80px;
.main {
border: 1px solid #979797;
border-radius: 20px;
text-align: center;
background-color: #fff;
padding: 7px 15px;
cursor: pointer;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
::v-deep .el-input__inner {
font-size: 17px;
font-weight: bold;
}
}
.plusIcon {
position: absolute;
top: 50%;
right: -40px;
transform: translateY(-50%);
cursor: pointer;
}
.pushChildIcon {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: -25px;
cursor: pointer;
}
}
.childList {
margin-top: 40px;
margin-left: 110px;
.childItem {
display: block;
.main {
border-radius: 5px;
::v-deep .el-input__inner {
font-size: 13px;
}
}
.plusIcon {
right: -40px;
}
}
}
}
}
</style>
三、子组件
子组件中引用自身组件实现循环引用可无限嵌套
<template>
<div class="childList">
<div :style="{ height: child.children.length ? 'auto' : '52px' }" v-for="(child, index) in item.children" :key="child.myId">
<div class="childItem" :id="child.myId" :class="{ active: curFocus === child.myId }">
<div class="main" @click="handleItemClick(child)">
<el-cascader
ref="refCascader"
v-model="child.name"
@change="changeValue($event, item, child, index)"
:options="menuList"
:props="cas_props"
:show-all-levels="false"
clearable></el-cascader>
</div>
<div class="plusIcon" v-if="curFocus === child.myId">
<i style="color: #008ad6" @click="pushChildItem(child)" class="el-icon-circle-plus"></i>
<i style="color: #ff4545" @click="removeChildItem(item, index)" class="el-icon-remove"></i>
</div>
<div class="pushChildIcon" v-if="curFocus === child.myId && index === item.children.length - 1">
<i style="color: #008ad6" @click="pushChildItem(item)" class="el-icon-circle-plus"></i>
</div>
</div>
<child-list
:item="child"
:menuList="menuList"
:curFocus="curFocus"
@getNavigationList="emitGetNavigationList"
@handleItemClick="handleItemClick"
@pushChildItem="pushChildItem"
@removeChildItem="removeChildItem" />
</div>
</div>
</template>
<script>
import { addNavigation, editNavigation } from '../api'
export default {
name: 'ChildList',
props: {
item: {
type: Object,
required: true
},
curFocus: {
type: String | Number,
required: true
},
menuList: {
type: Array,
required: true
}
},
data() {
return {
cas_props: { checkStrictly: true, label: 'menuName', value: 'menuId' }
}
},
mounted() {},
methods: {
handleItemClick(item, flag) {
this.$emit('handleItemClick', item, flag)
},
pushChildItem(child) {
this.$emit('pushChildItem', child)
},
removeChildItem(item, index) {
this.$emit('removeChildItem', item, index)
},
emitGetNavigationList() {
this.$emit('getNavigationList')
},
changeValue(val, parent, child, index) {
child.menuId = val.join(',')
child.parentId = parent.id
child.navigateUrl = '/orgNoService'
this.$refs.refCascader[index].getCheckedNodes()[0].pathNodes.forEach((item) => {
child.navigateUrl += '/' + item.data.path
})
console.log(child.navigateUrl, 'path')
}
}
}
</script>
<style lang='scss' scoped>
::v-deep .el-cascade,
.el-cascader--medium {
height: 20px !important;
line-height: 20px !important;
}
::v-deep .el-input__inner {
position: relative;
border: 0;
border-radius: 0;
// // vertical-align: bottom;
padding-left: 0;
text-align: center;
padding-right: 5px !important;
}
::v-deep .el-input__icon {
height: 20px;
}
::v-deep .el-icon-arrow-down {
position: absolute;
top: 50%;
transform: translateY(-85%);
right: -1px;
transition: all 0s !important;
}
::v-deep .el-input__suffix-inner {
position: absolute;
top: 50%;
transform: translateY(-50%);
height: 35px;
right: -20px;
// top: -5px;
}
::v-deep .el-select__caret.is-reverse {
transform: rotate(360deg) !important;
transform: translateY(-100%) !important;
// right: 0px;
}
.active {
border: 2px solid #008ad6 !important;
}
.childItem {
position: relative;
display: inline-block;
padding: 5px;
width: 150px;
// height: 52px;
box-sizing: border-box;
border: 2px solid transparent;
// margin-right: 80px;
.main {
border: 1px solid #979797;
border-radius: 20px;
text-align: center;
background-color: #fff;
padding: 7px 15px;
cursor: pointer;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
::v-deep .el-input__inner {
font-size: 17px;
font-weight: bold;
}
}
.plusIcon {
position: absolute;
top: 50%;
right: -40px;
transform: translateY(-50%);
cursor: pointer;
}
.pushChildIcon {
position: absolute;
left: 50%;
// transform: translateX(-50%);
bottom: -25px;
cursor: pointer;
}
}
.childList {
display: flex;
flex-direction: column;
justify-content: flex-start;
margin-top: 40px;
margin-left: 110px;
.childItem {
display: block;
.main {
border-radius: 5px;
::v-deep .el-input__inner {
font-size: 13px;
}
}
.plusIcon {
right: -40px;
}
}
}
</style>
四、实现效果