vue2使用antv/g6-editor实现可拖拽流程图

依赖下载

照着这个引入就好,然后npm install

源码

javascript 复制代码
<template>

  <div id="vue-g6-editor">

    <el-row>
      <el-col :span="24">

      </el-col>
    </el-row>
    <!-- 工具栏 -->
    <el-row>
      <el-col :span="24">
        <div id="toolbar">
          <i data-command="save" class="command fa fa-floppy-o" title="保存"></i>
          <i class="fa fa-history" title="历史数据" @click="readHistoryData"></i>
          <i class="fa fa-hdd-o" title="上传数据" @click="readUploadData"></i>
          <i class="fa fa-download" title="另存为文件" @click="saveAsFile"></i>
          <i class="fa fa-picture-o" title="另存为图片" @click="openSaveAsImageDialog"></i>
          <i data-command="undo" class="command fa fa-undo" title="撤销"></i>
          <i data-command="redo" class="command fa fa-repeat" title="重做"></i>
          <i data-command="delete" class="command fa fa-trash-o" title="删除"></i>
          <i data-command="zoomOut" class="command fa fa-search-minus" title="缩小"></i>
          <i data-command="zoomIn" class="command fa fa-search-plus" title="放大"></i>
          <i data-command="clear" class="command fa fa-eraser" title="清除画布"></i>
          <i data-command="toFront" class="command fa fa-arrow-up" title="提升层级"></i>
          <i data-command="toBack" class="command fa fa-arrow-down" title="下降层级"></i>
          <i data-command="selectAll" class="command fa fa-check-square-o" title="全选"></i>
          <i data-command="copy" class="command fa fa-files-o" title="复制"></i>
          <i data-command="paste" class="command fa fa-clipboard" title="粘贴"></i>
          <i data-command="autoZoom" class="command fa fa-expand" title="实际大小"></i>
          <i data-command="resetZoom" class="command fa fa-compress" title="适应页面"></i>
          <i data-command="addGroup" class="command fa fa-object-group" title="组合"></i>
          <i data-command="unGroup" class="command fa fa-object-ungroup" title="取消组合"></i>
          <i data-command="multiSelect" class="command fa fa fa-crop" title="多选"></i>
        </div>
      </el-col>
    </el-row>
    <!-- 元素面板 + 画布 + 属性栏 -->
    <el-row>
      <!-- 元素面板 -->
      <el-col :span="2">
        <div id="itempannel">
          <!-- 开始节点 -->
          <div id="startNode" class="getItem" data-type="node" data-shape="flow-circle" data-size="72*72"
            data-label="开始节点" data-color="#FA8C16" data-nodeType="startNode">
            <img draggable="false" :src="startNodeSVGUrl" alt srcset />
          </div>

          <!-- 常规节点 -->
          <div id="regularNode" class="getItem" data-type="node" data-size="100*50" data-label="常规节点"
            data-color="#1890ff">
            <img draggable="false" :src="regularNodeSVGUrl" alt srcset />
          </div>

          <!-- 条件节点 -->
          <div id="judgeNode" class="getItem" data-type="node" data-shape="flow-rhombus" data-size="80*80"
            data-label="条件节点" data-color="#13C2C2">
            <img draggable="false" :src="conditionNodeSVGUrl" />
          </div>

          <!-- 结束节点 -->
          <div id="endNode" class="getItem" data-type="node" data-shape="flow-circle" data-size="80*80"
            data-label="结束节点" data-color="#FA8C16" data-nodeType="endNode">
            <img draggable="false" :src="endNodeSVGUrl" />
          </div>
        </div>
      </el-col>
      <!-- 画布 -->
      <el-col :span="18">
        <el-col :span="24">
          <div id="page">
            <div class="controltab">ab</div>
          </div>
        </el-col>
      </el-col>
      <!-- 属性栏 -->
      <el-col :span="4">
        <section class="right-part">
          <div id="detailpannel">
            <!-- 节点属性栏 -->
            <div id="nodeAttributeBar" class="pannel" data-status="node-selected">
              <div class="title">节点属性</div>
              <div class="main">
                <el-form :model="nodeAttributeForm" label-position="top" label-width="80px">
                  <el-form-item label="节点文本">
                    <el-input v-model="nodeAttributeForm.label" @change="saveNodeAttribute"></el-input>
                  </el-form-item>
                  <el-form-item label="宽度">
                    <el-input v-model="nodeAttributeForm.width" @change="saveNodeAttribute"></el-input>
                  </el-form-item>
                  <el-form-item label="高度">
                    <el-input v-model="nodeAttributeForm.height" @change="saveNodeAttribute"></el-input>
                  </el-form-item>
                  <el-form-item label="颜色">
                    <el-color-picker v-model="nodeAttributeForm.color" @change="saveNodeAttribute"></el-color-picker>
                  </el-form-item>
                </el-form>
              </div>
            </div>
            <!-- 边属性栏 -->
            <div id="edgeAttributeBar" class="pannel" data-status="edge-selected">
              <div class="title">边属性</div>
              <div class="main">
                <el-form :model="edgeAttributeForm" label-position="top" label-width="80px">
                  <el-form-item label="边文本">
                    <el-input v-model="edgeAttributeForm.label" @change="saveEdgeAttribute"></el-input>
                  </el-form-item>
                  <el-form-item label="边文本">
                    <el-select v-model="edgeAttributeForm.shape" @change="saveEdgeAttribute">
                      <el-option label="流程图折线" value="flow-polyline"></el-option>
                      <el-option label="流程图圆⻆折线" value="flow-polyline-round"></el-option>
                      <el-option label="流程图曲线" value="flow-smooth"></el-option>
                    </el-select>
                  </el-form-item>
                </el-form>
              </div>
            </div>
            <div id="groupAttributeBar" class="pannel" data-status="group-selected">
              <div class="title">群组属性栏</div>
            </div>
            <div id="canvasAttributeBar" class="pannel" data-status="canvas-selected">
              <div class="title">画布属性栏</div>
              <div class="main">
                <el-form label-width="80px" label-position="right">
                  <el-form-item label="网格对齐">
                    <el-checkbox v-model="canvasAttributeForm.grid" @change="toggleGridShowStatus"></el-checkbox>
                  </el-form-item>
                </el-form>
              </div>
            </div>
            <div id="multiAttributeBar" class="pannel" data-status="multi-selected">
              <div class="title">多选时属性栏</div>
            </div>
          </div>
          <!-- 缩略图 -->
          <div id="minimap">
            <div class="title">缩略图</div>
          </div>
        </section>
      </el-col>
    </el-row>
    <!-- 弹窗 -->
    <article>
      <!-- 下载图片 -->
      <section class="save-as-image-dialog">
        <el-dialog title="下载图片" :visible.sync="saveAsImageDialogVisible" width="360px">
          <el-form label-width="100px" label-position="top">
            <el-form-item label="选择图片格式">
              <el-select v-model="saveAsImageFormat">
                <el-option label="jpg" value="jpg">
                  <span style="float: left;">jpg</span>
                  <span style="float: right; color: #8492a6; font-size: 13px;">白色背景</span>
                </el-option>
                <el-option label="png" value="png">
                  <span style="float: left;">png</span>
                  <span style="float: right; color: #8492a6; font-size: 13px;">透明背景</span>
                </el-option>
              </el-select>
            </el-form-item>
          </el-form>
          <span slot="footer">
            <el-button @click="saveAsImageDialogVisible = false">取 消</el-button>
            <el-button type="primary" @click="saveAsImage">确 定</el-button>
          </span>
        </el-dialog>
      </section>
    </article>
    <!-- 右键菜单 -->
    <section>
      <div id="contextmenu">
        <div data-status="node-selected" class="menu">
          <el-button data-command="copy" class="command">复制</el-button>
          <el-button data-command="paste" class="command">粘贴</el-button>
          <el-button data-command="delete" class="command">删除</el-button>
        </div>
        <div data-status="edge-selected" class="menu">
          <el-button data-command="delete" class="command">删除</el-button>
        </div>
        <div data-status="group-selected" class="menu">
          <el-button data-command="copy" class="command">复制</el-button>
          <el-button data-command="paste" class="command">粘贴</el-button>
          <el-button data-command="unGroup" class="command">取消组合</el-button>
          <el-button data-command="delete" class="command">删除</el-button>
        </div>
        <div data-status="canvas-selected" class="menu">
          <el-button data-command="undo" class="command">撤销</el-button>
          <el-button data-command="redo" class="command disable">重做</el-button>
        </div>
        <div data-status="multi-selected" class="menu">
          <el-button data-command="copy" class="command">复制</el-button>
          <el-button data-command="paste" class="command">粘贴</el-button>
          <el-button data-command="addGroup" class="command">组合</el-button>
        </div>
      </div>
    </section>
  </div>

