【前端】AntV X6学习笔记

作者 : admin 本文共12906个字,预计阅读时间需要33分钟 发布时间: 2024-06-10 共2人阅读

使用 AntV X6 版本 2.x,下面官网地址

Vue 节点 | X6 (antgroup.com)

path 路径节点,通过 svg 的 d 值来显示 svg

svg 在线编辑器_svg 矢量图在线制作工具-易点在线矢量图形编辑器 (wxeditor.com)

使用场景 | X6 (antgroup.com)

拖动一个图标去看它的源码里,复制 d 里面的值,放入 path 节点里的 path 属性值里面


path 节点

          {
            id: "node3",
            shape: "path",
            x: 180,
            y: 150,
            width: 40,
            height: 40,
            label: "svg图",
            path: "m193,160.31151l22.53615,0l6.96384,-19.86229l6.96385,19.86229l22.53615,0l-18.2321,12.27543l6.96421,19.86229l-18.23211,-12.27576l-18.2321,12.27576l6.96421,-19.86229l-18.2321,-12.27543",
            attrs: {
              body: {
                fill: "#efdbff",
                stroke: "#9254de",
              },
            },
          },

核心

引入的插件

import { Graph, Shape } from '@antv/x6' // 导入 @antv/x6 库中的 Graph 和 Shape 类
import { Stencil } from '@antv/x6-plugin-stencil' // 导入拖拽式元素选择插件
import { Transform } from '@antv/x6-plugin-transform' // 导入变换操作插件
import { Selection } from '@antv/x6-plugin-selection' // 导入选元素插件
import { Snapline } from '@antv/x6-plugin-snapline' // 导入图形对齐插件
import { Keyboard } from '@antv/x6-plugin-keyboard' // 导入键盘事件处理插件
import { Clipboard } from '@antv/x6-plugin-clipboard' // 导入复制和粘贴图形元素插件
import insertCss from 'insert-css' // 导入动态插入 CSS 函数
import { History } from '@antv/x6-plugin-history' // 导入图形编辑历史记录插件

第 1 步,绘制画布

initGraph() {
// 创建一个 Graph 实例,参数是一个配置对象
this.graph = new Graph({
container: document.getElementById('container'), // 画布容器
background: false, // 背景(透明)
autoResize: true, //自动更新大小
// 网格配置
grid: {
type: 'mesh', // 网格类型为网点(点阵)形式
size: 10, // 网格大小 10px
visible: true, // 渲染网格背景
args: {
color: '#eeeeee', // 网格线/点颜色
thickness: 2, // 网格线宽度/网格点大小
},
},
mousewheel: {
enabled: true, // 启用鼠标滚轮缩放功能
zoomAtMousePosition: true, // 缩放中心点在鼠标位置
modifiers: 'ctrl', // 按住 Ctrl 键进行缩放
minScale: 0.5, // 最小缩放比例
maxScale: 3, // 最大缩放比例
},
panning: {
enabled: true, // 允许平移功能
modifiers: 'alt', // 按住 Alt 键进行平移
},
// 配置连线规则
connecting: {
router: 'manhattan', // 连线的默认路由器为曼哈顿算法
connector: {
name: 'rounded', // 连接器形状为圆角
args: {
radius: 8, // 圆角半径
},
},
anchor: 'center', // 连线默认锚点为中心点
connectionPoint: 'anchor', // 连线默认连接点为锚点
allowBlank: false, // 不允许空连接
snap: {
radius: 20, // 连接吸附的半径范围
},
createEdge() {
// 创建连线时返回一个新的 Edge 对象
return new Shape.Edge({
attrs: {
line: {
stroke: '#A2B1C3', // 连线颜色
strokeWidth: 2, // 连线宽度
targetMarker: {
name: 'block', // 目标箭头形状为方块
width: 12, // 箭头宽度
height: 8, // 箭头高度
},
},
},
zIndex: 0, // 层级设置为 0
});
},
validateConnection({ targetMagnet }) {
// 验证连接是否有效,目标连接点必须存在
return !!targetMagnet;
},
},
highlighting: {
magnetAdsorbed: {
name: 'stroke', // 线条描边样式为高亮
args: {
attrs: {
fill: '#5F95FF', // 线条填充颜色
stroke: '#5F95FF', // 线条描边颜色
},
},
},
},
});
// 使用插件
this.graph
.use(
new Transform({
// 平移、缩放、旋转等变换操作
resizing: true, // 允许调整大小
rotating: true, // 允许旋转
})
)
.use(
new Selection({
// 选择和操纵图形。提供了选择区域、单选等功能
rubberband: true, // 启用选择框
showNodeSelectionBox: true, // 显示节点的选择框
})
)
.use(new Snapline()) // 可用于对齐图形元素
.use(new Keyboard()) // 允许通过键盘快捷键来控制图形编辑
.use(new Clipboard()) // 提供了对图形的复制和粘贴操作
.use(new History()); // 历史记录撤销使用
this.graph.centerContent(); // 画布内容居中显示
},

