Antv x6 封装使用
shell
pnpm add @antv/x6 @antv/layout @antv/x6-vue-shape @antv/x6-plugin-clipboard @antv/x6-plugin-export @antv/x6-plugin-history @antv/x6-plugin-keyboard @antv/x6-plugin-minimap @antv/x6-plugin-scroller @antv/x6-plugin-selection @antv/x6-plugin-snapline @antv/x6-plugin-stencil @antv/x6-plugin-transform查看封装代码
vue
<template>
<div class="x6-edit">
<IntervalContainer align="middle">
<a-button type="primary" size="small" :disabled="!historyInfo.canUndo" @click="graph?.undo()">
后退
</a-button>
<a-button type="primary" size="small" :disabled="!historyInfo.canRedo" @click="graph?.redo()">
前进
</a-button>
<a-button
ghost
danger
:disabled="!graph?.getCellCount()"
size="small"
@click="clearGraphData"
>
清空
</a-button>
</IntervalContainer>
<div class="mt-2 mb-2">
快捷键:
<a-typography-text keyboard>Ctrl + C (复制)</a-typography-text>
<a-typography-text keyboard>Ctrl + V (粘贴)</a-typography-text>
<a-typography-text keyboard>Ctrl + Z (撤销)</a-typography-text>
<a-typography-text keyboard>Delete 或 Backspace(删除)</a-typography-text>
<a-typography-text keyboard>节点上右键(菜单)</a-typography-text>
<a-typography-text keyboard>单击或框选(选中)</a-typography-text>
</div>
<a-row :gutter="8" class="content-container">
<a-col :span="20">
<div class="content" @contextmenu="e => e.preventDefault()">
<div class="x6-stencil-container" />
<div class="x6-box">
<div id="x6-container" />
<TeleportContainer />
</div>
<div class="x6-minimap-container" />
</div>
</a-col>
<a-col :span="4">
<a-card class="attr-card" size="small" title="属性">
<X6EditForm ref="x6EditFormRef" />
</a-card>
</a-col>
</a-row>
</div>
</template>
<script lang="ts" setup>
import { Graph } from '@antv/x6'
import { onMounted, reactive, ref, shallowRef } from 'vue'
import { TeleportContainer, registerGraphEvent, registerGraphPlugin } from '../'
import X6EditForm from './X6EditForm.vue'
import { graphOptions, registerEvents, registerStencil } from './x6-edit-helper'
const historyInfo = reactive({ canRedo: false, canUndo: false })
const graph = shallowRef<Graph>()
const x6EditFormRef = ref<InstanceType<typeof X6EditForm>>()
function initGraph() {
const container = document.querySelector('#x6-container') as HTMLDivElement
const graph = new Graph({
container,
...graphOptions
})
registerGraphEvent(graph)
registerGraphPlugin(graph)
registerStencil(graph)
registerEvents(graph)
graph.on('history:change', () => {
const currentSelects = graph.getSelectedCells()
if (currentSelects.length === 1) {
x6EditFormRef.value?.setCell(currentSelects[0])
} else {
x6EditFormRef.value?.setCell(undefined)
}
historyInfo.canRedo = graph.canRedo()
historyInfo.canUndo = graph.canUndo()
})
graph.on('selection:changed', ({ selected }) => {
if (selected.length === 1) {
x6EditFormRef.value?.setCell(selected[0])
} else {
x6EditFormRef.value?.setCell(undefined)
}
})
return graph
}
function clearGraphData() {
graph.value?.clearCells()
}
/**
* 设置画布的数据
* @param data 数据
* @param mode 设置模式,add: 添加,cover: 覆盖
*/
function setGraphData(data: any, mode: 'add' | 'cover' = 'cover') {
if (mode === 'cover') {
// graph.value?.fromJSON(data) // 直接这样加载数据会应用不上自定义的内容
graph.value?.clearCells()
}
const { cells = [] } = data
if (cells && cells.length) {
graph.value?.startBatch('add-datas')
graph.value?.cleanSelection()
cells.forEach((info: any) => {
const cell =
info.shape === 'edge' ? graph.value?.createEdge(info) : graph.value?.createNode(info)
if (cell) {
graph.value?.addCell(cell)
}
})
graph.value?.stopBatch('add-datas')
}
}
function getGraphData() {
const data = graph.value?.toJSON()
data?.cells.forEach(cell => {
Reflect.deleteProperty(cell, 'disableTransform')
Reflect.deleteProperty(cell, 'disableSelect')
Reflect.deleteProperty(cell, 'disableMove')
Reflect.deleteProperty(cell, 'disableProts')
})
return data
}
defineExpose({
setGraphData,
getGraphData
})
onMounted(() => {
graph.value = initGraph()
})
</script>
<style lang="less" scoped>
.x6-edit {
width: 100%;
.content-container {
height: calc(100% - 60px);
& > .ant-col {
max-height: 100%;
}
}
.attr-card {
height: 100%;
overflow: auto;
box-shadow: 0px 4px 8px 0px #ededf4;
}
.content {
height: 100%;
position: relative;
display: flex;
.x6-stencil-container {
position: relative;
height: 100%;
min-width: 200px;
width: 200px;
}
.x6-minimap-container {
position: absolute;
bottom: 24px;
right: 24px;
}
.x6-box {
width: calc(100% - 200px);
height: 100%;
margin-left: 4px;
:deep(.icon-envelope) {
opacity: 0;
position: absolute;
width: 48px;
font-size: 48px;
left: 0;
top: 0;
color: var(--color-primary);
}
:deep(.x6-widget-selection-box) {
border: 2px dashed var(--color-primary);
}
:deep(.x6-widget-selection-inner) {
border: 1px solid var(--color-primary);
}
}
}
.x6-tooltip-helper {
position: fixed;
opacity: 0;
z-index: -1;
left: 0;
top: 0;
}
:global(.x6-tooltip-helper-form) {
width: 200px;
}
:global(.x6-tooltip-helper-form .ant-form-item) {
margin-bottom: 0;
}
// 左侧文字超出省略
:global(.x6-widget-stencil-content div.image-node div.label div) {
max-width: 80px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
</style>ts
import type { Edge, Graph } from '@antv/x6'
import { Stencil } from '@antv/x6-plugin-stencil'
import {
addPortsForDragNode,
createImageNode as originalCreateImageNode,
type GraphOptions
} from '../'
import type { ImageNodeOptionI } from '../constants'
import { createBorderNode, createTextNode } from '../create'
export const nodePortOptions = {
ports: {
groups: {
left: {
position: 'left',
attrs: {
circle: {
magnet: true,
stroke: '#8f8f8f',
r: 4
}
},
label: {
position: 'left'
}
},
top: {
position: 'top',
attrs: {
circle: {
magnet: true,
stroke: '#8f8f8f',
r: 4
}
},
label: {
position: 'top'
}
},
right: {
position: 'right',
attrs: {
circle: {
magnet: true,
stroke: '#8f8f8f',
r: 4
}
},
label: {
position: 'right'
}
},
bottom: {
position: 'bottom',
attrs: {
circle: {
magnet: true,
stroke: '#8f8f8f',
r: 4
}
},
label: {
position: 'top'
}
}
}
}
}
export const createImageNode = (options: ImageNodeOptionI) =>
originalCreateImageNode({ ...nodePortOptions, isHolder: true, ...options })
/** 普通节点 */
const generalTypes = ['node', 'container']
const needIpTypes = ['network', 'route']
export const graphOptions = {
autoResize: true,
background: {
// color: '#F2F7FA'
},
mousewheel: {
enabled: true,
modifiers: ['ctrl']
},
// virtual: true,
grid: {
visible: true,
type: 'doubleMesh',
args: [
{
color: '#eee', // 主网格线颜色
thickness: 1 // 主网格线宽度
},
{
color: '#ddd', // 次网格线颜色
thickness: 1, // 次网格线宽度
factor: 4 // 主次网格线间隔
}
]
},
// 连线
connecting: {
// connectionPoint: {
// name: 'bbox',
// args: {
// sticky: true
// }
// },
// 吸附
snap: true,
allowBlank: false,
allowLoop: false,
allowEdge: false,
allowNode: false,
allowPort({ sourceCell, targetCell }) {
const sourceType = sourceCell?.getProp('type')
const targetType = targetCell?.getProp('type')
// 节点和节点之间不能互相连接
if (generalTypes.includes(sourceType) && generalTypes.includes(targetType)) {
return false
}
// 当是流量告警设备时,不能和普通节点连接
if (
(sourceType === 'device' && targetType === 'device') ||
(sourceType === 'device' && generalTypes.includes(targetType)) ||
(targetType === 'device' && generalTypes.includes(sourceType))
) {
return false
}
// 当是物理网络时,只能和节点或者容器连接
if (
(sourceType === 'physics' && targetType === 'physics') ||
(sourceType === 'physics' && !generalTypes.includes(targetType)) ||
(targetType === 'physics' && !generalTypes.includes(sourceType))
) {
return false
}
return true
},
highlight: true
}
} as Partial<GraphOptions.Manual>
export function registerStencil(graph: Graph) {
const stencil = new Stencil({
title: '元素',
target: graph,
stencilGraphWidth: 200,
stencilGraphHeight: 0,
stencilGraphPadding: 20,
groups: [
// { title: '默认', name: 'default' },
{ title: '文本', name: 'text' },
{ title: '节点', name: 'node' }
],
getDropNode(_) {
const node = _.clone()
node.prop('isHolder', null)
if (node.shape === 'border-node') {
node.prop('size/width', 200)
node.prop('size/height', 160)
}
if (node.shape === 'image-node') {
addPortsForDragNode(node)
}
const type = node.getProp('type')
const label = node.getProp('label')
// 判断同一种的话后面个数滚动
const count = graph
.getNodes()
.filter(
n =>
n.getProp('type') === type && n.getProp('label').split('_')[0] === node.getProp('label')
).length
node.setProp('label', `${label}_${count + 1}`)
return node
}
})
// 需要一个容纳 stencil 的 Dom 容器 stencilContainer
const stencilContainer = document.querySelector('.x6-stencil-container') as HTMLDivElement
stencilContainer.appendChild(stencil.container)
const text = graph.createNode(createTextNode({ label: '文本', isHolder: true }))
const border = graph.createNode(
createBorderNode({ label: '边框', width: 48, height: 48, isHolder: true })
)
stencil.load([text, border], 'text')
stencil.load(
[
graph.createNode(createImageNode({ label: '节点', type: 'node' })),
graph.createNode(createImageNode({ label: '设备', type: 'device' })),
graph.createNode(createImageNode({ label: '交换机', type: 'network' })),
graph.createNode(createImageNode({ label: '路由器', type: 'route' }))
],
'node'
)
return stencil
}
/**
* 检查和设置连线的data
* @param edge
*/
export function checkEdgeData(edge: Edge<Edge.Properties>) {
const { line: { stroke = '#333333', strokeWidth = 2 } = {} } = edge.getProp('attrs') || {}
const data = edge.getData() // 如果不在新增时设置,此项是undefined,考虑撤销功能或者导入功能
if (!data) {
edge.setData({
stroke,
strokeWidth
} as DragView.EdgeData)
} else {
edge.setData(data)
}
}
export function registerEvents(graph: Graph) {
// 撤销重做时重新添加或初始添加时需要把showPorts的逻辑加上
graph.on('cell:added', ({ cell }) => {
if (cell.isEdge()) {
cell.prop('attrs/line/targetMarker', null)
checkEdgeData(cell)
}
})
// 连接时添加ip占位字段
graph.on('edge:connected', ({ edge }) => {
const targetNode = edge.getTargetNode()
const targetType = targetNode?.getProp('type')
const sourceNode = edge.getSourceNode()
const sourceType = sourceNode?.getProp('type')
// 如果两端的某一端是交换机或路由,另一端是节点或容器
if (needIpTypes.includes(targetType) && generalTypes.includes(sourceType)) {
const data = sourceNode?.getData() || {}
const ipInfos = data.ipInfos ? [...data.ipInfos] : []
ipInfos.push({
// 目标交换机或路由的id
target: targetNode?.id,
label: targetNode?.getProp('label'),
ip: '',
autoIp: true
})
sourceNode?.setData({ ...data, ipInfos })
}
if (needIpTypes.includes(sourceType) && generalTypes.includes(targetType)) {
const data = targetNode?.getData() || {}
const ipInfos = data.ipInfos ? [...data.ipInfos] : []
ipInfos.push({
target: sourceNode?.id,
label: sourceNode?.getProp('label'),
ip: '',
autoIp: true
})
targetNode?.setData({ ...data, ipInfos })
}
})
// 删除时移除ip占位字段
graph.on('edge:removed', ({ cell }) => {
if (cell.isEdge()) {
// @ts-ignore
const targetNodeId = cell.target.cell || ''
const targetNode = graph.getCellById(targetNodeId)
const targetType = targetNode?.getProp('type')
// @ts-ignore
const sourceNodeId = cell.source.cell || ''
const sourceNode = graph.getCellById(sourceNodeId)
const sourceType = sourceNode?.getProp('type')
// 如果两端的某一端是交换机或路由,另一端是节点或容器
if (needIpTypes.includes(targetType) && generalTypes.includes(sourceType)) {
const data = sourceNode?.getData() || {}
const ipInfos = [...data.ipInfos]
const index = ipInfos.findIndex(item => item.target === targetNode?.id)
if (index != -1) {
ipInfos.splice(index, 1)
}
sourceNode?.replaceData({ ...data, ipInfos })
}
if (needIpTypes.includes(sourceType) && generalTypes.includes(targetType)) {
const data = targetNode?.getData() || {}
const ipInfos = [...data.ipInfos]
const index = ipInfos.findIndex(item => item.target === sourceNode?.id)
if (index != -1) {
ipInfos.splice(index, 1)
}
targetNode?.replaceData({ ...data, ipInfos })
}
}
})
graph.on('edge:change:data', ({ edge, current }) => {
const { stroke, strokeWidth } = current as DragView.EdgeData
edge.prop('attrs/line', { stroke, strokeWidth })
})
}基本使用
TIP
本例中不知何原因点击缩起会导致不显示,但在平时项目中不影响
快捷键: Ctrl + C (复制)Ctrl + V (粘贴)Ctrl + Z (撤销)Delete 或 Backspace(删除)节点上右键(菜单)单击或框选(选中)
属性
查看代码
vue
<template>
<div>
<a-button type="primary" @click="logData">控制台打印数据</a-button>
<X6Edit class="mt h-lg" ref="x6EditRef" />
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import antvPlugin from './assets'
import X6Edit from './assets/usage/X6Edit.vue'
antvPlugin.install() // 在进入页面之前安装插件
const x6EditRef = ref<InstanceType<typeof X6Edit>>()
const logData = () => {
const data = x6EditRef.value?.getGraphData()
console.log('[ data ]', data)
}
</script>