</template>
<script>
import G6Editor from '@antv/g6-editor'
import mixin from '../mixin'
import { construct } from 'netflix-conductor-json-tree/dist/index'
export default {
  name: 'VueG6Editor',
  mixins: [mixin],
  data() {
    return {
      // 节点属性表单
      nodeAttributeForm: {
        label: '',
        width: '',
        height: ''
      },
      // 节点属性表单
      edgeAttributeForm: {
        label: ''
      },
      // 画布属性栏表单
      canvasAttributeForm: {
        grid: true,
        cell: 20
      },
      // SVG节点图片URL地址
      startNodeSVGUrl: require('../../../assets/start-node.svg'),
      endNodeSVGUrl: require('../../../assets/end-node.svg'),
      regularNodeSVGUrl: require('../../../assets/regular-node.svg'),
      conditionNodeSVGUrl: require('../../../assets/condition-node.svg'),
      modelNodeSVGUrl: 'https://gw.alipayobjects.com/zos/rmsportal/rQMUhHHSqwYsPwjXxcfP.svg',
      // 编辑器
      editor: null,
      saveAsImageDialogVisible: false,
      saveAsImageFormat: 'jpg'
    }
  },
  mounted() {
    this.initG6Editor()
  },
  methods: {
    // 初始化
    initG6Editor() {
      const _this = this
      const editor = new G6Editor()
      this.editor = editor
      G6Editor.track(false)
      const Command = G6Editor.Command
      // 注册新命令save
      Command.registerCommand('save', {
        // 禁止保存命令进入队列
        queue: false,
        // 命令是否可用
        enable: (editor) => {
          return true
        },
        // 正向命令
        execute(editor) {
          const needSaveData = editor.getCurrentPage().save()
          console.log(needSaveData)
          localStorage.setItem('flowData', JSON.stringify(needSaveData))
          _this.save(needSaveData)
          _this.$message.success('数据已保存')
        },
        // 反向命令
        back(editor) {
          console.log('反向命令')
          console.log(editor)
        },
        // 快捷键:Ctrl + S
        shortcutCodes: [
          ['metaKey', 's'],
          ['ctrlKey', 's']
        ]
      })
      // 画布
      const flow = new G6Editor.Flow({
        graph: {
          container: 'page'
        },
        align: {
          line: {
            // 对齐线颜色
            stroke: '#FA8C16',
            // 对齐线粗细
            lineWidth: 1
          },
          // 开启全方位对齐
          item: true,
          // 网格对齐
          grid: true
        },
        grid: {
          // 网孔尺寸
          cell: 18
        },
        shortcut: {
          // 开启自定义命令保存的快捷键
          save: true
        }
      })
      window.flow = flow

      // 设置边
      flow.getGraph().edge({
        shape: 'flow-polyline'
      })

      // 元素面板栏
      const itempannel = new G6Editor.Itempannel({
        container: 'itempannel'
      })
      // 工具栏
      const toolbar = new G6Editor.Toolbar({
        container: 'toolbar'
      })
      // 属性栏
      const detailpannel = new G6Editor.Detailpannel({
        container: 'detailpannel'
      })
      // 缩略图
      let minimapWidth = getComputedStyle(document.querySelector('.right-part')).width
      minimapWidth = Number(minimapWidth.replace(/px$/, ''))
      const minimap = new G6Editor.Minimap({
        container: 'minimap',
        width: minimapWidth,
        height: 200
      })
      // 右键菜单
      const contextmenu = new G6Editor.Contextmenu({
        container: 'contextmenu'
      })
      // 挂载以上组件到Editor
      editor.add(flow)
      editor.add(itempannel)
      editor.add(toolbar)
      editor.add(detailpannel)
      editor.add(minimap)
      editor.add(contextmenu)
      // 挂载到window,方便调试
      window.editor = editor

      // 获取当前画布
      const currentPage = editor.getCurrentPage()
      currentPage.on('afterchange', (e) => {
        if (e.action === 'add') {
          if (e.model.nodetype === 'startNode' || e.model.nodetype === 'endNode') {
            const nodes = this.editor.getCurrentPage().getNodes()
            for (const item of nodes) {
              if (item.model.nodetype === e.model.nodetype && item.model.id !== e.model.id) {
                this.editor.getCurrentPage().remove(e.item)
                this.$message.warning('只能有一个开始节点或结束节点')
              }
            }
          }
        }
      })
      // 监听(选择对象后)事件
      currentPage.on('afteritemselected', (ev) => {
        console.log('打印所选对象属性', ev.item)
        console.log('打印所选对象数据模型', ev.item.model)
        const selectedItemDataModel = ev.item.model
        // 如果选择的对象是节点
        if (ev.item.isNode) {
          this.nodeAttributeForm.label = selectedItemDataModel.label
          this.nodeAttributeForm.width = selectedItemDataModel.size.split('*')[0]
          this.nodeAttributeForm.height = selectedItemDataModel.size.split('*')[1]
          this.nodeAttributeForm.color = selectedItemDataModel.color
        }
        // 如果选择的对象是边
        if (ev.item.isEdge) {
          ev.item.graph.edge({
            shape: 'flow-polyline-round'
          })
          this.edgeAttributeForm.label = selectedItemDataModel.label
          this.edgeAttributeForm.shape = selectedItemDataModel.shape
        }
      })
      // 监听(删除后)事件
      currentPage.on('afterdelete', (ev) => { })
    },
    // 打开保存为图片弹窗
    openSaveAsImageDialog() {
      this.saveAsImageDialogVisible = true
    },
    // 开启/关闭网格对齐
    toggleGridShowStatus(value) {
      if (value) {
        this.editor.getCurrentPage().showGrid()
      } else {
        this.editor.getCurrentPage().hideGrid()
      }
    },
    // 保存为图片
    saveAsImage() {
      let newCanvas
      if (this.saveAsImageFormat === 'jpg') {
        const canvas = this.editor.getCurrentPage().saveImage()
        newCanvas = document.createElement('canvas')
        newCanvas.width = canvas.width
        newCanvas.height = canvas.height
        const newContext = newCanvas.getContext('2d')
        newContext.fillStyle = '#fff'
        newContext.fillRect(0, 0, newCanvas.width, newCanvas.height)
        newContext.drawImage(canvas, 0, 0)
      }
      if (this.saveAsImageFormat === 'png') {
        newCanvas = this.editor.getCurrentPage().saveImage()
      }
      const imageDataURL = newCanvas.toDataURL()
      const downloadLink = document.createElement('a')
      downloadLink.download = '图片.jpg'
      downloadLink.href = imageDataURL
      document.body.appendChild(downloadLink)
      downloadLink.click()
      document.body.removeChild(downloadLink)
      this.saveAsImageDialogVisible = false
    },
    // 保存为文件
    saveAsFile() {
      const jsonString = JSON.stringify(this.editor.getCurrentPage().save())
      const blob = new Blob([jsonString])
      const blobURL = URL.createObjectURL(blob)
      const downloadLink = document.createElement('a')
      downloadLink.download = '数据.json'
      downloadLink.href = blobURL
      document.body.appendChild(downloadLink)
      downloadLink.click()
      URL.revokeObjectURL(blobURL)
      document.body.removeChild(downloadLink)
    },
    // 读取历史数据
    readHistoryData() {
      const stringData = localStorage.getItem('flowData')
      if (stringData === '' || stringData === '{}' || stringData === null) {
        this.$message.warning('无历史数据')
        return
      }
      const jsonData = JSON.parse(stringData)
      this.editor.getCurrentPage().read(jsonData)
    },
    // 读取上传数据
    readUploadData() {
      const uploadButton = document.createElement('input')
      uploadButton.setAttribute('type', 'file')
      uploadButton.setAttribute('accept', '.json')
      uploadButton.addEventListener('change', (e) => {
        console.dir(uploadButton)
        const file = uploadButton.files[0]
        const fileReader = new FileReader()
        fileReader.onload = (event) => {
          console.log(event)
          const text = JSON.parse(event.target.result)
          console.log(text)
          this.editor.getCurrentPage().read(text)
        }
        fileReader.readAsText(file)
      })
      uploadButton.click()
    },
    //
    save(source) {
      const edges = source.edges
      const nodes = source.nodes
      console.log(construct)
      const res = construct(source)
      console.log(JSON.stringify(res, null, 2))
    }
  }
}
</script>