第 2 步,初始化连接桩

    //初始化默认连接桩
initPorts() {
this.ports = {
groups: {
top: {
position: 'top',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#5F95FF',
strokeWidth: 1,
fill: '#fff',
style: {
visibility: 'hidden',
},
},
},
},
right: {
position: 'right',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#5F95FF',
strokeWidth: 1,
fill: '#fff',
style: {
visibility: 'hidden',
},
},
},
},
bottom: {
position: 'bottom',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#5F95FF',
strokeWidth: 1,
fill: '#fff',
style: {
visibility: 'hidden',
},
},
},
},
left: {
position: 'left',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#5F95FF',
strokeWidth: 1,
fill: '#fff',
style: {
visibility: 'hidden',
},
},
},
},
},
items: [
{
group: 'top',
},
{
group: 'right',
},
{
group: 'bottom',
},
{
group: 'left',
},
],
}
},

第 3 步,注册自定义节点

enrollNode() {
// 注册自定义模板节点 'custom-image'
Graph.registerNode(
'custom-image',
{
width: 62, // 节点宽度
height: 62, // 节点高度
markup: [
{
tagName: 'rect',
selector: 'body', // 节点外部矩形
},
{
tagName: 'image',
},
{
tagName: 'text',
selector: 'label',
},
],
attrs: {
body: {
stroke: 'green', // 外部矩形边框颜色
fill: 'pink', // 外部矩形填充颜色
},
image: {
refWidth: '100%', // 图片宽度和节点宽度一致
refHeight: '100%', // 图片高度和节点高度一致
},
label: {
refX: 20,
refY: 82,
textAnchor: 'center',
textVerticalAnchor: 'bottom',
fontSize: 16,
fill: '#fff', // 文字颜色
},
},
ports: { ...this.ports }, // 节点的连接点配置
},
true // 是否强制覆盖同名节点类型
);
},

第四步, 初始化侧边栏

initStencil() {
// 创建一个 Stencil 实例
this.stencil = new Stencil({
title: '接线图', // Stencil 标题
target: this.graph, // 目标 Graph 实例
stencilGraphWidth: 200, // Stencil 绘图区宽度
stencilGraphHeight: 0, // Stencil 绘图区高度(0 表示自适应高度)
collapsable: true, // 是否可折叠
groups: [
// 定义分组
// {
//   title: "基础流程图",
//   name: "group1",
//   graphHeight: 50,
// },
// 系统设计图分组
{
title: '系统设计图',
name: 'group2',
graphHeight: 0, // 分组内部绘图区高度(0 表示自适应高度)
layoutOptions: {
rowHeight: 70, // 分组内部行高
},
},
],
layoutOptions: {
columns: 2, // 列数
columnWidth: 80, // 列宽度
rowHeight: 55, // 行高度
},
});
// 将 Stencil 实例的容器添加到指定的 DOM 元素中
document.getElementById('stencil').appendChild(this.stencil.container);
// 插入样式
this.insertCss();
},

第 4 步,绘制左侧栏

initStencil() {
// 创建一个 Stencil 实例
this.stencil = new Stencil({
title: '接线图', // Stencil 标题
target: this.graph, // 目标 Graph 实例
stencilGraphWidth: 200, // Stencil 绘图区宽度
stencilGraphHeight: 0, // Stencil 绘图区高度(0 表示自适应高度)
collapsable: true, // 是否可折叠
groups: [
// 定义分组
// {
//   title: "基础流程图",
//   name: "group1",
//   graphHeight: 50,
// },
// 系统设计图分组
{
title: '系统设计图',
name: 'group2',
graphHeight: 0, // 分组内部绘图区高度(0 表示自适应高度)
layoutOptions: {
rowHeight: 70, // 分组内部行高
},
},
],
layoutOptions: {
columns: 2, // 列数
columnWidth: 80, // 列宽度
rowHeight: 55, // 行高度
},
});
// 将 Stencil 实例的容器添加到指定的 DOM 元素中
document.getElementById('stencil').appendChild(this.stencil.container);
// 插入样式,修改了左侧栏底色
this.insertCss();
},

