Vue项目中使用AntV X6绘制流程图

一、需求

  • Vue2.xVue3.x项目同理)项目中使用AntV X6组件库绘制流程图,需要实现以下需求:
  • 需求1:左侧菜单中的模块可以拖拽进入画布中生成对应的流程图模块
  • 需求2:流程图中的节点之间可以进行连线交互
  • 需求3:点击对应的节点后可以进行操作节点(删除、查看节点的相关信息参数)
  • 需求4:鼠标悬浮在连线上时可以删除当前连线
  • 隐含需求:节点样式需要满足UI设计,所以需要自定义节点样式
  • 关于AntV X6是什么组件库,可以看X6简介
  • 该项目demo的仓库地址在章末

二、解决

  • 首先分析需求,通过AntV X6组件给出的文档和API是可以满足以上需求的,以下以Vue2.x项目中使用AntV X6并满足相应需求为例,讲述AntV X6使用,帮助初学者快速上手,后文中使用x6代替AntV X6

1.安装X6组件库

  • 搭建Vue的项目后就可以安装x6了,执行命令npm install @antv/x6 --save,详见文档X6快速上手

2.使用x6组件库

  • 安装好x6之后就可以直接使用了,找到需要使用x6的界面中引入Graph

    import { Graph } from '@antv/x6'
    
  • 在需要的页面中引入后即可开始初始化画布,初始化画布函数代码如下:

    HomeView.vue
    ...
    <div id="container"></div>
    ...
    
    <script>
        import { Graph } from '@antv/x6'
        export default {
            data() {
                return {
                    ...
                    graph: null // 画布实例对象
                    ...
                }
            }
            mounted() {
                this.initGraph()
            },
            methods: {
                // 初始化流程图画布
                initGraph() {
                    let container = document.getElementById('container')
                    this.graph = new Graph({
                        container: container, // 画布容器
                        width: container.offsetWidth, // 画布宽
                        height: container.offsetHeight, // 画布高
                        background: false, // 背景(透明)
                        snapline: true, // 对齐线
                        // 配置连线规则
                        connecting: {
                            snap: true, // 自动吸附
                            allowBlank: false, //是否允许连接到画布空白位置的点
                            allowMulti: false, //是否允许在相同的起始节点和终止之间创建多条边
                            allowLoop: false, //是否允许创建循环连线,即边的起始节点和终止节点为同一节点
                            highlight: true, //拖动边时,是否高亮显示所有可用的节点
                            validateEdge({ edge, type, previous }) {
                                // 连线时设置折线
                                edge.setRouter({
                                    name: 'er',
                                })
                                // 设置连线样式
                                edge.setAttrs({
                                    line: {
                                        stroke: '#275da3',
                                        strokeWidth: 4,
                                    },
                                })
                                return true
                            },
                        },
                        panning: {
                            enabled: true,
                        },
                        mousewheel: {
                            enabled: true, // 支持滚动放大缩小
                        },
                        grid: {
                            type: 'mesh',
                            size: 20,      // 网格大小 10px
                            visible: true, // 渲染网格背景
                            args: {
                                color: '#eeeeee', // 网格线/点颜色
                                thickness: 2,     // 网格线宽度/网格点大小
                            },
                        },
                    })
                },
            }
        }
    </script>
    
  • 其中初始化画布时,画布中的部分属性在注释中给出,如果想要深入了解,建议在官方文档中根据对应案例进行学习了解

