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


    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'
      
      • 1
    • 在需要的页面中引入后即可开始初始化画布,初始化画布函数代码如下:

      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
      • 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
    • 其中初始化画布时,画布中的部分属性在注释中给出,如果想要深入了解,建议在官方文档中根据对应案例进行学习了解

    (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>
      
      • 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

    (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
        }
      }
      
      • 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

    (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>
      
      • 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

    (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>
      
      • 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
    • 节点相关信息全部都储存在变量curSelectNode中,一般用到的属性值都在store->data中,自定义的属性也在这个里面(如笔者自定义的type

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

    • 到这里以上的需求就都完成了,如果跟着做完相信你对x6组件库已经有了一定的了解,对于一些简单的需求也可以试着做了,如果需求复杂还是需要参考官方文档,文档中大量的属性和api都没有使用到,节点的连线逻辑也只是一笔带过,最后把这个demo的地址分享出来,csdn资源地址:解压即用x6_learning.rar,仓库地址:x6_learning_demo: Antv X6组件库绘制流程图demo (gitee.com),希望能帮到你🌈
      在这里插入图片描述
  • 相关阅读:
    ES6模块化、Express、node.js 实现的API 接口案例
    SciencePlot-科研绘图
    Ionic list - ion-item的相关用法
    Prompt 指南之零样本与少样本提示,超详细解析!
    【数据结构与算法】手撕二叉查找树
    项目团队情绪管理的几点注意事项
    WiFi protocol 详解
    C/C++ 数据结构 - 队列
    x-zse-96的JSRPC解决方案
    架构师聊编程-技术晋级篇
  • 原文地址:https://blog.csdn.net/baoyin0822/article/details/125622147