第 5 步,添加监听事件

initEvent() {
// 当鼠标进入节点时,显示连接点
this.graph.on('node:mouseenter', () => {
const container = document.getElementById('graph-container');
const ports = container.querySelectorAll('.x6-port-body');
this.showPorts(ports, true);
});
// 当鼠标离开节点时,隐藏连接点
this.graph.on('node:mouseleave', () => {
const container = document.getElementById('graph-container');
const ports = container.querySelectorAll('.x6-port-body');
this.showPorts(ports, false);
});
// 节点点击事件
this.graph.on('cell:click', ({ cell }) => {
// 清除之前选中节点的样式
this.curCel ? this.curCel.attr('body/stroke', null) : null;
this.curCel ? this.curCel.attr('line/stroke', '#c0c0c0') : '#c0c0c0';
// 设置当前选中节点的样式为红色
this.curCel = cell;
this.curCel.attr('body/stroke', 'red');
this.curCel.attr('line/stroke', 'red');
// 获取节点的文字标签(可能在text/text中,也可能在label/text中)
this.formData.nodeName = cell.getAttrs()?.text?.text
? cell.getAttrs()?.text?.text
: cell.getAttrs()?.label?.text
? cell.getAttrs()?.label?.text
: null;
// 获取节点的名称
this.formData.Name = cell.getAttrs()?.data?.Name;
});
// 快捷键与事件
this.graph.bindKey(['meta+c', 'ctrl+c'], () => {
// 复制选中的单元格
const cells = this.graph.getSelectedCells();
if (cells.length) {
this.graph.copy(cells);
}
return false;
});
this.graph.bindKey(['meta+x', 'ctrl+x'], () => {
// 剪切选中的单元格
const cells = this.graph.getSelectedCells();
if (cells.length) {
this.graph.cut(cells);
}
return false;
});
this.graph.bindKey(['meta+v', 'ctrl+v'], () => {
// 粘贴剪贴板中的内容
if (!this.graph.isClipboardEmpty()) {
const cells = this.graph.paste({ offset: 32 });
this.graph.cleanSelection();
this.graph.select(cells);
}
return false;
});
this.graph.bindKey('delete', () => {
// 删除选中的单元格
const cells = this.graph.getSelectedCells();
if (cells.length) {
this.graph.removeCells(cells);
}
});
this.graph.bindKey(['ctrl+1', 'meta+1'], () => {
// 放大画布
const zoom = this.graph.zoom();
if (zoom < 1.5) {
this.graph.zoom(0.1);
}
});
this.graph.bindKey(['ctrl+2', 'meta+2'], () => {
// 缩小画布
const zoom = this.graph.zoom();
if (zoom > 0.5) {
this.graph.zoom(-0.1);
}
});
this.graph.bindKey(['meta+z', 'ctrl+z'], () => {
// 撤销操作
if (this.graph.canUndo()) {
this.graph.undo();
}
return false;
});
this.graph.bindKey(['meta+shift+z', 'ctrl+shift+z'], () => {
// 重做操作
if (this.graph.canRedo()) {
this.graph.redo();
}
return false;
});
this.graph.bindKey(['meta+a', 'ctrl+a'], () => {
// 全选
const nodes = this.graph.getNodes();
if (nodes) {
this.graph.select(nodes);
}
});
},

第六步,加载数据