(1)满足需求1
  • 满足左侧菜单栏的拖拽效果可以利用x6stencil初始化一个左侧菜单栏,这样菜单栏内部的模块就可以进行拖动了;但是为了较高的自定义样式这里舍弃使用这种方式,而是利用H5draggable属性,帮助我们间接完成拖拽模块的功能,这里只举例出几个模块作为演示和学习,菜单栏代码如下:

    HomeView.vue
    ...
    <div class="menu-list">
        <div
             v-for="item in moduleList"
             :key="item.id"
             draggable="true"
             @dragend="handleDragEnd($event, item)"
             >
            <p>{{item.name}}</p>
        </div>
    </div>
    ...
    <div
         id="container"
         @dragover="dragoverDiv"
         ></div>
    ...
    
    <script>
        data() {
            return {
                moduleList: [
                    {
                        id: 1,
                        name: '开始模块',
                        type: 'initial' // 初始模块(用于区分样式)
                    },
                    {
                        id: 2,
                        name: '结束模块',
                        type: 'initial'
                    },
                    {
                        id: 3,
                        name: '逻辑模块1',
                        type: 'logic' // 逻辑模块(用于区分样式)
                    },
                    {
                        id: 4,
                        name: '逻辑模块2',
                        type: 'logic'
                    }
                ] // 列表可拖动模块
            }
        },
        methods {
            // 拖动后松开鼠标触发事件
            handleDragEnd(e, item) {
                console.log(e, item) // 可以获取到最后拖动后松开鼠标时的坐标和拖动的节点相关信息
            },
            // 拖动节点到画布中鼠标样式变为可拖动状态
            dragoverDiv(ev) {
                ev.preventDefault()
            }
            ...
        }
    </script>
    
(2)满足需求2
  • 到目前为止已经完成了模块的拖动部分,接下来需要拖动到画布中生成相应的模块,这里需要满足隐含的需求,自定义每个模块生成节点的样式,利用x6高级指引-使用 HTML/React/Vue/Angular 渲染 出需要的节点样式,笔者在这里手写了一个工具类的函数,帮助我们生成相应的节点,需求2中的节点之间可以连线也在生成节点中加上可以连线的属性,这里没有使用连线桩进行连线,而是节点之间直接进行连线,如果需要使用连线桩,建议阅读官方文档深入学习群组 Group连接桩 Port的使用方法,如果你也直接使用节点之间连线的方式可以参考以下代码:

    graphTools.js
    /* 
    antv x6图谱相关工具函数
    */
    export default {
      /* 
      初始化初始节点(开始,结束节点)
      x:x轴坐标
      y:y轴坐标
      id:开始节点id
      name:节点内容,默认为空
      type:节点类型,默认为空
      */
      initInitialNode(x, y, id, name, type) {
        let node = {
          shape: 'html',
          type: type,
          id: id, // String,可选,节点的唯一标识
          x: x, // Number,必选,节点位置的 x 值
          y: y, // Number,必选,节点位置的 y 值
          width: 140, // Number,可选,节点大小的 width 值
          height: 50, // Number,可选,节点大小的 height 值
          html: `
                <div class="custom_node_initial">
                  <div>
                    <i>🌐</i>
                    <p title=${name}>${name||''}</p>
                  </div>
                </div>
                `,
          attrs: {
            body: {
              stroke: 'transparent',
              strokeWidth: 10,
              magnet: true,
            }
          },
        }
        return node
      },
    
      /* 
      初始化逻辑节点
      x:x轴坐标
      y:y轴坐标
      id:开始节点id
      name:节点内容,默认为空
      type:节点类型,默认为空
      */
      initLogicNode(x, y, id, name, type) {
        let node = {
          shape: 'html',
          type: type, // 动作所属类型
          id: id, // String,可选,节点的唯一标识
          x: x, // Number,必选,节点位置的 x 值
          y: y, // Number,必选,节点位置的 y 值
          width: 140, // Number,可选,节点大小的 width 值
          height: 50, // Number,可选,节点大小的 height 值
          html: `
                  <div class="custom_node_logic">
                    <div>
                      <i>💠</i>
                      <p title=${name}>${name||''}</p>
                    </div>
                  </div>
                `,
          attrs: {
            body: {
              stroke: 'transparent',
              strokeWidth: 10,
              magnet: true,
            }
          },
        }
        return node
      }
    }
    