<style lang="less">
@import url("./index.less");
</style>

index.less

javascript 复制代码
@backgroundColor: #fbfbfb;
@borderColor: #dadce0;

@itempannelAndPageBorder: 1px solid #ccc;
@pageHeight: calc(100vh - 41px - 37px);

body {
  margin: 0;

}

.all {
  width: 100%;
  height: 100%;


  .main {
    overflow-y: auto;

    #vue-g6-editor {

      width: 100%;
      height: 100%;

      overflow-x: hidden;
      background-color: white;

      // transform: scale(0.5);
      .showcontent {
        height: 700px;

        .showface {
          height: 700px;
        }

        .showcanvas {
          height: 700px;
        }

        .shownode {
          height: 700px;
        }
      }


      .showtab {
        width: 100%;
        height: 700px;

      }



      // 主画布
      #page {
        height: 700px;
        position: relative;
        display: flex;

        .graph-container {
          height: 700px;
        }


        .controltab {
          width: 100%;
          height: 200px;
          background-color: white;
          position: absolute;

          bottom: 0;
          z-index: 999;
          /* 子盒子底部与父盒子底部对齐 */
          display: flex;
          flex-direction: column;
          /* 垂直方向排列子元素 */
          opacity: 0.5;
          background-color: white;

          /* 设置盒子的透明度为 0.5,即 50% 不透明 */
          .controlbt {
            opacity: 1;
            width: 100%;
            height: 50px;
            display: flex;
            font-size: 50px;
            justify-content: center;
            align-items: center;


          }

        }

        .activeshow {
          width: 90%;

          opacity: 1;
          flex-grow: 1;
          /* 第二个子盒子沾满剩余空间 */



          .header {
            width: 100%;
            color: black;
            font-weight: 550;
            font-size: 20px;
            padding-left: 10px;
            padding-top: 10px;
          }

          .showmain {
            display: flex;

            .objectshow {
              flex: 1;
              color: black;
              font-weight: 550;
            }
          }

          .staticshow {
            width: 60%;
            height: 110px;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;

            .admi {

              color: black;
              font-weight: 550;
              margin-bottom: 15px;
            }

            .setps {
              color: black;
              font-weight: 550;
              margin-bottom: 15px;
            }

            .atomname {
              color: black;
              font-weight: 550;
            }
          }


        }



      }


      header:nth-of-type(1) {
        background: @backgroundColor;
        line-height: 40px;
        padding-left: 20px;
        border-bottom: 1px solid @borderColor;
        box-sizing: border-box;
      }
    }
  }



  // 工具栏
  #toolbar {
    display: flex;
    justify-content: center;
    background: @backgroundColor;
    border-bottom: 1px solid @borderColor;
    padding: 4px 14px;

    i {
      font-size: 18px;
      padding: 4px;
      margin-right: 8px;
      color: #999999;

      &:hover {
        cursor: pointer;
        background-color: #eeeeee;
        color: #5cb6ff;
      }
    }
  }

  // 元素面板
  #itempannel {
    box-sizing: border-box;
    background-color: @backgroundColor;
    border-right: 1px solid @borderColor;
    height: @pageHeight;
    padding-top: 10px;
    overflow: hidden;
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: space-around;
    align-content: flex-start;

    .getItem {
      cursor: move;
      width: 80px;
      height: 80px;
      margin-bottom: 20px;
      display: flex;
      justify-content: center;
      align-items: center;

      img {
        width: 100%;
      }
    }
  }



  // 右侧部分(属性栏 + 缩略图)
  .right-part {
    height: @pageHeight;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
  }

  // 属性栏
  #detailpannel {
    flex-grow: 1;

    background-color: @backgroundColor;
    border-left: 1px solid @borderColor;
    overflow-y: scroll;

    #nodeAttributeBar,
    #edgeAttributeBar,
    #groupAttributeBar,
    #canvasAttributeBar,
    #multiAttributeBar {
      .title {
        height: 34px;
        line-height: 34px;
        text-align: center;
        box-sizing: border-box;
        font-weight: bold;
        font-size: 13px;
        border-width: 0 0 1px 0;
        border-style: solid;
        border-color: @borderColor;
      }

      .main {
        padding: 10px;
      }
    }
  }

  // 缩略图
  #minimap {
    background-color: @backgroundColor;
    border-top: 1px solid #ccc;
    border-left: 1px solid #ccc;

    .title {
      height: 34px;
      line-height: 34px;
      text-align: center;
      box-sizing: border-box;
      font-weight: bold;
      font-size: 13px;
      border-width: 0 0 1px 0;
      border-style: solid;
      border-color: @borderColor;
    }
  }

  // 右键菜单
  #contextmenu {
    display: none;

    .menu {
      /deep/ .el-button {
        width: 100%;
        display: block;
        margin-left: 0;
        border-radius: 0 !important;
        border-bottom: none;

        &:nth-last-of-type(1) {
          border-bottom: 1px solid #dcdfe6;
        }
      }
    }
  }

  // 下载图片弹窗
  .save-as-image-dialog {
    /deep/ .el-select {
      display: block;
    }
  }
}

