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
        });
      });
    }
  }
};
相关推荐
✎ ﹏梦醒͜ღ҉繁华落℘4 天前
Visio 绘制流程图
流程图
秋98 天前
AI快速生成可编辑的流程图的方法
流程图
宁静致远20219 天前
Mermaid VSCode插件制作流程图保存方法
ide·vscode·流程图
min18112345611 天前
小型网站开发简易流程步骤 在线画图工具快速生成开发流程图方法
论文阅读·信息可视化·毕业设计·流程图·论文笔记
DYuW5gBmH12 天前
Kafka 成功消费消息的完整流程图
分布式·kafka·流程图
数说星榆18114 天前
无人员伤亡车辆事故处理流程图 快速结案流程
架构·电脑·流程图·职场发展·课程设计
dajun18112345614 天前
信息系统运维管理全流程详解 在线画图工具绘制运维流程图表技巧
运维·数据库·信息可视化·流程图·旅游·论文笔记
数说星榆18114 天前
罕见病例报告流程图学术论文用
论文阅读·人工智能·流程图
fish-man15 天前
测试流程图显示
流程图
橙色日落15 天前
Vue2 + LogicFlow 实现可视化流程图编辑功能+常用属性大全
前端·vue·流程图·logicflow