(3)满足隐含需求
  • 在页面组件中引入工具函数,并添加一个节点生成函数,将模块的参数传入节点生成函数中,生成相应的节点,代码如下:

    HomeView.vue
    ...
    <script>
        ...
        import Tools from '@/assets/js/graphTools.js'
        ...
        export default {
            methods: {
                // 添加节点事件
                addHandleNode(x, y, id, name, type) {
                    type === 'initial'
                        ?
                        this.graph.addNode(Tools.initInitialNode(x, y, id, name, type))
                        :
                        this.graph.addNode(Tools.initLogicNode(x, y, id, name, type))
                },
                // 拖动后松开鼠标触发事件
                handleDragEnd(e, item) {
                    this.addHandleNode(e.pageX - 240, e.pageY - 40, new Date().getTime(), item.name, item.type)
                },
            }
        }
    </script>
    <style lang="less">
        // 其中节点样式加到没有scoped包裹的style标签中,否则样式不生效
        // 初始节点样式
        .custom_node_initial {
            width: 100%;
            height: 100%;
            display: flex;
            border-radius: 3px;
            background: rgba(22, 184, 169, 0.6);
            flex-direction: column;
            overflow: hidden;
            > div {
                width: 100%;
                height: 100%;
                display: flex;
                align-items: center;
                justify-content: center;
                padding: 5px;
                box-sizing: border-box;
                border: 5px solid rgba(47, 128, 235, 0.6);
                i {
                    line-height: 22px;
                    font-size: 18px;
                    color: #ffffff;
                    display: flex;
                    align-items: center;
                    margin-right: 5px;
                    justify-content: center;
                    font-style: normal;
                }
                p {
                    color: #ffffff;
                    font-size: 16px;
                    overflow: hidden;
                    text-overflow: ellipsis;
                    white-space: nowrap;
                }
            }
        }
        // 逻辑节点样式
        .custom_node_logic {
            width: 100%;
            height: 100%;
            display: flex;
            background: rgba(47, 128, 235, 0.5);
            flex-direction: column;
            overflow: hidden;
            border-radius: 5px;
            > div {
                width: 100%;
                height: 100%;
                display: flex;
                align-items: center;
                justify-content: center;
                padding: 5px;
                box-sizing: border-box;
                border: 5px solid rgba(22, 184, 169, 0.5);
                border-radius: 5px;
                line-height: 22px;
                i {
                    line-height: 22px;
                    font-size: 18px;
                    color: #b5cde9;
                    margin-right: 5px;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    font-style: normal;
                }
                p {
                    color: #ffffff;
                    font-size: 14px;
                    overflow: hidden;
                    text-overflow: ellipsis;
                    white-space: nowrap;
                }
            }
        }
    </style>
    
(4)满足需求3和4
  • 这里需要利用x6提供的方法,给节点绑定相应的事件,代码如下:

    HomeView.vue
    <script>
        export default {
            data() {
                return{
                    ...
                    curSelectNode: null, // 当前选中的节点和节点相关信息
                }
            },
            methods: {
                initGraph() {
                    ...
                    this.nodeAddEvent()
                }
                // 节点绑定事件
                nodeAddEvent() {
                  // 节点绑定点击事件
                  this.graph.on('node:click', ({ e, x, y, node, view }) => {
                    // 判断是否有选中过节点
                    if (this.curSelectNode) {
                      // 移除选中状态
                      this.curSelectNode.removeTools()
                      // 判断两次选中节点是否相同
                      if (this.curSelectNode !== 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
                            }
                          }
                        }])
                        this.curSelectNode = node
                      } else {
                        this.curSelectNode = null
                      }
                    } else {
                      this.curSelectNode = 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
                          }
                        }
                      }])
                    }
                  })
                  // 连线绑定悬浮事件
      		      this.graph.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.zIndex = 99 // 保证当前悬停的线在最上层,不会被遮挡
      		        }
      		      })
                  this.graph.on('cell:mouseleave', ({ cell }) => {
                    if (cell.shape === 'edge') {
                      cell.removeTools()
                      cell.setAttrs({
                        line: {
                          stroke: '#275da3',
                        },
                      })
                      cell.zIndex = 1 // 保证未悬停的线在下层,不会遮挡悬停的线
                    }
                  })
                }
            }
        }
    </script>
    
  • 节点相关信息全部都储存在变量curSelectNode中,一般用到的属性值都在store->data中,自定义的属性也在这个里面(如笔者自定义的type

3.成果展示和demo仓库地址

  • 到这里以上的需求就都完成了,如果跟着做完相信你对x6组件库已经有了一定的了解,对于一些简单的需求也可以试着做了,如果需求复杂还是需要参考官方文档,文档中大量的属性和api都没有使用到,节点的连线逻辑也只是一笔带过,最后把这个demo的地址分享出来,csdn资源地址:解压即用x6_learning.rar,仓库地址:x6_learning_demo: Antv X6组件库绘制流程图demo (gitee.com),希望能帮到你🌈
    在这里插入图片描述
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