先看效果圖辜妓,可拖拽枯途,可實現(xiàn)頁面的縮放和平移
1713343220203.png
1713344998565.png
(1)用jsplumb實現(xiàn)拓?fù)鋱D的繪制以及拖拽功能
(2)用panzoom實現(xiàn)縮放和平移功能
引入jsplumb、panzoom
npm install jsplumb --save
npm install panzoom --save
直接上代碼籍滴,以后有時間在整理
copy 到工程里可以直接使用
目錄結(jié)構(gòu)
1713344595928.png
index.vue
<template>
<div class="flow-view" ref="flowView">
<!-- 設(shè)置拓?fù)鋱D區(qū)域 -->
<div id="jsplumb-flow" ref="JsplumbFlow">
<div class="manage">
<CardView :list="list1" id="manage-area" />
</div>
<div class="system">
// 點擊節(jié)點高亮酪夷,目前只加了中間這個
<CardGroupView :list="list2" id="system-area" @clickNode="handleClickNode" />
</div>
<div class="serve">
<CardGroupView :list="list3" id="serve-area" />
</div>
</div>
</div>
</template>
<script>
import jsplumbModule from 'jsplumb'
import CardView from './components/cardView.vue'
import CardGroupView from './components/cardGroupView.vue'
import { jsPlumbDefaultConfig } from './jsplumbConfig'
import panzoom from "panzoom";
import { debounce } from '@/utils/utils'
const jsplumb = jsplumbModule.jsPlumb
export default {
components: { CardView, CardGroupView },
data() {
return {
list1: [],
list2: [],
list3: [],
connectList: [],
jsplumbInstance: null,
panzoomInstance: null,
remberSelectNodeId: '', // 記錄當(dāng)前選中的節(jié)點,重復(fù)點擊孽惰,回復(fù)默認(rèn)顏色
resizeObserver : null
}
},
mounted() {
this.resizeObserver = new ResizeObserver(debounce(() => {
this.handleResize()
}), 500)
this.resizeObserver.observe(this.$refs.flowView)
// 加載數(shù)據(jù)
this.getData().then(() => {
this.$nextTick(() => {
// 初始化jsplumb
this.initJsplumb()
})
})
},
methods: {
initJsplumb() {
jsplumb.ready(() => {
//設(shè)置jsplumb實例晚岭、設(shè)置jsplumb默認(rèn)配置、設(shè)置jsplumb容器
this.jsplumbInstance = jsplumb.getInstance().importDefaults(jsPlumbDefaultConfig)
// 重設(shè)container
// this.jsplumbInstance.setContainer('jsplumb-flow')
// 先清除一下畫布,防止緩存
this.jsplumbInstance.reset();
// 處理節(jié)點數(shù)據(jù)
this.handleNodeData()
// // 會使整個jsPlumb立即重繪勋功。
// this.jsplumbInstance.setSuspendDrawing(false, true);
this.initPanZoom()
})
},
// 處理數(shù)據(jù)坦报,繪制節(jié)點
handleNodeData() {
this.list1.forEach((item) => {
// 設(shè)置拖拽, 拖拽方法狂鞋,調(diào)用接口重繪的時候片择,settingDrag 不需要重復(fù)調(diào)用,不然會報錯
// 我猜測可能是節(jié)點已經(jīng)存在要销,目前我的解決方法是傳一個參數(shù)构回,控制是否調(diào)用settingDrag方法
this.settingDrag(item)
// 初始化節(jié)點
this.initNodes(item)
})
this.list2.forEach((item) => {
this.settingDrag(item)
this.initNodes(item)
})
this.list3.forEach((item) => {
this.settingDrag(item)
this.initNodes(item)
})
// 節(jié)點連接的數(shù)據(jù)
this.connectList.forEach(item => {
const { source, target } = item
this.nodeConnect(source.nodeId, target.nodeId)
})
},
// 初始化節(jié)點
initNodes(node) {
// let endPointConfig = {}
const { nodeId } = node
if (node.type === 1) {
// 這是左側(cè)第一列,只需要右側(cè)連接點
// uuid 設(shè)置nodeID-Right疏咐,為了節(jié)點相連纤掸,對準(zhǔn)方向
this.jsplumbInstance.addEndpoint(node.nodeId, { anchor: 'Right', uuid: `${nodeId}-Right` }, jsPlumbDefaultConfig)
} else if (node.type === 2) {
// 中間數(shù)據(jù)
// 遞歸調(diào)用,將子節(jié)點下的所有節(jié)點浑塞,全部加入endPoint
if (node.children && node.children.length > 0) {
node.children.forEach(item => {
this.initNodes({...item, ...{ type: 2 }})
})
}
// 添加父節(jié)點借跪,如果不需要鏈接父節(jié)點,可不寫
this.jsplumbInstance.addEndpoint(node.nodeId, { anchor: 'Right', uuid: `${nodeId}-Right` }, jsPlumbDefaultConfig)
this.jsplumbInstance.addEndpoint(node.nodeId, { anchor: 'Left', uuid: `${nodeId}-Left` }, jsPlumbDefaultConfig)
} else {
// 右側(cè)數(shù)據(jù)酌壕,只需要暴露左側(cè)連接點
if (node.children && node.children.length > 0) {
node.children.forEach(item => {
this.initNodes({...item, ...{ type: 3 }})
})
}
this.jsplumbInstance.addEndpoint(node.nodeId, { anchor: 'Left', uuid: `${nodeId}-Left` }, jsPlumbDefaultConfig)
}
},
// 節(jié)點相連
nodeConnect(sourceId, targetId) {
this.jsplumbInstance.connect({ uuids: [`${sourceId}-Right`, `${targetId}-Left`]})
},
settingDrag(node) {
// 也可以限制節(jié)點的拖拽區(qū)域
// if (node.type === 1) {
// this.jsplumbInstance.draggable(node.nodeId, {
// containment: 'manage-area'
// });
// } else if (node.type === 2) {
// this.jsplumbInstance.draggable(node.nodeId, {
// containment: 'system-area'
// });
// } else {
// this.jsplumbInstance.draggable(node.nodeId, {
// containment: 'serve-area'
// });
// 設(shè)置節(jié)點可拖拽
this.jsplumbInstance.draggable(node.nodeId);
},
getData() {
return new Promise((resolve, reject) => {
// 添加節(jié)點數(shù)據(jù)
const arr1 = []
const arr2 = []
const arr3 = []
for (let i = 1; i < 20; i++) {
if (i <= 6) {
arr1.push({
name: '管理' + i,
nodeId: 'manage' + i,
type: 1
})
} else if (i < 13) {
const childrenList = [
{
name: '系統(tǒng)' + i + '-ip1',
nodeId: 'system' + i + 'ip1',
},
{
name: '系統(tǒng)' + i + '-ip2',
nodeId: 'system' + i + 'ip2',
},
{
name: '系統(tǒng)' + i + '-ip3',
nodeId: 'system' + i + 'ip3',
}
]
arr2.push({
name: '系統(tǒng)' + i,
nodeId: 'system' + i,
type: 2,
children: i === 7 ? childrenList : []
})
} else {
const childrenList = [
{
name: '服務(wù)' + i + '-ip1',
nodeId: 'serve' + i + 'ip1',
},
{
name: '服務(wù)' + i + '-ip2',
nodeId: 'serve' + i + 'ip2',
},
{
name: '服務(wù)' + i + '-ip3',
nodeId: 'serve' + i + 'ip3',
}
]
arr3.push({
name: '服務(wù)' + i,
nodeId: 'serve' + i,
type: 3,
children: i === 14 ? childrenList : []
})
}
}
// 設(shè)置節(jié)點位置掏愁,如果需要拖拽,node節(jié)點必須使用absolute卵牍,絕對定位去設(shè)置坐標(biāo)點
this.fixNodesPosition(arr1, 1)
this.fixNodesPosition(arr2, 2)
this.fixNodesPosition(arr3, 3)
// 獲取節(jié)點相連的數(shù)據(jù)
this.connectList = [
{ source: arr1[1], target: arr2[0].children[0] },
{ source: arr2[0].children[2], target: arr3[1].children[1] },
]
resolve()
})
},
// 手動計算節(jié)點位置
fixNodesPosition(arr, type) {
const topSpace = 12
if (type === 1) {
const modeWidth = 150
const modeHeight = 35
const width = this.$refs.flowView.offsetWidth
const modeLeft = (width / 3 / 2) - (modeWidth / 2)
arr.forEach((item, i) => {
item['width'] = modeWidth
item['height'] = modeHeight
item['top'] = (modeHeight * i) + (topSpace * (i + 1))
item['left'] = modeLeft
})
this.list1 = arr
} else if (type === 2 || type === 3) {
const modeWidth = 150
const emptyHeight = 40
const headerHeight = 30
const rowHeight = 30
const width = this.$refs.flowView.offsetWidth
const modeLeft = (width / 3 / 2) - (modeWidth / 2)
let totalHeight = 0
arr.forEach((item, i) => {
let modeHeight = 0
if (item.children && item.children.length > 0) {
if (item.children.length * rowHeight < 30) {
modeHeight = headerHeight + emptyHeight
} else {
modeHeight = (item.children.length * rowHeight) + headerHeight
}
} else {
modeHeight = headerHeight + emptyHeight
}
item['width'] = modeWidth
item['height'] = modeHeight
item['top'] = totalHeight + (topSpace * (i + 1))
item['left'] = modeLeft
totalHeight = modeHeight + totalHeight
})
if (type === 2) {
this.list2 = arr
} else {
this.list3 = arr
}
}
},
// 使用panZoom 實現(xiàn)縮放功能
//初始化縮放功能
initPanZoom() {
// panzoom(縮放區(qū)域果港,相關(guān)配置)
this.panzoomInstance = panzoom(this.$refs.JsplumbFlow, {
smoothScroll: false,
bounds: true,
// autocenter: true,
zoomDoubleClickSpeed: 1,
minZoom: 0.5,
maxZoom: 2,
})
},
handleClickNode(node) {
// 重置所有線條顏色
const connectionsAll = this.jsplumbInstance.getAllConnections()
connectionsAll.map(item => {
item.setPaintStyle({ stroke: '#000' })
})
// 設(shè)置source鏈接線
const connect = this.jsplumbInstance.getConnections({
source: node.nodeId,
target: ''
})
// target
const connect2 = this.jsplumbInstance.getConnections({
source: '',
target: node.nodeId
})
connect.map((item) => {
item.setPaintStyle({ stroke: 'red' })
})
connect2.map((item) => {
item.setPaintStyle({ stroke: 'red' })
})
},
handleResize() {
// 改變view尺寸后,重新連線
this.jsplumbInstance.repaintEverything()
},
beforeDestroy() {
// 組件銷毀前移除panzoom以避免內(nèi)存泄露
this.panzoomInstance.dispose();
this.resizeObserver.unobserve(this.$refs.flowView)
this.resizeObserver.disconnect()
}
}
}
</script>
<style lang="less" scoped>
.flow-view {
width: 100%;
height: 500px;
border: 1px solid gray;
overflow: hidden;
#jsplumb-flow {
position: relative;
width: 100%;
height: 100%;
display: flex;
.manage, .system, .serve {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
p {
text-align: center;
font-size: 20px;
font-weight: bold;
margin: 0;
height: 40px;
line-height: 40px;
}
}
// .manage {
// background-color: honeydew;
// }
// .system {
// background-color: pink;
// }
// .serve {
// background-color: paleturquoise;
// }
}
}
</style>
jsplumbConfig/index.js
// 基礎(chǔ)配置
export const jsPlumbDefaultConfig = {
Container: "jsplumb-flow",
Anchors: ['Left', 'Right'],
//四種樣式:Bezier/Straight/Flowchart/StateMachine
Connector: ["Bezier"],
// Connector: ["Straight", {stub: [20, 50], gap: 0}],
// Connector: ["Flowchart", { stub: [20, 10], gap: 10, cornerRadius: 5, alwaysRespectStubs: true }],
// Connector: ["StateMachine"],
// 連線的端點
Endpoint: "Blank", // Blank:空糊昙,不可見辛掠;Dot:圓點;Image释牺;Rectangle
// 端點的樣式
EndpointStyle: {
fill: "#c4c4c4",
outlineWidth: 10
},
// 通常連線的樣式
PaintStyle: {
stroke: '#000',
strokeWidth: 2,
outlineWidth: 20
},
//hover激活連線的樣式
HoverPaintStyle: {
stroke: 'blue',
strokeWidth: 2
},
maxConnections: -1, // 設(shè)置連接點最多可以連接幾條線 -1不限
// 繪制箭頭
Overlays: [
[
"Arrow",
{
width: 8, // 箭頭寬度
length: 8, // 箭頭長度
location: 1 // 線尾部(0-1)
}
]
],
DrapOptions: { cursor: "crosshair", zIndex: 2000 },
}
components/cardView.vue
<template>
<div class="card-view">
<div
v-for="item in list"
:key="item.nodeId"
:id="item.nodeId"
:style="getStyle(item)"
class="item"
>
{{ item.name }}
</div>
</div>
</template>
<script>
export default {
props: {
list: {
type: Array,
default: () => []
}
},
data() {
return {
}
},
methods: {
// 根據(jù)計算的top萝衩、left、width没咙、height,設(shè)置style
getStyle(item) {
return {
width: `${item.width}px`,
height: `${item.height}px`,
top: `${item.top}px`,
left: `${item.left}px`,
lineHeight: `${item.height}px`
}
}
}
}
</script>
<style lang="less" scoped>
.card-view {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
position: absolute;
// top: 40px;
top: 0;
left: 0;
right: 0;
bottom: 0;
.item {
text-align: center;
background: orange;
position: absolute;
font-size: 14px;
}
}
</style>
components/cardGroupView.vue
<template>
<div class="card-view">
<div
v-for="item in list"
:key="item.nodeId"
:id="item.nodeId"
:style="getStyle(item)"
class="item"
>
<p class="title">{{ item.name }}</p>
<div class="children">
<template v-if="item.children && item.children.length > 0">
<p
v-for="childrenItem in item.children"
:key="childrenItem.nodeId"
:id="childrenItem.nodeId"
class="row"
@click="clickNode(item.children[childIndex])"
>
{{ childrenItem.name }}
</p>
</template>
<p v-else class="empty">
{{ emptyText }}
</p>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
emptyText: {
type: String,
default: '暫無設(shè)備'
},
list: {
type: Array,
default: () => []
}
},
data() {
return {
}
},
methods: {
// 根據(jù)計算的top猩谊、left、width祭刚、height,設(shè)置style
getStyle(item) {
return {
width: `${item.width}px`,
height: `${item.height}px`,
top: `${item.top}px`,
left: `${item.left}px`,
lineHeight: `${item.height}px`
}
}牌捷,
clickNode(node) {
this.$emit('clickNode', node)
}
}
}
</script>
<style lang="less" scoped>
.card-view {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
position: absolute;
// top: 40px;
top: 0;
left: 0;
right: 0;
bottom: 0;
.item {
text-align: center;
background: orange;
position: absolute;
font-size: 14px;
border: 1px solid gray;
box-sizing: border-box;
display: flex;
flex-direction: column;
.title {
border-bottom: 1px solid gray;
box-sizing: border-box;
}
p {
margin: 0;
height: 30px;
line-height: 30px;
}
.children {
display: flex;
flex-direction: column;
min-height: 40px;
justify-content: space-around;
flex: 1;
p {
height: 20px;
line-height: 20px;
}
.row {
height: 20px;
line-height: 20px;
margin: 0 16px;
font-size: 12px;
border: 1px solid gray;
}
}
}
}
</style>
新增:點擊節(jié)點,高亮顯示連接線涡驮,同步更新在上面代碼裏
1714099326320.png
主要代碼:
method: {
handleClickNode(node) {
// 增加判斷宜鸯,重復(fù)點擊恢復(fù)默認(rèn)顏色
if (node.nodeId === this.remberSelectNodeId) {
this.remberSelectNodeId = ''
this.settingDefaultLine()
return
}
this.remberSelectNodeId = node.nodeId
this.settingDefaultLine()
this.settingSelectLine({ source: node.nodeId, target: '' })
this.settingSelectLine({ source: '', target: node.nodeId })
},
// 設(shè)置高亮顏色
settingSelectLine(lineObj) {
const connect = this.jsplumbInstance.getConnections(lineObj)
connect.map((item) => {
item.setPaintStyle({ stroke: 'red' })
})
},
settingDefaultLine() {
// 重置所有線條顏色
const connectionsAll = this.jsplumbInstance.getAllConnections()
connectionsAll.map(item => {
item.setPaintStyle({ stroke: '#000' })
})
},
}