X6 是 AntV 旗下的图编辑引擎,提供了一系列开箱即用的交互组件和简单易用的节点定制能力,方便我们快速搭建流程图、DAG 图、ER 图等图应用.

参考一些网站的写法,然后自己加上了撤销操作

节点和连接线可以进行删除

element 画流程图 vue绘制流程图_html

安装

npm install @antv/x6 --save
 import { Graph } from '@antv/x6';

代码

<template>
  <div class="wrap">
    <div class="home">
      <div class="menu-bar">
        <!-- 模块列表 -->
        <div class="menu-list">
          <div
            v-for="item in state.moduleList"
            :key="item.id"
            draggable="true"
            @dragend="handleDragEnd($event, item)"
          >
            <plus-outlined />
            <span>{{ item.name }}</span>
          </div>
          <a-button :disabled="!state.canUndo" @click="onUndo">撤销操作</a-button>
        </div>
        <!-- 画布部分 -->
        <div class="canvas-card">
          <div id="container" @dragover="dragoverDiv"></div>
        </div>
      </div>
    </div>
  </div>
  <page-footer />
</template>

<script setup>
import { onMounted, reactive } from 'vue'
import { DownloadOutlined, PlusOutlined } from '@ant-design/icons-vue'
import { Graph } from '@antv/x6'
import Tools from './js/graphTools.js'

const state = reactive({
  moduleList: [
    {
      id: 1,
      name: '模块1',
      type: 'initial' // 初始模块(用于区分样式)
    },
    {
      id: 2,
      name: '模块2',
      type: 'initial'
    },
    {
      id: 3,
      name: '模块3',
      type: 'logic' // 逻辑模块(用于区分样式)
    },
    {
      id: 4,
      name: '模块4',
      type: 'logic'
    }
  ], // 列表可拖动模块
  graph: null, // 画布实例对象
  curSelectNode: null, // 当前选中的节点
  canUndo: false,
  history: null
})

onMounted(() => {
  initGraph()
})

// 初始化流程图画布
function initGraph() {
  let container = document.getElementById('container')
  state.graph = new Graph({
    container: container, // 画布容器
    width: container.offsetWidth, // 画布宽
    height: container.offsetHeight, // 画布高
    background: false, // 背景(透明)
    snapline: true, // 对齐线
    history: 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: 1
          }
        })
        return true
      }
    },
    panning: {
      enabled: true
    },
    mousewheel: {
      enabled: true // 支持滚动放大缩小
    },
    grid: {
      type: 'mesh',
      size: 5, // 网格大小 10px
      visible: false, // 渲染网格背景
      args: {
        color: '#eeeeee', // 网格线/点颜色
        thickness: 1 // 网格线宽度/网格点大小
      }
    }
  })
  state.history = state.graph.history
  nodeAddEvent()
  state.graph.history.on('change', () => {
    state.canUndo = state.graph.history.canUndo()
  })
}

