AntV X6 在vue3项目中的实践

8,971 阅读6分钟

官方文档写的真的是很简洁啊,有些方法属性好难找,得一边写一边摸索x6.antv.vision/zh/docs/tut…

安装

# npm
$ npm install @antv/x6 --save

# yarn
$ yarn add @antv/x6

安装完成之后,使用 importrequire 进行引用。

import { Graph } from '@antv/x6';

开始使用

Step 1 创建容器

<div class="content">
   <div class="app-stencil" ref="stencilContainer"></div>
   <div class="app-content" id="flowContainer" ref="container"></div>
</div>

Step 2 准备数据

在我们真实开发中,初始画布肯定是不需要这些假数据的,我之所以给列在这里,是为了清楚它的数据格式,这样在跟后端调接口的时候,可以一起定义数据格式,方便双方开发。尤其是在做回显时,从接口拿数据进行渲染画布时,直接使用这种格式就可以了。我会在后面列出我开发时是怎么运用的,以下是官方文档提供的事例:

X6 支持 JSON 格式数据,该对象中需要有节点 nodes 和边 edges 字段,分别用数组表示:

const data = {
  // 节点
  nodes: [
    {
      id: 'node1', // String,可选,节点的唯一标识
      x: 40,       // Number,必选,节点位置的 x 值
      y: 40,       // Number,必选,节点位置的 y 值
      width: 80,   // Number,可选,节点大小的 width 值
      height: 40,  // Number,可选,节点大小的 height 值
      label: 'hello', // String,节点标签
    },
    {
      id: 'node2', // String,节点的唯一标识
      x: 160,      // Number,必选,节点位置的 x 值
      y: 180,      // Number,必选,节点位置的 y 值
      width: 80,   // Number,可选,节点大小的 width 值
      height: 40,  // Number,可选,节点大小的 height 值
      label: 'world', // String,节点标签
    },
  ],
  // 边
  edges: [
    {
      source: 'node1', // String,必须,起始节点 id
      target: 'node2', // String,必须,目标节点 id
    },
  ],
};

Step 3 渲染画布

首先,我们需要创建一个 Graph 对象,并为其指定一个页面上的绘图容器,通常也会指定画布的大小。

import { Graph, Shape, Addon, FunctionExt } from '@antv/x6'// 使用 CDN 引入时暴露了 X6 全局变量
// const { Graph } = X6