load(){
this.$http({
url: this.$http.adornUrl('/topoImg/loader/queryImgList'),
method: 'post',
data: this.$http.adornData({}),
}).then(({ data }) => {
if (data && data.code === 0) {
this.imgList = data.list
const encounteredKeys = {}
//相同名称的设备只取其一
for (let index = 0; index < this.imgList.length; index++) {
const item = this.imgList[index]
if (!encounteredKeys[item.imgKey]) {
encounteredKeys[item.imgKey] = true
this.imageNodes.push(
this.graph.createNode({  //调用自定义节点
shape: 'custom-image',
label: item.imgKey,
attrs: {
data: {
deviceName: '',
filePositionOptions: '下居中',
valueLamp: '0',
},
image: {
'xlink:href': item.imgContent.startsWith('img')
? this.$http.adornUrl('/') + item.imgContent
: item.imgContent,
},
},
})
)
}
}
this.imageNodes.push(
this.graph.createNode({
shape: 'custom-text',
attrs: {
data: {
deviceName: '',
filePositionOptions: '中居中',
valueLamp: '0',
},
image: {
'xlink:href': '',
},
},
})
)
// console.log("左侧的数据是", this.imageNodes);
this.stencil.load(this.imageNodes, 'group2')
} else {
this.imgList = []
}
this.load(this.receiveId)
})
}
//获取画布数据
load(id) {
if (!id) {
return
}
this.$http({
url: this.$http.adornUrl('/topo/loader/getInfo'),
method: 'post',
data: this.$http.adornData({
id: id,
}),
}).then(({ data }) => {
if (data && data.code === 0) {
if (data.data.x6json === undefined || data.data.x6json === '') {
return
}
this.graph.fromJSON(JSON.parse(data.data.x6json))
// let topojson = data.data.topojson;
// let x6json = data.data.x6json;
// console.log("---------------------X--");
// console.log("topojson值", JSON.parse(JSON.parse(topojson)));
// console.log("-------------X----------");
// console.log("x6json值", JSON.parse(x6json));
//下面先遍历页面的图,然后再去遍历连接线
// nodeS.nodeDataArray.map((item) => {
//   const [x, y] = item.loc.split(" ").map((value) => parseInt(value));
//   // console.log("x和y的值是", x, y);
//   this.graph.addNode({
//     shape: "custom-image",
//     label: item.imgKey,
//     id: item.key,
//     x: x,
//     y: y,
//     width: item.width,
//     height: item.height,
//     data: {
//       size: item.size,
//       loc: item.loc,
//       imgKey: item.imgKey,
//       valContent: item.valContent,
//     },
//     attrs: {
//       image: {
//         "xlink:href": item.source.startsWith("img")
//           ? this.$http.adornUrl("/") + item.source
//           : item.source,
//       },
//     },
//   });
// });
//添加连接线
// nodeS.linkDataArray.map((item) =>
//   this.graph.addEdge({
//     source: { cell: item.from },
//     target: { cell: item.to },
//     attrs: {
//       line: {
//         stroke: "#c0c0c0",
//         strokeWidth: 2,
//         targetMarker: {
//           name: "block",
//           width: 12,
//           height: 8,
//         },
//       },
//     },
//     zIndex: 0,
//   })
// );
}
})
},

补充

    //修改画布内置的样式,正常css无法修改
insertCss() {
return insertCss(`
#container {
display: flex;
border: 1px solid #dfe3e8;
}
#stencil {
width: 180px;
height: 100%;
position: relative;
border-right: 1px solid #dfe3e8;
}
#graph-container {
width: calc(100% - 180px);
height: 100%;
}
.x6-widget-stencil  {
background-color: #fff;
}
.x6-widget-stencil-title {
background-color: #fff;
}
.x6-widget-stencil-group-title {
background-color: #fff !important;
}
.x6-widget-transform {
margin: -1px 0 0 -1px;
padding: 0px;
border: 1px solid #239edd;
}
.x6-widget-transform > div {
border: 1px solid #239edd;
}
.x6-widget-transform > div:hover {
background-color: #3dafe4;
}
.x6-widget-transform-active-handle {
background-color: #3dafe4;
}
.x6-widget-transform-resize {
border-radius: 0;
}
.x6-widget-selection-inner {
border: 1px solid #239edd;
}
.x6-widget-selection-box {
opacity: 0;
}
`)
},
props: ['id'],
mounted() {
this.receiveId = this.$props.id
this.initGraph() //初始化画布
this.initPorts() //初始化连接桩
this.enrollNode() //注册自定义节点
this.initStencil() //初始化左侧栏ui
this.initEvent() //添加监听事件
this.queryApiList() //查找接口列表
this.loadData() //加载数据
},
<!-- 下面是节点属性框 -->
<div v-if="drawer" class="node_attr_box">
<div style="font-size: 20px; padding: 10px 5px; background-color: slategray">
<span>组态属性</span>
<span style="float: right" class="close_icon"><i class="el-icon-close" @click="drawer = false"></i></span>
</div>
<el-form :model="nodeForm">
<h3>节点设置</h3>
<el-form-item label="设备ID" prop="idNum">
<el-input v-model="nodeForm.idNum" size="mini" style="width: 100px"></el-input>
</el-form-item>
<hr />
<h3>文本设置</h3>
<el-form-item label="文本" prop="nodeName">
<el-input v-model="nodeForm.nodeName" @change="onNameChange" size="mini" style="width: 100px"></el-input>
</el-form-item>
</el-form>
<hr />
</div>

