前言
一个用于物流行业的一个功能;需求需要一个画布可以拖拽节点(条形码,文字等等信息)组成一个信息图(类似快递上的物流信息),最终可以导出图片;
每个节点(条形码,文字等等)可以拖拽编辑,这里使用vue作为antv-x6的节点来实现交互和导出图片
可以对节点拖拽,有容器节点;选中节点可以在右侧对节点一些属性修改,这些是和节点组件可通信的;
这里只是示例,可通过自定义的扩展节点组件和右侧表单字段扩展
git仓库
用到了这些库
{
"@antv/x6": "^2.18.1",
"@antv/x6-plugin-export": "^2.1.6",
"@antv/x6-plugin-stencil": "^2.1.5",
"@antv/x6-vue-shape": "^2.1.2",
"monaco-editor": "^0.52.0",
"vue": "^3.4.37"
}
-
@antv/x6
主要的库了,提供了基本的画布、默认节点的操作交互
-
@antv/x6-plugin-stencil
antv/x6的一个扩展--左侧可拖拽到画布中的菜单
-
@antv/x6-plugin-export
导出各类图片用到的扩展
-
@antv/x6-vue-shape
将vue组件作为节点的扩展
-
monaco-editor
一个代码编辑器,可以输入代码格式化等等;vscode同款
一、vue组件注册为节点
x6.antv.antgroup.com/tutorial/in…
在@antv/x6-vue-shape
扩展库中提供了,getTeleport
和register
方法
-
register 将vue组件注册为节点
import {register} from '@antv/x6-vue-shape' import address from "../nodeList/address.vue"; register({ shape: 'custom-address', //具体使用时节点名称/类型 width: 260, height: 32, component: address, //vue组件 })
-
getTeleport 使用扩展
在antv实例的vue组件中,加入以下代码
import { getTeleport } from '@antv/x6-vue-shape'; const TeleportContainer = getTeleport();
template中使用
TeleportContainer
<template> <div class="designer"> <div class="designer-menu" ref="menuRef"></div> <div class="designer-graph" ref="graphRef"></div> <div class="designer-form"> <div class="designer-form-actions"> <button @click="handleExportPng">导出PNG</button> <button @click="handleExportSvg">导出SVG</button> <button @click="handleExportBase">获取BASE64</button> </div> <nodeForm :selectCell="selectCell"></nodeForm> </div> </div> <TeleportContainer /> </template>
二、节点组件的内容,和如何知晓更新了内容(通信)
x6.antv.antgroup.com/tutorial/in… 该文档下方更新节点可查看官方描述
例如上方用到的address组件,它的内容如下
nodeBase.ts
通过inject('getGraph')
inject('getNode')
分别获取画布实例和节点对象
可通过节点事件change:data
监听节点的data属性变化,去做一些操作
import {computed, inject, ref} from 'vue';
export default function(){
const Graph = inject<any>('getGraph')('getGraph');
const Node = inject<any>('getNode')('getNode');
const data = ref(Node.data);
Node.on('change:data', ({current}: any) => {
data.value = current;
})
const selectedToClassName = computed(()=>{
if(data.value?.selected){
return {
'node-list-selected':true,
}
}else{
return {}
}
})
return {
Graph,
Node,
data,
selectedToClassName
}
}
address.vue
<script setup lang="ts">
import nodeBase from './tool/node-base.ts';
const {Graph, Node, data, selectedToClassName} = nodeBase();
</script>
<template>
<div class="node-list-address" :class="selectedToClassName">
<div class="node-list-component-content" :style="data?.styles">
{{ data.label }}
</div>
</div>
</template>
三、添加默认节点到画布中
使用画布实例addNode
方法添加一个元素到画布中
该方法中的shape
是在[一]当中register-shape
字段值
const defaultNodes = () => {
if (!example.value.graph) return;
const parent = example.value.graph.addNode({
x: 100,
y: 100,
width: 400,
height: 400,
shape: 'custom-parent',
data: { parent: true },
});
parent.addChild(example.value.graph.addNode({
x: 110,
y: 110,
width: 100,
height: 30,
shape: 'custom-address',
data: {
label: '北京市-朝阳区-xx-xx',
styles: { color: '#333' }
}
}));
parent.addChild(example.value.graph.addNode({
x: 110,
y: 310,
width: 100,
height: 30,
shape: 'custom-box',
data: {
label: 'defalut node',
styles: { color: '#333' }
}
}));
};
调用该函数集合添加已经注册的节点到画布中,这里用到parent.addChild
是父子节点
我是通过data:{parent:true}
来表明这是一个可以加入子节点的容器
四、父子节点的嵌套
需要有嵌套关系(拖动父节点,其中的子节点相对的移动),需要在实例画布时加入以下内容:
embedding
配置中 看到是通过node.getData().parent来判断是否嵌套
export const installGraph = (dom:HTMLElement) => {
const graph = new Graph({
container: dom,
grid: true,
mousewheel: {
enabled: true,
zoomAtMousePosition: true,
modifiers: 'ctrl',
minScale: 0.5,
maxScale: 3,
},
panning:true,
// 是否嵌套节点
embedding:{
enabled: true,
findParent({ node }) {
//! 用来区分是否是父节点的,通过设置 节点的data:{parent:true}得知
const bbox = node.getBBox()
return this.getNodes().filter((node) => {
const data = node.getData<{ parent: boolean }>()
if (data && data.parent) {
const targetBBox = node.getBBox()
return bbox.isIntersectWithRect(targetBBox)
}
return false
})
},
},
});
graph.use(new Export())
return graph;
}
五、更新节点尺寸,data,zindex
- 更新尺寸 x6.antv.antgroup.com/api/model/n…
- 更新data对象 x6.antv.antgroup.com/api/model/c…
- 更新zindex x6.antv.antgroup.com/api/model/c…
给画布添加节点按下事件cell:mousedown
,我们来获取选中节点;
let selectCell = ref();
const nodeSelectedFn = (cell: Cell) => {
if (selectCell.value && selectCell.value.setData) {
selectCell.value.setData({ selected: false });
}
selectCell.value = graph.getCellById(cell.id);
selectCell.value.setData({ selected: true });
};
graph.on('cell:mousedown', ({ cell }) => {
nodeSelectedFn(cell);
});
假如有一按钮点击后修改data
中的label
const click = () =>{
selectCell.value.setData({
label:+new Date()
})
}
修改尺寸size
//cell.size()返回宽高信息
const click = () =>{
selectCell.value.size({
width:100,
height:selectCell.value.size().height
})
}
更多查看文档
六、使用导出图片扩展
在【四】中看到有一段代码graph.use(new Export())
我们引入Export
即可
import { Export } from '@antv/x6-plugin-export';
具体使用
// 导出png
const handleExportPng = () => {
example.value.graph?.exportPNG();
};
// 导出svg
const handleExportSvg = () => {
example.value.graph?.exportSVG();
};
function base64ToFile(base64: string, filename: string): File {
const arr = base64.split(',');
const mime = arr[0].match(/:(.*?);/)[1];
const bstr = atob(arr[1]);
const n = bstr.length;
const u8arr = new Uint8Array(n);
for (let i = 0; i < n; i++) {
u8arr[i] = bstr.charCodeAt(i);
}
return new File([u8arr], filename, { type: mime });
}
// 转为base64,再将base64转为file等等可用来接口提交
const handleExportBase = () => {
example.value.graph?.toPNG((a: string) => {
console.log(a);
const file = base64ToFile(a, 'x6.png');
console.log(file);
});
}
七、拖拽菜单
1. 注册菜单实例
传入菜单的容器元素,菜单可拖拽的目标实例
export const installMenu = (dom:HTMLElement, target:Graph) => {
const stencil = new Stencil({
search: true,
target: target,
title: '自定义的菜单以及节点',
stencilGraphWidth: 260,
stencilGraphPadding: 0,
layoutOptions: {
columns: 1,
dx: 0,
dy: 3,
columnWidth: 'compact',
rowHeight: 'compact'
},
groups: [
{
title: '基础流程图',
name: 'group1',
},
],
})
dom.appendChild(stencil.container);
return stencil;
}
2. 添加菜单选项
传入画布实例,菜单实例
export const installMenuOptions = (graph:Graph, menu:Stencil) => {
const r1 = graph.createNode({
shape: 'custom-address',
data: {
label: 'vue-component-01',
styles:{
background:'#ccc',
fontSize:'16px'
}
}
})
const r2 = graph.createNode({
shape: 'custom-box',
data: {
label:'vue-component-02',
styles:{
background:'#ccc',
fontSize:'16px'
}
}
})
const r3 = graph.createNode({
shape: 'custom-parent',
data: {
parent:true,
label:'vue-component-parent',
styles:{
background:'#ccc',
fontSize:'16px',
lineHeight:'32px'
}
}
})
menu.load([r1, r2, r3], 'group1')
}