export default defineComponent({
   const imageShapes = [  //左侧拖拽样式      {        body: {          fill: "#EFF4FF",          stroke: "#5F95FF",        },        label: {          text: state.collectLabel,          fill: '#5F95FF',        },        image: require('/src/assets/Scheduler/rowCook.svg'),        type:'ruleSet', //每一个都自定义一个type      },      {        // ...... 省略其他      },  ]  
  let graph = null;
  const init= () => {
      graph = new Graph({ // 新建画布
        container: document.getElementById('flowContainer'),
        grid: true,
        scroller: {
          enabled: true,
          pageVisible: false,
          pageBreak: false,
        },
        snapline: {
          enabled: true,
          sharp: true,
        },
        mousewheel: {
          enabled: true,
          modifiers: ["ctrl", "meta"],
          minScale: 0.5,
          maxScale: 2,
        },
        // 画布调整
        selecting: {
          enabled: true,
          multiple: true,
          rubberband: true,
          movable: true,
          showNodeSelectionBox: true,
        },
        // 连线规则
        connecting: {
          snap: true,  // 当 snap 设置为 true 时连线的过程中距离节点或者连接桩 50px 时会触发自动吸附
          allowBlank: false,  // 是否允许连接到画布空白位置的点,默认为 true
          allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点,默认为 true 
          allowMulti: false, // 当设置为 false 时,在起始和终止节点之间只允许创建一条边
          highlight: true,  // 拖动边时,是否高亮显示所有可用的连接桩或节点,默认值为 false。
          sourceAnchor: {  // 当连接到节点时,通过 sourceAnchor 来指定源节点的锚点。
            name: 'bottom',
            args: {
              dx: 0,
            },
          },
          targetAnchor: {  // 当连接到节点时,通过 targetAnchor 来指定目标节点的锚点。
            name: 'top',
            args: {
              dx: 0,
            },
          },
          connectionPoint: 'anchor',  // 指定连接点,默认值为 boundary。
          connector: 'algo-edge',  // 连接器将起点、路由返回的点、终点加工为 元素的 d 属性,决定了边渲染到画布后的样式,默认值为 normal。
          createEdge() {
            return graph.createEdge({
              attrs: {
                line: {
                  strokeDasharray: '5 5',
                  stroke: '#808080',
                  strokeWidth: 1,
                  targetMarker: {
                    name: 'block',
                    args: {
                      size: '6',
                    },
                  },
                },
              },
            })
          },
          validateMagnet({ magnet }) {
            return magnet.getAttribute('port-group') !== 'in'
          },
          validateConnection({
            sourceView,
            targetView,
            sourceMagnet,
            targetMagnet
          }) {
            if (sourceView === targetView) {
              return false;
            }
            if (!sourceMagnet) {
              return false;
            }
            // 只能连接到输入链接桩 
           if (
              !targetMagnet ||
              targetMagnet.getAttribute("port-group") !== "in" 
           ) {
              return false;
            }
            return true;
          },          // 当停止拖动边的时候根据 validateEdge 返回值来判断边是否生效,如果返回 false, 该边会被清除。
          validateEdge({ edge }) {
            const { source, target } = edge
            return true
          }
        },
      });
      // graph.isPannable() // 画布是否可以平移
      // graph.enablePanning() // 启用画布平移
      graph.centerContent();
      /******************************** 左侧模型栏 ****************************/
      const stencil = new Stencil({
        title: "数据集成",
        target: graph,
        search: false, // 搜索
        collapsable: true,
        stencilGraphWidth: 300,
        stencilGraphHeight: 600,
        groups: [
          {
            name: "processLibrary",
            title: "dataSource",
          },
        ],
        layoutOptions: {
          dx: 30,
          dy: 20,
          columns: 1,
          columnWidth: 130,
          rowHeight: 100,
        },
      });
      proxy.$refs.stencilContainer.appendChild(stencil.container)
      // 初始化图形
      const ports = {
        groups: {
          in: {
            position: 'top',
            attrs: {
              circle: {
                r: 4,
                magnet: true,
                stroke: '#108ee9',
                strokeWidth: 2,
                fill: '#fff',
                style: {
                  visibility: "hidden",
                },
              }
            }
          },
          out: {
            position: 'bottom',
            attrs: {
              circle: {
                r: 4,
                magnet: true,
                stroke: '#31d0c6',
                strokeWidth: 2,
                fill: '#fff',
                style: {
                  visibility: "hidden",
                },
              }
            }
          }
        },
        items: [ //后缀添加in和out是因为弄了上下两个链接桩,在回显接口返回的数据时,可以正确显示连线的上下连接位置
          {
            id: state.currentCode + '_in',
            group: 'in',
          },
          { 
           id: state.currentCode + '_out', 
           group: 'out',
          },
        ], 
     }
      //设计画布左侧节点样式
      Graph.registerNode(
        'custom-node',
        {
          inherit: 'rect',
          width: 140,
          height: 76,
          attrs: {
            body: {
              strokeWidth: 1,
            },
            image: {
              width: 16,
              height: 16,
              x: 12,
              y: 6,
            },
            text: {
              refX: 40,
              refY: 15,
              fontSize: 15,
              'text-anchor': 'start',
            },
            label: {
              text: 'Please nominate this node',
              refX: 10,
              refY: 30,
              fontSize: 12,
              fill: 'rgba(0,0,0,0.6)',
              'text-anchor': 'start',
              textWrap: {
                width: -10,      // 宽度减少 10px
                height: '70%',   // 高度为参照元素高度的一半
                ellipsis: true,  // 文本超出显示范围时,自动添加省略号
                breakWord: true, // 是否截断单词
              }
            },
          },
          markup: [
            {
              tagName: 'rect',
              selector: 'body',
            },
            {
              tagName: 'image',
              selector: 'image',
            },
            {
              tagName: 'text',
              selector: 'text',
            },
            { 
              tagName: 'text',
              selector: 'label',
            },
          ],
          data: {},
          relation: {},
          ports: { ...ports },
        },
        true,
      )
      const imageNodes = imageShapes.map((item) =>
        graph.createNode({
          shape: 'custom-node',
          attrs: {
            image: {
              'xlink:href': item.image,
            },
            body: item.body,
            text: item.label,
          },
        }),
      ) 
     stencil.load(
        imageNodes,
        "processLibrary"
      );
      graph.toJSON()
      //绑定事件
      graph.on('node:added', ({ node }) => {
         state.currentCode = node.id //每个节点都有一个唯一id 
      })
      // 鼠标进入节点-节点显示连接桩
      graph.on("node:mouseenter",FunctionExt.debounce(() => {
           const ports = container.querySelectorAll(".x6-port-body");
           showPorts(ports, true);
         }),
         500
      );
      // 鼠标进入节点-节点删除操作 
      graph.on("node:mouseenter", ({ node }) => {
           node.addTools({
               name: "button-remove",
               args: {
                  x: 0,
                  y: 0,
                  offset: { x: 10, y: 10 },
                },
            });
      });
      // 鼠标离开节点   
      graph.on("node:mouseleave", ({ node }) => { 
          const ports = container.querySelectorAll(".x6-port-body"); 
          showPorts(ports, false); //隐藏连接桩  
          node.removeTools(); // 隐藏删除按钮
      });  
     // 鼠标进入线-线删除操作 
     graph.on("edge:mouseenter", ({ edge }) => {  
        edge.addTools([
            "target-arrowhead",
                {
                    name: "button-remove",
                    args: {
                        distance: -30,
                    },
                },
             ]); 
      }); 
     //这个方法我没用上,知道的小伙伴也可以告诉我连线的删除
     graph.on("edge:removed", ({ edge, options }) => {   
        // if (!options.ui) { 
            // return;
        // } 
       // const cellId = edge.getTargetCellId()    
       // const target = graph.getCellById(cellId)     
       // target && target.setPortProp(target.id, 'connected', false)
      });  
     // 鼠标离开线-线删除操作   
     graph.on("edge:mouseleave", ({ edge }) => {  
          edge.removeTools(); 
     });        
     graph.on('node:change:data', ({node}) => {    
       node.data = eachNodeData   
     })     
     graph.bindKey("backspace", () => {   
        const cells = graph.getSelectedCells();
        if (cells.length) {    
            graph.removeCells(cells); 
        }
      });      
     //双击节点打开节点配置 --- 最主要用到的方法     
     graph.on("cell:dblclick", ({ node, cell }) => {  
        const typeList = graph.getNodes().map(x => ({ //拿到所有节点 type\id\name           
            code:x.id,      
            type:x.store.data.attrs.type,  
            name:x.store.data.attrs.label.text      
        })) 
        const relationShip = graph.getEdges().map(x => ({  //获取连线关系          
             child:x.target.cell,  
             parent:x.source.cell        
        }))               
        //寻找当前节点是否有连线且拿到父节点          
       let fatherCode = []         
       relationShip.map((item)=>{          
          if(item.child==node.id){            
            fatherCode.push(item.parent)         
           }        
        })                
        // 双重for循环拿到父节点信息         
       let result = []          
       for(let i = 0; i < fatherCode.length; i++) {              
          let tempArr1 = fatherCode[i]      
          for(let j = 0; j < typeList.length; j++) {       
              let tempArr2 = typeList[j]             
              if(tempArr2.code == tempArr1){           
                 result.push(tempArr2)       
                     break;      
               }           
          }             
        }   
        state.resultShip=result; //父节点集合 这个是因为我们的连线之间存在父子关系,需要用父子关系来控制显示对应的数据,没这个需求的不用加上述操作               
        state.currentCode = node.id //当前节点id        
        //打开对应弹框 可以用text判断,也可以用自定义的type去区分           
        let text = node.getAttrs().text.text             
           if(text=='规则集'){        
              state.visibleRule=true;   
              if(allData.ruleSetList.length>=1){            
                let idx= allData.ruleSetList.findIndex((itm) => itm.code == node.id)              
                if(idx > -1){           
                   state.currentRuleSet=allData.ruleSetList[idx]  
                 }else{            
                   state.currentRuleSet={}       
                 }     
                }     
           }else if(text=='决策'){      
              state.visibleTactics=true;       
             // ............. 省略                      
           }else if(text=='表达式'){      
              state.visibleExpression=true;            
              // ............. 省略      
          }    
        })
        //删除节点及已经保存的弹框数据 --- 最主要用到的方法
        graph.on("node:removed", ({ node, options }) => {      
          if(node.store.data.attrs.type=="expression"){            
             let idx= allData.expressionList.findIndex((itm) => itm.code== node.id)    
             if(idx > -1){         
               allData.expressionList.splice(idx,1)    
              }        
           }else if(node.store.data.attrs.type=="ruleSet"){    
                //删除规则集         
               // ............. 省略            
           }else if(node.store.data.attrs.type=="plot"){          
              //删除策略           
             // ............. 省略             
           }                   
        })            
      // 控制连接桩显示/隐藏   
     const showPorts = (ports, show) => {        
        for (let i = 0, len = ports.length; i < len; i = i + 1) {         
          ports[i].style.visibility = show ? "visible" : "hidden";    
        } 
     }
                                        
})

Step 4 根据接口回显画布

const getData = () =>{   
    const cells = []        
    const location = JSON.parse(state.canvasPos)          
    //节点        
    location.locationList.map((data)=>{        
       cells.push(       
          graph.addNode({             
            id: data.nodeCode,          
            x: data.axisX,                 
            y: data.axisY,           
            shape: 'custom-rect',                
            attrs: {                   
               label: {                       
                  text: data.nodeName,    
                },                
                text: {       
                    text: data.paramType === "expression" ? "表达式" : data.paramType === "ruleSet" ? "规则集" : "决策",                       
                    fill: '#5F95FF',             
                 },               
                 body: {                 
                    fill: "#EFF4FF",        
                    stroke: "#5F95FF",              
                  },                   
                 type:data.paramType,          
              }     
         }),         
       )        
    })         
   //连线        
   location.relationList.map((data)=>{       
       cells.push(       
         graph.addEdge({             
           source: {cell:data.parentNodeDTO.parentCode,port:data.parentNodeDTO.parentFlag  },          
           target: {cell:data.childNodeDTO.childCode,port:data.childNodeDTO.childFlag},           
             attrs: {               
               line: {           
                   stroke: '#A2B1C3',                
                   strokeWidth: 2,                      
                   targetMarker: {           
                        name: 'block',           
                        width: 12,               
                        height: 8,                
                },                
             },              
           },                
           zIndex: 0,             
           shape: 'edge',        
           connector: {        
                name: 'rounded',                
                args: {                
                  radius: 8,            
                },               
            },                
            anchor: 'center',              
            connectionPoint: 'anchor',               
        }),   
    )   
  })          
  graph.resetCells(cells)          
  graph.centerContent()       
}