遇到问题

gojs和x6的图形显示不符合预期

在 X6 中,网格是渲染/移动节点的最小单位,默认是 10px ,也就是说位置为 { x: 24, y: 38 } 的节点渲染到画布后的实际位置为 { x: 20, y: 40 }

      // 构建画布
myDiagram = $(
sx.Diagram,
'myDiagramp', // 必须指定或引用 HTML 元素
{
// 是否显示网格
grid: $(
sx.Panel,
'Grid',
$(sx.Shape, 'LineH', { stroke: 'lightgray', strokeWidth: 0.5 }), // 水平辅助线,浅灰色
$(sx.Shape, 'LineH', {
stroke: 'gray',
strokeWidth: 0.5,
interval: 10,
}), // 水平主要线,灰色,每隔10个单位绘制一条
$(sx.Shape, 'LineV', { stroke: 'lightgray', strokeWidth: 0.5 }), // 垂直辅助线,浅灰色
$(sx.Shape, 'LineV', {
stroke: 'gray',
strokeWidth: 0.5,
interval: 10,
}) // 垂直主要线,灰色,每隔10个单位绘制一条
),
// 只读模式
isReadOnly: false,
// 初始化画布缩放比例
scale: 0.7,
allowDrop: true, // 允许从工具栏拖拽节点到画布
allowZoom: true, // 允许缩放
'draggingTool.dragsLink': true, // 拖拽节点时是否同时移动连线
'draggingTool.isGridSnapEnabled': true, // 拖拽节点时是否吸附到网格
'linkingTool.isUnconnectedLinkValid': true, // 允许创建未连接的连线
'linkingTool.portGravity': 20, // 连线吸附到节点端口的力度
'relinkingTool.isUnconnectedLinkValid': true, // 允许重新连接未连接的连线
'relinkingTool.portGravity': 20, // 重新连接连线时吸附到节点端口的力度
'relinkingTool.fromHandleArchetype': $(sx.Shape, 'Diamond', {
segmentIndex: 0,
cursor: 'pointer',
desiredSize: new sx.Size(8, 8),
fill: 'tomato',
stroke: 'darkred',
}), // 重新连接连线时起始点的手柄样式
'relinkingTool.toHandleArchetype': $(sx.Shape, 'Diamond', {
segmentIndex: -1,
cursor: 'pointer',
desiredSize: new sx.Size(8, 8),
fill: 'darkred',
stroke: 'tomato',
}), // 重新连接连线时结束点的手柄样式
'linkReshapingTool.handleArchetype': $(sx.Shape, 'Diamond', {
desiredSize: new sx.Size(7, 7),
fill: 'lightblue',
stroke: 'deepskyblue',
}), // 调整连线形状时的手柄样式
rotatingTool: $(TopRotatingTool), // 自定义旋转工具
'rotatingTool.snapAngleMultiple': 15, // 旋转时对齐到的角度倍数
'rotatingTool.snapAngleEpsilon': 15, // 角度对齐的容差范围
'undoManager.isEnabled': true, // 启用撤销/重做功能
}
)

同步gojs中渲染的表格,也是10px,现在考虑解决x6缩放问题

graph.resize(800, 600) // resize 画布大小
graph.translate(20, 20) // 在 x、y 方向上平移画布
graph.zoom(0.2) // 将画布缩放级别增加 0.2(默认为1)
graph.zoom(-0.2) // 将画布缩放级别减少 0.2
graph.zoomTo(1.2) // 将画布缩放级别设置为 1.2
// 将画布中元素缩小或者放大一定级别,让画布正好容纳所有元素,可以通过 maxScale 配置最大缩放级别
graph.zoomToFit({ maxScale: 1 })
graph.centerContent() // 将画布中元素居中展示

加入了zoom后,差不多比例了

接着研究线的坐标,发现是base64图片有偏移,保存时候添加了,解决了这个问题。又发现了新的问题,还是线的问题,图片与图片直连,两边都可以,就是旋转之后,连接线不匹配。研究线

本站无任何商业行为
个人在线分享 » 【前端】AntV X6学习笔记
E-->