mixin.js

javascript 复制代码
export default {
  methods: {
    // 保存节点属性
    saveNodeAttribute() {
      this.editor.executeCommand(() => {
        // 获取画布
        const page = this.editor.getCurrentPage();
        // 获取所选对象
        const selectedItem = page.getSelected()[0];
        page.update(selectedItem.id, {
          label: this.nodeAttributeForm.label,
          size: this.nodeAttributeForm.width + "*" + this.nodeAttributeForm.height,
          color: this.nodeAttributeForm.color
        });
      });
    },
    // 保存边属性
    saveEdgeAttribute() {
      this.editor.executeCommand(() => {
        // 获取画布
        const page = this.editor.getCurrentPage();
        // 获取所选对象
        const selectedItem = page.getSelected()[0];
        console.log(this.edgeAttributeForm);
        page.update(selectedItem.id, {
          label: this.edgeAttributeForm.label,
          shape: this.edgeAttributeForm.shape
        });
      });
    }
  }
};
相关推荐
TMS320VC5257H13 小时前
通义灵码生成的流程图是黑色背景怎么办
流程图·通义灵码·mermaid
安的列斯凯奇2 天前
苍穹外卖的分层所用到的技术以及工具+jwt令牌流程图(jwt验证)
流程图
小阮的学习笔记6 天前
Vue3中使用LogicFlow实现简单流程图
javascript·vue.js·流程图
爱分享的淘金达人9 天前
2025年山东省考报名流程图解
java·考研·spring·eclipse·tomcat·流程图
烟雨国度12 天前
Spring MVC 完整生命周期和异常处理流程图
spring·mvc·流程图
冰淇淋噢!13 天前
一般公司流程图详情版
流程图
半块菠萝17 天前
html简易流程图
css·html·流程图
寰梦21 天前
上传Gitee仓库流程图
gitee·流程图
技术路上的苦行僧21 天前
Spring源码解析(35)之Spring全体系源码流程图
java·spring·流程图·1024程序员节
烟雨国度21 天前
阿里云用STS上传oss的完整程序执行流程图 和前端需要哪些参数uniapp
javascript·阿里云·uni-app·流程图·1024程序员节