使用 AntV X6 版本 2.x,下面官网地址
Vue 节点 | X6 (antgroup.com)
path 路径节点,通过 svg 的 d 值来显示 svg
svg 在线编辑器_svg 矢量图在线制作工具-易点在线矢量图形编辑器 (wxeditor.com)
使用场景 | X6 (antgroup.com)
拖动一个图标去看它的源码里,复制 d 里面的值,放入 path 节点里的 path 属性值里面
1 <path id="svg_1" d="m199.50248,160.86426l0.19807,-0.63855l0.80193,-0.28418l0.80193,0.28418l0.19807,0.63855l-0.55497,0.51208l-0.89007,0l-0.55497,-0.51208z" stroke-width="1.5" stroke="#000" fill="#fff"/>
path 节点 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { 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", }, }, },
核心 引入的插件 1 2 3 4 5 6 7 8 9 10 import { Graph , Shape } from '@antv/x6' 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' import { History } from '@antv/x6-plugin-history'
第 1 步,绘制画布 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 initGraph ( ) { this .graph = new Graph ({ container : document .getElementById ('container' ), background : false , autoResize : true , grid : { type : 'mesh' , size : 10 , visible : true , args : { color : '#eeeeee' , thickness : 2 , }, }, mousewheel : { enabled : true , zoomAtMousePosition : true , modifiers : 'ctrl' , minScale : 0.5 , maxScale : 3 , }, panning : { enabled : true , modifiers : 'alt' , }, connecting : { router : 'manhattan' , connector : { name : 'rounded' , args : { radius : 8 , }, }, anchor : 'center' , connectionPoint : 'anchor' , allowBlank : false , snap : { radius : 20 , }, createEdge ( ) { return new Shape .Edge ({ attrs : { line : { stroke : '#A2B1C3' , strokeWidth : 2 , targetMarker : { name : 'block' , width : 12 , height : 8 , }, }, }, zIndex : 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 步,初始化连接桩 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 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 步,注册自定义节点 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 enrollNode ( ) { 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 ); },
第四步, 初始化侧边栏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 initStencil ( ) { this .stencil = new Stencil ({ title : '接线图' , target : this .graph , stencilGraphWidth : 200 , stencilGraphHeight : 0 , collapsable : true , groups : [ { title : '系统设计图' , name : 'group2' , graphHeight : 0 , layoutOptions : { rowHeight : 70 , }, }, ], layoutOptions : { columns : 2 , columnWidth : 80 , rowHeight : 55 , }, }); document .getElementById ('stencil' ).appendChild (this .stencil .container ); this .insertCss (); },
第 4 步,绘制左侧栏 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 initStencil ( ) { this .stencil = new Stencil ({ title : '接线图' , target : this .graph , stencilGraphWidth : 200 , stencilGraphHeight : 0 , collapsable : true , groups : [ { title : '系统设计图' , name : 'group2' , graphHeight : 0 , layoutOptions : { rowHeight : 70 , }, }, ], layoutOptions : { columns : 2 , columnWidth : 80 , rowHeight : 55 , }, }); document .getElementById ('stencil' ).appendChild (this .stencil .container ); this .insertCss (); },
第 5 步,添加监听事件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 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' ); 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); } }); },
第六步,加载数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 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' : '' , }, }, }) ) 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 )) } }) },
补充 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 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 () 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 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 myDiagram = $( sx.Diagram , "myDiagramp" , { grid : $( sx.Panel , "Grid" , $(sx.Shape , "LineH" , { stroke : "lightgray" , strokeWidth : 0.5 }), $(sx.Shape , "LineH" , { stroke : "gray" , strokeWidth : 0.5 , interval : 10 , }), $(sx.Shape , "LineV" , { stroke : "lightgray" , strokeWidth : 0.5 }), $(sx.Shape , "LineV" , { stroke : "gray" , strokeWidth : 0.5 , interval : 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 缩放问题
1 2 3 4 5 6 7 8 graph.resize (800 , 600 ) graph.translate (20 , 20 ) graph.zoom (0.2 ) graph.zoom (-0.2 ) graph.zoomTo (1.2 ) graph.zoomToFit ({ maxScale : 1 }) graph.centerContent ()
加入了 zoom 后,差不多比例了
接着研究线的坐标,发现是 base64 图片有偏移,保存时候添加了,解决了这个问题。又发现了新的问题,还是线的问题,图片与图片直连,两边都可以,就是旋转之后,连接线不匹配。研究线
常用api https://x6.antv.antgroup.com/api/model/cell
节点/边渲染到画布后可以通过
cell.isNode()
cell.getZIndex()
cell.setZIndex(z: number) 来获取或设置 zIndex的值
cell.toFront()
cell.toBack() 来将其移到最顶层或最底层
cell.attrs 获取属性 要是 = 就是设置新的属性
cell.getAttrs() 获取属性
setAttrs(attrs: Attr.CellAttrs, options?: Cell.SetAttrOptions) 设置属性
默认深merge,值全部替换,如果原先有body,替换的没有,body也会消失
1 2 3 4 cell.setAttrs ({ body : { fill : '#f5f5f5' }, label : { text : 'My Label' }, })
浅的则body捕获消失,只替换替换的值
1 cell.setAttrs ({ label : { text : 'My Label' } }, { deep : false })
replaceAttrs(…) 相当于刚刚上面那个身
updateAttrs(…) 相当于浅的
removeAttrs(…) 删除属性
cell.getAttrByPath() 路径为空时返回全部属性
cell.getAttrByPath(‘body’) 通过字符串路径获取属性值
还可以 [‘body’]
setAttrByPath(…) 根据属性路径设置属性值
cell.setAttrByPath(‘body’, { stroke: ‘#000000’ }) // 替换 body 属性值 深
cell.setAttrByPath(‘body/fill’, ‘#f5f5f5’) // 设置 body.fill 属性值 浅保留原
cell.removeAttrByPath(‘body/fill’) 通过字符串路径删除属性值
attr(…)该方法是 getAttrByPath
、setAttrByPath
和 setAttrs
三个方法的整合,提供了上面四种函数签名,是一个非常实用的方法。
cell.attr()
cell.attr(‘line/stroke’, ‘#c0c0c0’)
cell.attr(‘body/fill’)
1 2 3 4 5 6 7 8 9 10 11 const rect = new Shape.Rect({ x: 40, y: 40, width: 100, height: 40, data: { bizID: 125, date: '20200630', price: 89.0, }, })
data声明在和宽高同级别
cell.getData() 获取这个data里的对象
cell.setData() 设置 设置关联的数据,并触发 change:data
事件和画布重绘
cell.setData(data) 默认深merge
cell.setData(data, { overwrite: true }) 替换旧数据
cell.setData(data, { deep: false }) 浅
1 2 3 4 5 6 7 8 9 10 const obj = { name : 'x6' , star : true }node.setData (obj) obj.star = false node.setData (obj) node.setData ({ ...obj, star : false , })
replaceData(…) 用指定的数据替换原数据,相当于调用 setData(data, { ...options, overwrite: true })
。
updateData(…) 通过浅 merge 来更新数据,相当于调用 setData(data, { ...options, deep: false })
。
removeData(…) 删除数据。默认情况触发 change:data
事件和画布重绘,
getParent() …. 获取父节点。暂时没用到
节点属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const rect = new Shape .Rect ({ x : 30 , y : 30 , width : 100 , height : 40 , attrs : {...}, data : {...}, zIndex : 10 , sale : {...}, product : { id : '1234' , name : 'apple' , price : 3.99 , }, })
上面代码中的 attrs
、data
、zIndex
都是标准的属性,其中 x
和 y
是一对自定义选项 ,节点初始化时被转换为了 position
属性,同样 width
和 height
也是一对自定义选项 ,节点初始化时被转换为了 size
属性,最后剩余的 sale
和 product
两个对象是非标准的属性。
1 2 3 4 5 6 7 const zIndex = node.getProp ('zIndex' )const position = node.getProp ('position' )node.getProp ('size' ) const product = node.getProp ('product' )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 node.setProp ('size' , { width : 100 , height : 30 }) node.setProp ('zIndex' , 10 ) node.setProp ({ size : { width : 100 , height : 30 , }, zIndex : 10 , }) rect.removeProp ('zIndex' ) rect.removeProp ('product/id' )
prop()是上面的整合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 rect.prop () rect.prop ('zIndex' ) rect.prop ('product/price' ) rect.prop ('zIndex' , 10 ) rect.prop ('product/price' , 5.99 ) rect.prop ({ product : { id : '234' , name : 'banana' , price : 3.99 , }, })