// 生成节点函数
function addHandleNode(x, y, id, name, type) {
  type === 'initial'
    ? state.graph.addNode(Tools.initInitialNode(x, y, id, name, type))
    : state.graph.addNode(Tools.initLogicNode(x, y, id, name, type))
}
function nodeAddEvent() {
  // 节点绑定点击事件
  state.graph.on('node:click', ({ e, x, y, node, view }) => {
    // 判断是否有选中过节点
    if (state.curSelectNode) {
      // 移除选中状态
      state.curSelectNode.removeTools()
      // 判断两次选中节点是否相同
      if (state.curSelectNode !== node) {
        node.addTools([
          {
            name: 'boundary',
            args: {
              attrs: {
                fill: '#16B8AA',
                stroke: '#c3d0e2',
                strokeWidth: 1,
                fillOpacity: 0.1
              }
            }
          },
          {
            name: 'button-remove',
            args: {
              x: '100%',
              y: 0,
              offset: {
                x: 0,
                y: 0
              }
            }
          }
        ])
        state.curSelectNode = node
      } else {
        state.curSelectNode = null
      }
    } else {
      state.curSelectNode = node
      node.addTools([
        {
          name: 'boundary',
          args: {
            attrs: {
              fill: '#16B8AA',
              stroke: '#c3d0e2',
              strokeWidth: 1,
              fillOpacity: 0.1
            }
          }
        },
        {
          name: 'button-remove',
          args: {
            x: '100%',
            y: 0,
            offset: {
              x: 0,
              y: 0
            }
          }
        }
      ])
    }
  })
  // 连线绑定悬浮事件
  state.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: '#c3d0e2'
        }
      })
      cell.zIndex = 99
    }
  })
  state.graph.on('cell:mouseleave', ({ cell }) => {
    if (cell.shape === 'edge') {
      cell.removeTools()
      cell.setAttrs({
        line: {
          stroke: '#c3d0e2'
        }
      })
      cell.zIndex = 1
    }
  })
}
// 拖动后松开鼠标触发事件
function handleDragEnd(e, item) {
  addHandleNode(e.pageX - 240, e.pageY - 40, new Date().getTime(), item.name, item.type)
}
// 拖动节点到画布中鼠标样式变为可拖动状态
function dragoverDiv(ev) {
  ev.preventDefault()
}
function onUndo() {
  state.history.undo()
}
</script>
<style lang="less" scoped>
.wrap {
  margin: 10px;
  height: 100%;
  padding: 20px;
  background: #fff;
  .buttons {
    margin-bottom: 20px;
  }
  .home {
    width: 100%;
    height: 94%;
    display: flex;
    // 模块列表部分
    .menu-bar {
      width: 50%;
      height: 100%;
      margin-right: 20px;
      padding: 20px;
      border: 1px solid #c3d0e2;
      .menu-list {
        display: flex;
        flex-direction: row;
        justify-content: center;
        margin-bottom: 20px;
        > div {
          height: 32px;
          line-height: 32px;
          border: 1px solid #4c84ff;
          color: #4c84ff;
          border-radius: 5px;
          padding: 0 10px;
          margin: 0 20px;
          cursor: pointer;
          box-sizing: border-box;
        }
        span {
          margin-left: 5px;
        }
      }
      // 画布部分
      .canvas-card {
        height: 100%;
        > div {
          width: 100%;
          height: 100%;
        }
      }
    }
  }
}
</style>
<style lang="less">
// 其中节点样式加到没有scoped包裹的style标签中,否则样式不生效
// 初始节点样式
.custom_node_initial {
  width: 100%;
  height: 100%;
  // border: 1px solid #c3d0e2;
  border-radius: 4px;
  display: flex;
  color: #0000;
  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: 1px solid #c3d0e2;
    li {
      list-style: none;
      color: #000;
      font-size: 12px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
  }
}
// 逻辑节点样式
.custom_node_logic {
  width: 100%;
  height: 100%;
  border: 1px solid #c3d0e2;
  display: flex;
  color: #000;
  flex-direction: column;
  overflow: hidden;
  border-radius: 4px;
  > div {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 5px;
    box-sizing: border-box;
    border: 1px solid #c3d0e2;
    border-radius: 5px;
    line-height: 22px;
    li {
      list-style: none;
      color: #000;
      font-size: 12px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
  }
}
</style>

graphTools.js

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: 90, // Number,可选,节点大小的 width 值
      height: 32, // Number,可选,节点大小的 height 值
      html: `
            <div class="custom_node_initial">
              <div>
                <li title=${name}>${name || ''}</li>
              </div>
            </div>
            `,
      attrs: {
        // 这里给生成的节点的body加上透明的边框,一定要给边框宽度加上>0的值,否则节点将不能连线
        body: {
          stroke: 'transparent',
          strokeWidth: 1, // 边框的粗细
          magnet: true // 节点是否可以连线
        }
      }
    }
    return node
  },
  initLogicNode(x, y, id, name, type) {
    let node = {
      shape: 'html',
      type: type, // 动作所属类型
      id: id, // String,可选,节点的唯一标识
      x: x, // Number,必选,节点位置的 x 值
      y: y, // Number,必选,节点位置的 y 值
      width: 90, // Number,可选,节点大小的 width 值
      height: 32, // Number,可选,节点大小的 height 值
      html: `
              <div class="custom_node_logic">
                <div>
                  <li title=${name}>${name || ''}</li>
                </div>
              </div>
            `,
      attrs: {
        body: {
          stroke: 'transparent',
          strokeWidth: 1,
          magnet: true
        }
      }
    }
    return node
  }
}

效果

element 画流程图 vue绘制流程图_html_02