摘要
** 用Vue3+TypeScript+AntVX6实现Web组态(从技术层面与实现层面进行分析),包含画布创建、节点设计、拖拽实现(实际案例)、节点连线、交互功能,后续文章持续更新。**
注:本文章可以根据目录进行导航
文档支持
AntVX6使用文档
https://x6.antv.antgroup.com/tutorial/getting-started
AntVX6接口参数文档
https://x6.antv.antgroup.com/api/graph/graph
SVG基础文档
https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Introduction
大致描述
个人认为以下图片为AntVX6的一些基础关键(详细请见官方文档)
1.提供了画布的参数修改=>方便面板的构建
2.提供了节点的修改=>可以对节点进行增、删、改,并且可以定制化操作(增代表增加节点、删代表删除节点、改代表修改节点的属性)
3.元素式Cell是节点Node、边Edge的基类,也就是Node、Edge继承于Cell(Cell有的属性Node、Edge都有)
元素、节点、边对应参数截图(节点的学习关键是学习元素的参数,详细见API文档):
具体实现
步骤一:绘制画布
完整代码如下(使用Vue3+TypeScript构建)
<div id="container"></div>
const graph = ref<Graph | null>(null);
onMounted(() => {
graph.value = new Graph({
width: 1800,
height: 1200,
panning:true,
mousewheel:true,
background: {
color: '#F2F7FA',
},
container: document.getElementById('container')!, // 断言该值不为 null
grid: {
visible: true,
type: 'doubleMesh',
args: [
{
color: '#eee', // 主网格线颜色
thickness: 1, // 主网格线宽度
},
{
color: '#ddd', // 次网格线颜色
thickness: 1, // 次网格线宽度
factor: 4, // 主次网格线间隔
},
],
},
});
});
代码解释:
1.我把graph画布单独定义出来,这样就可以定义更多的自定义属性(要记住单独定义完以后要通过graph.value才可以访问里面的属性)。
2.设置画布的大小width、height(官方提供了自动大小
autoResize属性,
但是在我代码上一直有一些小bug所以就用自定义的宽和高,没有用自动设置的这个参数,需要的可自行研究)
3.Graph 中通过
panning
和
mousewheel
配置来实现缩放与平移,鼠标按下画布后移动时会拖拽画布,滚动鼠标滚轮会缩放画布。
4.background为背景色(官方提供自定义背景,并且可以放置图片)
5.配置绘制画布对应的页面区域,并且加上!断言不为空(解决TS报可能为空的错误)
container: document.getElementById('container')!, // 断言该值不为 null
6.设置网格grid(可以直接复制,目前已知作用是让画布更好看)
7.附上对画布尺寸、位置进行操作一些常用的 API
最终的画布效果
步骤二:节点设计
节点本身构造
节点本身构造难点:markup与attrs两个参数,所以我们重点分析。
以下为官方对markup与attrs的解释:
以下是作者本人对这两个参数的理解:
1.首先两者关系是:**attrs⊂markup**(attrs包含于markup,也就是首先要记住attrs是markup中的属性)
2.举个形象的例子来说明
attrs
和
markup
的作用,可以想象你正在搭建一个房子,而这个房子的结构(墙壁、窗户、门等)就是
markup
,而你为这些结构上色、装饰的细节(颜色、边框、材质等)就是
attrs
。
**
markup
**:定义了房子的组成部分,比如墙、窗户、门等。你可以通过它告诉 X6:房子有哪些部分,每个部分是什么类型(是矩形?是图片?是文本?)。**
attrs
**:用来决定这些部分的样子。你可以为墙刷上白色油漆、为窗户加上边框、为门安装一个红色的把手。3. 其实简单理解就是:markup就是定义当前节点或边具有哪些部分,attrs就是改的markup中的对应部分。 4.注意:若加上了markup参数,在 AntV X6 中,markup 是用来定义节点的结构和内容的,控制着节点渲染时使用的 SVG 或 HTML 元素。如果你在 markup 中传递了空数组([]),X6 不会自动生成任何内容,因此即使你定义了 shape 和 imageUrl,也不会有任何元素被渲染出来。
attrs: {
},
markup: [
],
以下代码则正确显示节点。若移除 markup: 如果你移除 markup 属性,X6 将使用默认的标记来渲染节点,这样 shape: 'image' 和 imageUrl 的配置会生效,图像将会被渲染出来。
markup对应参数如下
官方对markup参数的解释
作者本人理解如下:
1.tagName
tagName:就比如这个代码例子,可以这么理解,tagName代表创建一个 <rect> 元素,所以如果你要创建一个矩形,你会使用
tagName: 'rect'
,要创建文本,则使用
tagName: 'text'。
站在html上理解:也就是相当于tagName:'rect' = <rect></rect>,tagName:'text' = <text></text>。
markup: [
{
tagName: 'rect',
},
],
2.selector
selector:该元素的唯一选择器,通过选择器为该元素指定属性样式。
markup
部分定义了节点的结构,规定了有哪些元素,比如rect
,image
,text等
。- 每个元素通过
selector
连接到attrs
对象中对应的属性。
const commonAttrs = {
btnText: {
fontSize: 14,
fill: 'red',
text: 'x',
refX: '88%',
y: -35,
cursor: 'pointer',
pointerEvent: 'none',
},
};
attrs: commonAttrs,
markup: [
{
tagName: 'text',
selector: 'btnText', // 用 `btnText` 作为选择器
},
]
节点设置流程(此处以添加节点的方式解析节点设置):
1.设置节点大小与位置
根据Api的说明,节点的大小与位置设置为position、size
const node = graph.value.addNode({
position:{
x: 290,
y: 150,
},
size:{
width: 150,
height: 150,
},
})
但是根据使用说明文档发现,可以直接使用x、y、width、height字段(亲测两个都可以实现,并且效果是一样的)
测试方法:console.log(source.prop())两个写法输出都一致。
2.设置节点类型(此处用自定义图片方式)
以下为官方提供的节点形状:
以下为图片形状的设计代码:
首先设置shape为image,让节点为图片状,而后在markup上注册image区域(因为有后续自定义需要所以定义了markup,不定义也可以,但是需删除markup字段)。
而后设置图片的路径,并且定义自定义标签,此处在外部定义label,会导致而后所有在markup上注册的text都为相同的标签,但是可以在attrs自定义标签,并用selector选择器进行选择即可解决。
import Ceyear4082PImg from '@/assets/InstrumentLibImage'
const node = graph.value.addNode({
position:{
x: 290,
y: 150,
},
size:{
width: 150,
height: 150,
},
shape: 'image',
imageUrl: Ceyear4082PImg,
label:'图片名',
markup: [
{
tagName: 'image',
selector: 'image',
},
{
tagName: 'text',
selector: 'label',
},
],
attrs: {
label:{
refX: 0.5,
refY: '100%',
refY2: 4,
textAnchor: 'middle',
textVerticalAnchor: 'top',
},
},
})
规范写法如下:
const node = graph.value.addNode({
position:{
x: 290,
y: 150,
},
size:{
width: 150,
height: 150,
},
shape: 'image',
markup: [
{
tagName: 'image',
selector: 'image',
},
{
tagName: 'text',
selector: 'label',
},
],
attrs: {
image: {
href: Ceyear4082PImg // 设置图片的 URL
},
label: {
text: "图片名", // 设置文本内容
refX: 0.5, // 文本相对于节点位置的 X 坐标
refY: '100%', // 文本相对于节点位置的 Y 坐标,100% 表示节点的底部
refY2: 4, // Y 坐标偏移量
textAnchor: 'middle', // 文本水平对齐方式
textVerticalAnchor: 'top', // 文本垂直对齐方式
},
},
})
上述有个小bug(当图片的高度不同的时候,label是显示在节点大小的底部,如果图片很矮则会间隔很大,改节点的大小则会显得很小。)
步骤三(实际案例):拖拽外部图片进入画布
实现原理:在图片上加入拖拽监听,在放置于画布区域的时候检测放入的内容,计算位置后生成对应节点。
图片区域加入draggable="true", @dragend="ondragEnd($event, item)",两个代码,首先让图片为可以拖拽,然后在拖拽结束调用自定义方法。
@dragover.prevent
是 Vue.js 中的指令,用于监听
dragover
事件并阻止其默认行为。
作用解释:
dragover
事件:- 当某个元素或节点正在被拖动,并且鼠标指针进入到某个目标元素上时,会触发dragover
事件。这个事件默认情况下会阻止元素作为拖拽目标的行为。.prevent
修饰符:- Vue.js 提供的.prevent
修饰符会调用event.preventDefault()
,即阻止默认行为。在dragover
事件中,默认行为是浏览器不允许该元素作为放置目标。- **为什么使用
@dragover.prevent
**:- 拖放操作中,目标元素必须明确表示它可以接受拖动的内容。默认情况下,dragover
事件是不会允许放置行为的,必须通过event.preventDefault()
来阻止默认行为,使目标元素能够正确接收拖动操作。- 比如,当你希望将某个元素拖动到目标区域时,需要通过@dragover.prevent
来告诉浏览器:该元素可以作为有效的拖放目标,从而允许你后续使用drop
事件进行拖放。
<el-image
:src="item.imgSrc"
style="object-fit: contain; cursor: grab;height: 100px;"
draggable="true"
@dragend="ondragEnd($event, item)"
></el-image>
<div id="container" @dragover.prevent></div>
拖拽后放置调用ondragEnd方法,获取当前拖动物体在页面的位置,并通过AntVX6画布的pageToLocal(...)将页面坐标转换为画布本地坐标(目前只实现以鼠标的位置为坐标系原点方法,若有更好的方法,欢迎讨论)。
注:HTML的坐标系是原点往负半轴延申,也就是常规坐标系的反着。
在拖拽放置的方法里调用添加节点方法,将传入的X、Y、图片信息,设置到添加节点方法里,实现拖拽功能。
//****拖拽后放置****//
const ondragEnd = (event:DragEvent,item:any) => {
const { x, y } = graph.value!.pageToLocal(event.pageX, event.pageY); // 将页面坐标转换为画布本地坐标。
addDragNode(x, y, item);
}
//****添加节点进画布****//
const addDragNode=(x:number,y:number,item:any)=>{
const node = graph.value!.addNode({
id:item.title,
position:{
x: x,
y: y,
},
size:{
width: 150,
height: 100,
},
shape: 'image',
markup: [
{
tagName: 'image',
selector: 'image',
},
{
tagName: 'text',
selector: 'label',
},
],
attrs: {
image:{
href:item.imgSrc
},
label: {
fontSize:10,
text: item.title, // 设置文本内容
refX: 0.5, // 文本相对于节点位置的 X 坐标
refY: '100%', // 文本相对于节点位置的 Y 坐标,100% 表示节点的底部
refY2: 1, // Y 坐标偏移量
},
},
})
}
步骤四(实际案例):画布内的节点相互连线
效果图如下
1.配置连接桩
要实现连线功能,首先要理解AntVX6的连接桩属性,在添加节点的时候,为节点配置上连接桩
官方解释:
首先我们将具有相同行为和外观的连接桩归为同一组,并通过
groups
选项来设置分组,该选项是一个对象
{ [groupName: string]: PortGroupMetadata }
,组名为键,值为每组连接桩的默认选项。
然后我们配置
items
,
items
是一个数组
PortMetadata[]
,数组的每一项表示一个连接桩,连接桩支持的选项如下。
个人理解:
group就是设置连接桩的属性,通过groups可以统一管理,记得要magnet: true,才可以进行连线,然后items是就是把设置好的连接桩放置在节点上(然后可以设置样式),个人认为也是相当于注册了一个HTML在节点上。
ports: {
groups: {
group1: {
position: {
name: 'left',
args: { x: 0, y: 0 },
},
attrs: {
circle: {
magnet: true, // 允许连线
stroke: '#8f8f8f', // 设置连接桩的样式
r: 5,
},
},
},
group2: {
position: {
name: 'right',
args: { x: 0, y: 0 },
},
attrs: {
circle: {
magnet: true, // 允许连线
stroke: '#8f8f8f', // 设置连接桩的样式
r: 5,
},
},
},
},
items: [
{
group: 'group1',
args: {
x: '60%',
y: 32,
angle: 45,
},
},
{
group: 'group2',
args: {
x: '60%',
y: 32,
angle: 45,
},
},
],
},
添加节点完整代码:
const addDragNode=(x:number,y:number,item:any)=>{
const node = graph.value!.addNode({
id:item.title,
position:{
x: x,
y: y,
},
size:{
width: 150,
height: 100,
},
shape: 'image',
markup: [
{
tagName: 'image',
selector: 'image',
},
{
tagName: 'text',
selector: 'label',
},
],
attrs: {
image:{
href:item.imgSrc
},
label: {
fontSize:10,
text: item.title, // 设置文本内容
refX: 0.5, // 文本相对于节点位置的 X 坐标
refY: '100%', // 文本相对于节点位置的 Y 坐标,100% 表示节点的底部
refY2: 1, // Y 坐标偏移量
},
},
ports: {
groups: {
group1: {
position: {
name: 'left',
args: { x: 0, y: 0 },
},
attrs: {
circle: {
magnet: true, // 允许连线
stroke: '#8f8f8f', // 设置连接桩的样式
r: 5,
},
},
},
group2: {
position: {
name: 'right',
args: { x: 0, y: 0 },
},
attrs: {
circle: {
magnet: true, // 允许连线
stroke: '#8f8f8f', // 设置连接桩的样式
r: 5,
},
},
},
},
items: [
{
group: 'group1',
args: {
x: '60%',
y: 32,
angle: 45,
},
},
{
group: 'group2',
args: {
x: '60%',
y: 32,
angle: 45,
},
},
],
},
})
}
2.在画布上设置连线交互
官方对连线的解释见链接:
https://x6.antv.antgroup.com/api/model/interaction#%E8%BF%9E%E7%BA%BF
连线交互的代码解释我已经写在注释,具体请看注释。
//连线交互
connecting: {
snap: {
radius: 50, //自动吸附,并设置自动吸附路径
},
allowBlank: false, // 是否允许连接到画布空白位置的点(就是能不能拉线连空白的地方)
allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点,就是能不能自我连线(箭头不能穿过仪器)
allowNode:false, //是否允许边连接到节点(非节点上的连接桩),默认为 true 。(就是要让它必须连接到连接桩,连接到节点不行)
allowEdge:false, //是否可以同一个起点终点,在箭头的线中间加一个箭头,就是一条线能一直加箭头
allowMulti: true, // 是否可以一个起点连多个终点
highlight: true, // 拖动边时,是否高亮显示所有可用的连接桩或节点,默认值为 false 。一般都会与 highlighting 联合使用。
},
//高亮器
highlighting:{
// 当连接桩可以被链接时,在连接桩外围渲染一个 2px 宽的红色矩形框
magnetAvailable: {
name: 'stroke',
args: {
padding: 4,
attrs: {
'stroke-width': 2,
stroke: 'red',
},
},
}
}
完整代码如下:
onMounted(() => {
graph.value = new Graph({
width: 1800,
height: 1200,
panning:true,
mousewheel:true,
background: {
color: '#F2F7FA',
},
container: document.getElementById('container')!, // 断言该值不为 null
grid: {
visible: true,
type: 'doubleMesh',
args: [
{
color: '#eee', // 主网格线颜色
thickness: 1, // 主网格线宽度
},
{
color: '#ddd', // 次网格线颜色
thickness: 1, // 次网格线宽度
factor: 4, // 主次网格线间隔
},
],
},
//连线交互
connecting: {
snap: {
radius: 50, //自动吸附,并设置自动吸附路径
},
allowBlank: false, // 是否允许连接到画布空白位置的点(就是能不能拉线连空白的地方)
allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点,就是能不能自我连线(箭头不能穿过仪器)
allowNode:false, //是否允许边连接到节点(非节点上的连接桩),默认为 true 。(就是要让它必须连接到连接桩,连接到节点不行)
allowEdge:false, //是否可以同一个起点终点,在箭头的线中间加一个箭头,就是一条线能一直加箭头
allowMulti: true, // 是否可以一个起点连多个终点
highlight: true, // 拖动边时,是否高亮显示所有可用的连接桩或节点,默认值为 false 。一般都会与 highlighting 联合使用。
},
//高亮器
highlighting:{
// 当连接桩可以被链接时,在连接桩外围渲染一个 2px 宽的红色矩形框
magnetAvailable: {
name: 'stroke',
args: {
padding: 4,
attrs: {
'stroke-width': 2,
stroke: 'red',
},
},
}
}
});
//画布开启对齐线功能
graph.value.use(new Snapline({
enabled:true
}))
// 监听节点的鼠标进入事件,显示连接桩
graph.value.on('node:mouseenter', () => {
changePortsVisible(true);
});
// 节点点击事件
graph.value.on('node:click', ({ node }) => {
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('node:mouseleave', () => {
changePortsVisible(false);
});
// 监听连线悬浮进入事件
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.setZIndex(99);
}
});
// 监听连线悬浮离开事件
graph.value.on('cell:mouseleave', ({ cell }) => {
if (cell.shape === 'edge') {
// 移除工具按钮
cell.removeTools();
// 恢复连线的颜色
cell.setAttrs({
line: {
stroke: 'black',
},
});
// 恢复连线的层级
cell.setZIndex(1);
}
});
// 将画布中元素缩小或者放大一定级别,让画布正好容纳所有元素,可以通过 maxScale 配置最大缩放级别
// graph.value.zoomToFit({ maxScale: 4 })
});
3.设置节点删除与连线删除
声明:以下代码改写于CSDN博主:先知demons
直接添加至项目即可,开箱即用,博主还在研究addTools,解释敬请期待....
const curSelectNode = ref<any>(null); // 当前选中的节点
//画布开启对齐线功能
graph.value.use(new Snapline({
enabled:true
}))
// 监听节点的鼠标进入事件,显示连接桩
graph.value.on('node:mouseenter', () => {
changePortsVisible(true);
});
// 节点点击事件
graph.value.on('node:click', ({ node }) => {
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('node:mouseleave', () => {
changePortsVisible(false);
});
// 监听连线悬浮进入事件
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.setZIndex(99);
}
});
// 监听连线悬浮离开事件
graph.value.on('cell:mouseleave', ({ cell }) => {
if (cell.shape === 'edge') {
// 移除工具按钮
cell.removeTools();
// 恢复连线的颜色
cell.setAttrs({
line: {
stroke: 'black',
},
});
// 恢复连线的层级
cell.setZIndex(1);
}
});
技术与工具分析:
工具一:对齐线工具
以下图片为官方的效果图,功能就是放置节点的时候有个对齐线。
实现步骤 :根据官方的描述,对齐线是移动节点排版的辅助工具,我们提供了一个独立的插件包
@antv/x6-plugin-snapline
来使用这个功能,所以先导包,将对应所需包导入。
npm install @antv/x6-plugin-snapline --save
具体代码如下,要在画布设置的时候将其添加进去。
graph.value.use(new Snapline({
enabled:true
}))
import { Snapline } from '@antv/x6-plugin-snapline'
const graph = ref<Graph | null>(null);
onMounted(() => {
graph.value = new Graph({
width: 1800,
height: 1200,
panning:true,
mousewheel:true,
background: {
color: '#F2F7FA',
},
container: document.getElementById('container')!, // 断言该值不为 null
grid: {
visible: true,
type: 'doubleMesh',
args: [
{
color: '#eee', // 主网格线颜色
thickness: 1, // 主网格线宽度
},
{
color: '#ddd', // 次网格线颜色
thickness: 1, // 次网格线宽度
factor: 4, // 主次网格线间隔
},
],
},
});
graph.value.use(new Snapline({
enabled:true
}))
// 将画布中元素缩小或者放大一定级别,让画布正好容纳所有元素,可以通过 maxScale 配置最大缩放级别
// graph.value.zoomToFit({ maxScale: 4 })
});
技术分析一:
节点如果设置了id属性,那么添加相同id的节点时候会添加失败,因为已存在相同的id。
文章持续更新,敬请期待.............
版权归原作者 I like Code? 所有, 如有侵权,请联系我们删除。