npm install --save @antv/x6
<template>
<div class="dashboard-container">
<p>選擇節點</p>
<button @click="save">儲存</button>
<div class="antvBox">
<div class="menu-list">
<div
v-for="item in moduleList"
:key="item.id"
draggable="true"
@dragend="handleDragEnd($event, item)"
>
<img :src="item.image" alt="" />
<p>{{ item.name }}</p>
</div>
</div>
<div class="canvas-card">
<div id="container" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { Graph } from '@antv/x6'
import t1 from '../../assets/images/1.png'
import t2 from '../../assets/images/2.png'
import t3 from '../../assets/images/3.png'
import t4 from '../../assets/images/4.png'
import t5 from '../../assets/images/5.png'
const moduleList = ref([
{
id: 1,
name: '節點1',
image: t1
},
{
id: 8,
name: '節點2',
image: t2
},
{
id: 2,
name: '節點3',
image: t3
},
{
id: 3,
name: '節點4',
image: t4
},
{
id: 4,
name: '節點5',
image: t5
}
])
const curSelectNode = ref(null)
/**
* 拖拽左側
* @param e
* @param item
*/
const handleDragEnd = (e, item) => {
addHandleNode(e.pageX - 300, e.pageY - 200, new Date().getTime(), item.image, item.name)
}
const graph = ref<Graph | null>(null)
const initGraph = () => {
const container = document.getElementById('container')
if (container === null) {
throw new Error('Container element not found')
}
graph.value = new Graph({
container: container, // 畫布容器
width: container.offsetWidth, // 畫布寬
height: container.offsetHeight, // 畫布高
background: false, // 背景(透明)
snapline: true, // 對齊線
// 配置連線規則
connecting: {
snap: true, // 自動吸附
allowBlank: false, // 是否允許連線到畫布空白位置的點
allowMulti: true, // 是否允許在相同的起始節點和終止之間建立多條邊
allowLoop: true, // 是否允許建立迴圈連線,即邊的起始節點和終止節點為同一節點
highlight: true, // 拖動邊時,是否高亮顯示所有可用的節點
highlighting: {
magnetAdsorbed: {
name: 'stroke',
args: {
attrs: {
fill: '#5F95FF',
stroke: '#5F95FF'
}
}
}
},
router: {
// 對路徑新增額外的點
name: 'orth'
},
connector: {
// 邊渲染到畫布後的樣式
name: 'rounded',
args: {
radius: 8
}
}
},
panning: {
enabled: false
},
mousewheel: {
enabled: true, // 支援滾動放大縮小
zoomAtMousePosition: true,
modifiers: 'ctrl',
minScale: 0.5,
maxScale: 3
},
grid: {
type: 'dot',
size: 20, // 網格大小 10px
visible: true, // 渲染網格背景
args: {
color: '#a0a0a0', // 網格線/點顏色
thickness: 2 // 網格線寬度/網格點大小
}
}
})
nodeAddEvent()
}
/**
* 新增畫布到節點
* x座標、y座標、id節點唯一標識、image圖片、name節點名稱
*/
//新增節點到畫布
const addHandleNode = (x, y, id, image, name) => {
graph.value.addNode({
id: id,
shape: 'image', // 指定使用何種圖形,預設值為 'rect'
x: x,
y: y,
width: 60,
height: 60,
imageUrl: image,
attrs: {
body: {
stroke: '#ffa940',
fill: '#ffd591'
},
label: {
textWrap: {
width: 90,
text: name
},
fill: 'black',
fontSize: 12,
refX: 0.5,
refY: '100%',
refY2: 4,
textAnchor: 'middle',
textVerticalAnchor: 'top'
}
},
ports: {
groups: {
group1: {
position: [30, 30]
}
},
items: [
{
group: 'group1',
id: 'port1',
attrs: {
circle: {
r: 6,
magnet: true,
stroke: '#ffffff',
strokeWidth: 2,
fill: '#5F95FF'
}
}
}
]
},
zIndex: 10
})
}
/**
* 滑鼠移入節點再顯示連結樁
*/
const nodeAddEvent = () => {
const container = document.getElementById('container')
if (container === null) {
throw new Error('Container element not found')
}
const changePortsVisible = visible => {
const ports = container.querySelectorAll('.x6-port-body')
for (let i = 0, len = ports.length; i < len; i = i + 1) {
ports[i].style.visibility = visible ? 'visible' : 'hidden'
}
}
graph.value.on('node:mouseenter', () => {
changePortsVisible(true)
})
graph.value.on('node:mouseleave', () => {
changePortsVisible(false)
})
// 節點繫結點選事件 刪除節點
// eslint-disable-next-line @typescript-eslint/no-unused-vars
graph.value.on('node:click', ({ e, x, y, node, view }) => {
console.log('點選!!!', node)
// 判斷是否有選中過節點
if (curSelectNode.value) {
// 移除選中狀態
curSelectNode.value.removeTools()
// 判斷兩次選中節點是否相同
if (curSelectNode.value !== node) {
node.addTools([
{
name: 'boundary',
args: {
attrs: {
fill: '#16B8AA',
stroke: '#2F80EB',
strokeWidth: 1,
fillOpacity: 0.1
}
}
},
{
name: 'button-remove',
args: {
x: '100%',
y: 0,
offset: {
x: 0,
y: 0
}
}
}
])
curSelectNode.value = node
} else {
curSelectNode.value = null
}
} else {
curSelectNode.value = node
node.addTools([
{
name: 'boundary',
args: {
attrs: {
fill: '#16B8AA',
stroke: '#2F80EB',
strokeWidth: 1,
fillOpacity: 0.1
}
}
},
{
name: 'button-remove',
args: {
x: '100%',
y: 0,
offset: {
x: 0,
y: 0
}
}
}
])
}
})
// 刪除連結節點的線
// 連線繫結懸浮事件
graph.value.on('cell:mouseenter', ({ cell }) => {
if (cell.shape == 'edge') {
cell.addTools([
{
name: 'button-remove',
args: {
x: '100%',
y: 0,
offset: {
x: 0,
y: 0
}
}
}
])
cell.setAttrs({
line: {
stroke: '#409EFF'
}
})
cell.zIndex = 99 // 保證當前懸停的線在最上層,不會被遮擋
}
})
graph.value.on('cell:mouseleave', ({ cell }) => {
if (cell.shape === 'edge') {
cell.removeTools()
cell.setAttrs({
line: {
stroke: 'black'
}
})
cell.zIndex = 1 // 保證未懸停的線在下層,不會遮擋懸停的線
}
})
}
//儲存畫布,並提交
const save = () => {
console.log(graph.value.toJSON(), 'graph')
console.log(graph.value.getNodes(), 'node')
}
onMounted(() => {
initGraph()
})
</script>
<style lang="scss" scoped>
/* @use ''; 引入css類 */
.dashboard-container {
.antvBox {
display: flex;
width: 100%;
height: 100%;
color: black;
padding-top: 20px;
.menu-list {
height: 100%;
width: 300px;
padding: 0 10px;
box-sizing: border-box;
display: flex;
justify-content: space-between;
align-content: flex-start;
flex-wrap: wrap;
> div {
margin-bottom: 10px;
border-radius: 5px;
padding: 0 10px;
box-sizing: border-box;
cursor: pointer;
color: black;
width: 105px;
display: flex;
flex-wrap: wrap;
justify-content: center;
img {
height: 50px;
width: 50px;
}
P {
width: 90px;
text-align: center;
}
}
}
.canvas-card {
width: 1700px;
height: 750px;
box-sizing: border-box;
> div {
width: 1400px;
height: 750px;
border: 2px dashed #2149ce;
}
}
}
}
</style>
借鑑https://blog.csdn.net/wzy_PROTEIN/article/details/136305034