如何搭建免费好用的流程图画图工具(免登录/付费/无门槛)

这是多年前做的一个项目经历。公司需要个能画流程图或者时序图或者其他诡异不规则图的软件或者工具,并且节点间能配合逻辑可交互(点击、高亮、跳转等),即有开始就有结束,能攻能守,能动能静,可长可短,可盐可甜。鉴于立项时间和现有人力都不算富裕,所以本着快速响应实现原则,胡搜海搜了一波,国内少之又少,要么就特么需要登录,要么就需要付费,我呸。无意间发现某个付费的web版本process啊on啊的,仔细端详了下这个web版的代码调用,发现这个核心就是一个编辑器界面,随之发现重点核心就是那个mxclient,然后奔着这个名字搜了一波,功夫不负有心人找到了出处,原来墙外的世界真大,早就有了这个玩意,废话说了一堆,见笑,链接点此处。(英文不好的,查字典)

废话一堆,真把式拿出来溜溜,欢迎唾弃~~~

真实运行图如下

一段简单粗暴的spa常见开场代码 

<div id="app">
    <div class="pannel">
        <div class="editor__action">
          <el-button type="default" size="small" @click="onCancel">取消</el-button>
          <el-button type="default" size="small" @click="onSave()">保存</el-button>
        </div>
    </div>
    <div ref="editorRef"></div>
</div>

接下来拿vue2.x练手,上菜 

import '^/mxgraph/editor/styles/grapheditor.css'
import '^/mxgraph/editor/resources/grapheditor.txt.js'
import '^/mxgraph/editor/styles/default.xml.js'
import '^/mxgraph/editor/sanitizer/sanitizer.min.js'
import '^/mxgraph/editor/js/EditorUi.js'
import '^/mxgraph/editor/js/Editor.js'
import '^/mxgraph/editor/js/Sidebar.js'
import '^/mxgraph/editor/js/Graph.js'
import '^/mxgraph/editor/js/Format.js'
import '^/mxgraph/editor/js/Shapes.js'
import '^/mxgraph/editor/js/Actions.js'
import '^/mxgraph/editor/js/Menus.js'
import '^/mxgraph/editor/js/Toolbar.js'
import '^/mxgraph/editor/js/Dialogs.js'
import FlowHelper from './flow.js'

export default {
  name: 'editor',  
  data() {
    return {
      message: '',
      xml: ''
    }
  },
  created() {
    this.cellErrors = {}
  },
  mounted() {
    this.initEditor()
  },
  destroyed() {
    let children = [...document.body.children]
    children.forEach(c => {
      if (
        c.nodeType === 1 &&
        ((c.className &&
          (c.className.indexOf('ge') == 0 || c.className.indexOf('mx') == 0)) ||
          (c.style.touchAction === 'none' &&
            c.firstChild.nodeName.toLowerCase() === 'svg'))
      ) {
        document.body.removeChild(c)
      }
    })
  },
  methods: {
    initEditor() {      
        this.graph = FlowHelper.initEditor(
        this.$refs.editorRef,
        grapheditorTXT,
        defaultThemeStylesheet,
        cell => {
          this.taskOnClick(cell)
        }
      )
      this.renderXml()
    },
    renderFromXml(data) {
      this.xml = this.preParseXML(data.flowXml)
      let flowTree = JSON.parse(this.xml)
      flowTree.tasks.forEach(t => this.createTask(t, flowTree))
      if (
        data.processModelAttribute.label &&
        Number(this.$route.params.modifyType) === 0 &&
        data.processModelAttribute.label.length <= 26
      ) {
        data.processModelAttribute.label += '(复制)'
      }
      this.processModelAttribute = data.processModelAttribute
      this.renderXml()
    },
    preParseXML(xml) {
      let xmlObj = JSON.parse(xml)
      xmlObj.cells.forEach(c => {
        if (c.flow) {
          c.style.fillColor = '#fff'
          c.style.strokeColor = '#999'
        }
      })
      xmlObj.tasks.forEach(t => {
        t.value = t.attrs.label
        if (t.flow) {
          t.style.fillColor = '#fff'
          t.style.strokeColor = '#999'
          t.style.strokeWidth = 1
        }
      })
      return JSON.stringify(xmlObj)
    },
    renderXml() {
      if (this.xml && this.graph) {
        const cells = []
        FlowHelper.display(this.xml, null, null, this.graph, false, false)
        FlowHelper.getTaskCells(this.graph).forEach(t => {
          const { label, executorPost: post, assignee } = this.tempTasks[
            'task' + t.id
          ]
          const executor = this.persons.find(val => val.id === assignee) || {}
          FlowHelper.updateCellAttrs(this.graph, t, {
            label: t.value,
            assigneeName: executor.name || assignee,
            executorPost: post
          })
          if (!assignee && /task|condition/.test(t.flow)) {
            cells.push(t)
          }
        })
        if (cells.length) {
          this.$message.error('请填写任务执行人')
          this.setCellStyle(cells, '#f00')
        }
      }
    },
    createTask(task, flowTree) {
      let id = 'task' + task.id
      let temp = { label: '', assignee: '', description: '' }
      if (!this.tempTasks) {
        this.tempTasks = {}
      }
      if (!this.tempTasks[id]) {
        this.tempTasks[id] = {}
      }
      if (task.attrs) {
        this.tempTasks[id] = task.attrs
      }
      if (task.flow == 'condition') {
        temp = { agree: '', reject: '' }
        if (flowTree) {
          /**
           * 编辑时候需要设置决策节点的agree属性为指定的edge.id
           * 编辑时候需要设置决策节点的reject属性为指定的edge.id
           */
          flowTree.cells
            .filter(({ source }) => source == task.id)
            .forEach(e => {
              if (e.conditionExpression == 0) {
                temp.agree = e.id
              }
              if (e.conditionExpression == 1) {
                temp.reject = e.id
              }
            })
        }
      } else if (task.flow == 'task') {
        temp = { tools: '', knowledge: '' }
      } else if (task.flow == 'flow') {
        temp = { label: '', description: '', subFlow: task.subFlow }
      }

      !this.tempTasks[id].agree && delete this.tempTasks[id].agree
      !this.tempTasks[id].reject && delete this.tempTasks[id].reject
      this.tempTasks[id] = Object.assign(temp, this.tempTasks[id], {
        label: this.tempTasks[id].label || this.getValue(task)
      })
    },
    onCancel() {
      this.$confirm('是否确定取消流程模板编辑?取消后不可恢复', '提示', {
        confirmButtonText: '确定取消',
        cancelButtonText: '暂不取消',
        type: 'danger'
      })
        .then(_ => this.$router.go(-1))
        .catch(e => e)
    },
    onSave(releaseTag) {
      this.toggleSaveLoading()
      this.validFlow()
      if (this.validErrors()) {
        return this.toggleSaveLoading()
      }
      this.setCellStyle(FlowHelper.getCells(this.graph).filter(c => c.flow))
      let flowTree = FlowHelper.encodeXML2JSON(this.graph)
      if (this.tempTasks) {
        Object.keys(this.tempTasks).forEach(t => {
          let id = t.slice(4)
          flowTree.tasks.forEach(c => {
            if (c.id == id) {
              c.attrs = JSON.parse(JSON.stringify(this.tempTasks[t]))
              if ('agree' in c.attrs) delete c.attrs.agree
              if ('reject' in c.attrs) delete c.attrs.reject
              if ('assigneeName' in c.attrs) delete c.attrs.assigneeName
              if (c.flow === 'flow') {
                c.subFlow = c.attrs.subFlow
                delete c.attrs.subFlow
              }
            }
          })
        })
      }

      // 子流程更换后重置recode标识
      this.modifySubFlowList &&
        flowTree.tasks.forEach(val => {
          if (this.modifySubFlowList.includes(val.id)) {
            val.recode = ''
          }
        })

      let params = {
        flowTree,
        processModelAttribute: this.processModelAttribute,
        releaseTag: !releaseTag ? 0 : 1
      }
      if (
        this.$route.params.modelId &&
        this.$route.params.modifyType &&
        Number(this.$route.params.modifyType) === 1
      ) {
        params.modelId = this.$route.params.modelId
      }
      this.$api.template
        .save(params, {
          headers: { groupId: this.$route.params.storeId },
          needToast: false
        })
        .then(r => {
          this.$message.success('保存成功')
          if (this.$route.params.storeId != undefined) {
            this.$router.replace(
              `/${this.$route.params.storeId}/setting/operation`
            )
          }
        })
        .catch(e => {
          this.toggleSaveLoading()
          if (parseInt(e.code, 10) === 10001) {
            this.showTemplateForm = true
            this.showTaskForm = false
            this.existTemplateLabel.push(this.processModelAttribute.label)
          } else {
            this.$message.error(e.message)
          }
        })
    },

    validFlow() {
      this.validStartEnd()
      this.validTasks()
      this.validConditions()
      this.validMergeBranch()
    },    

    validStartEnd() {
      let start = FlowHelper.getStartCells(this.graph)
      let end = FlowHelper.getEndCells(this.graph)
 
      if (start && start[0] && start[0].edges && start[0].edges.length > 1) {
        this.makeErrors({
          key: start[0].id,
          message: '开始节点只能关联一个其它节点',
          cell: start[0]
        })
      }

      if (start.length <= 0 || start.length > 1) {
        if (start.length <= 0) {
          this.makeErrors({ key: 'start', message: '缺少开始节点' })
        } else {
          start.forEach(s =>
            this.makeErrors({ key: s.id, message: '节点仅限一个', cell: s })
          )
        }
      } else {
        start = start[0]
        if (
          !start.edges ||
          start.edges.length < 1 ||
          !start.edges.every(
            e =>
              e &&
              e.target &&
              e.target !== e.source &&
              e.target !== start &&
              e.target !== end &&
              e.source !== end &&
              e.target.isVertex() &&
              e.source.isVertex()
          )
        ) {
          console.log('1')
          this.makeErrors({
            key: start.id,
            message: '节点必须至少关联一个非开始和结束的节点,不能被关联',
            cell: start,
            type: 0
          })
        }
      }
      if (end.length <= 0) {
        this.makeErrors({ key: 'terminal', message: '缺少结束节点' })
      } else {
        for (let i = 0; i < end.length; i++) {
          let ed = end[i]
          let b =
            !ed.edges ||
            ed.edges.length < 1 ||
            !ed.edges.every(
              e =>
                e &&
                e.target &&
                e.target === ed &&
                e.source &&
                e.source.isVertex() &&
                !/terminal|start/.test(e.source.flow)
            )
          if (b) {
            this.makeErrors({
              key: ed.id,
              cell: ed,
              type: 0,
              message: '节点必须至少被一个非开始和结束的节点关联,只能被关联'
            })
          }
          if (end.length > 1) {
            this.makeErrors({
              key: ed.id,
              cell: ed,
              type: 0,
              message: '节点仅限一个'
            })
          }
        }
      }
    },

    validMergeBranch() {
      let cells = FlowHelper.getCells(this.graph).filter(
        t => t.flow == 'merge' || t.flow == 'branch'
      )
      for (let i = 0; i < cells.length; i++) {
        let c = cells[i]
        let f = c.flow
        let edges = c.edges
        let node = '节点'
        if (!edges || edges.length < 1) {
          this.makeErrors({
            key: c.id,
            cell: c,
            type: 0,
            message: `${node}不能独立存在`
          })
        } else {
          let x
          let w
          let message
          if (f == 'merge') {
            x = edges.findIndex(e => e.source == c)
            w =
              edges
                .filter((e, n) => n != x)
                .findIndex(e => /merge|branch/.test(e.source.flow)) > -1
            message = `${node}必须关联一个其他类型的节点`
          }
          if (f == 'branch') {
            x = edges.findIndex(e => e.target == c)
            w =
              edges
                .filter((e, n) => n != x)
                .findIndex(e => /merge|branch/.test(e.target.flow)) > -1
            message = `${node}必须被一个其他类型的节点关联`
          }
          if (x == -1) {
            this.makeErrors({ key: c.id, cell: c, type: 0, message })
          }
          if (w) {
            this.makeErrors({
              key: c.id,
              cell: c,
              type: 0,
              message: `${node}不能互联分支或合并节点`
            })
          }
          let z = edges.length < 3
          if (z) {
            message =
              cells.flow == 'branch'
                ? `${node}必须关联两个以上的其他类型节点`
                : `${node}必须被两个以上的其他类型节点关联`
            this.makeErrors({ key: c.id, cell: c, type: 0, message })
          }
        }
      }
    },

    validConditions() {
      let conditions = FlowHelper.getConditions(this.graph)
      for (let i = 0; i < conditions.length; i++) {
        let c = conditions[i]
        if (!c.edges) {
          this.makeErrors({
            key: c.id,
            cell: c,
            type: 0,
            message: '决策节点不能独立存在'
          })
        } else {
          let t
          c.edges.forEach(e => {
            if (e.target.flow == 'start' || e.source.flow == 'start') {
              this.makeErrors({
                key: c.id,
                cell: c,
                type: 0,
                message: '节点不能关连开始节点'
              })
            }
            if (
              e.source.flow == 'condition' &&
              e.conditionExpression != 0 &&
              e.conditionExpression != 1
            ) {
              this.makeErrors({
                key: c.id,
                cell: c,
                type: 1,
                message: '请完善节点的流转规则配置'
              })
            }
          })
        }
      }
    },

    validTasks() {
      let tasks = FlowHelper.getTaskCells(this.graph)
      if (tasks.length <= 1) {
        this.makeErrors({
          key: 'task',
          cell: tasks[0],
          message: '至少包含两个任务节点'
        })
      } else {
        for (let i = 0; i < tasks.length; i++) {
          let t = tasks[i]
          const validMsg = this.validTask(tasks[i])
          validMsg.forEach(val => {
            this.makeErrors({
              key: t.id,
              cell: t,
              type: 0,
              message: val
            })
          })

          if (!this.validTaskAttr(tasks[i])) {
            this.makeErrors({
              key: t.id,
              cell: t,
              type: 0,
              message: '请检查节点的必填信息是否填写完善'
            })
          }
        }
      }
    },

    validTask(task) {
      const taskId = task.id
      const edges = task.edges || []
      const msg = []
      // 决策结点连接校验
      if (task.flow === 'condition') {
        let targetNum = 0
        let sourceNum = 0
        edges.forEach(val => {
          if (val.source.id === taskId) {
            sourceNum++
          }
          if (val.target.id === taskId) {
            targetNum++
          }
        })
        if (sourceNum !== 2) {
          msg.push('该节点必须关联两个其它节点')
        }
        if (targetNum < 1) {
          msg.push('该节点至少被一个节点关联')
        }
      }

      // 任务及子流程结点连接校验
      if (/task|robot|flow/.test(task.flow)) {
        let sourceNum = 0
        let targetNum = 0
        edges.forEach(val => {
          if (val.source.id === taskId) {
            sourceNum++
          }
          if (val.target.id === taskId) {
            targetNum++
          }
        })
        if (sourceNum > 1) {
          msg.push('该节点只能关联一个节点')
        }
        if (!sourceNum) {
          msg.push('该节点没有关联其它节点')
        }
        if (targetNum < 1) {
          msg.push('该节点至少被一个节点关联')
        }
      }
      return msg
    },

    validTaskAttr(cell) {
      let value = cell.value
      if (
        !this.tempTasks ||
        (!this.tempTasks['task' + cell.id] ||
          !this.tempTasks['task' + cell.id][
            /task|condition|robot/.test(cell.flow) ? 'label' : 'subFlow'
          ])
      ) {
        return false
      }
      return true
    },

    taskOnClick(cell) {
      if (!/task|condition|flow|robot/.test(cell.flow)) return
      this.activeCell = cell
      // this.taskType = cell.flow == 'task' ? 1 : cell.flow == 'condition' ? 2 : 3
      switch (cell.flow) {
        case 'task':
          this.taskType = 1
          break
        case 'condition':
          this.taskType = 2
          break
        case 'flow':
          this.taskType = 3
          break
        case 'robot':
          this.taskType = 4
          break
      }

      if (cell.flow == 'condition') {
        this.setYesNoTask(cell)
      }
      this.showTemplateForm = false
      this.showTaskForm = true

      this.createTask(cell)

      this.taskFormAtts = { ...this.tempTasks['task' + cell.id] }
    },

    updateCellAttrs(attrs) {
      attrs = JSON.parse(JSON.stringify(attrs))
      let id = 'task' + this.activeCell.id

      if (attrs.assignee) {
        const executor =
          this.persons.find(val => val.id === attrs.assignee) || {}
        attrs.assigneeName = executor.name || attrs.assignee
        this.setCellStyle([this.activeCell])
      } else {
        attrs.assigneeName = ''
      }
      Object.assign(this.tempTasks[id], attrs)
      FlowHelper.updateCellAttrs(this.graph, this.activeCell, attrs)
      const executor = this.persons.find(val => val.id === attrs.assignee)
    },

    toggleSaveLoading() {
      this.saveLoading = !this.saveLoading
    },

    updateCategories(data) {
      this.categories = data
    },

    setYesNoTask(cell) {
      if (!cell.edges || cell.edges.length <= 0) {
        this.nodesYesNo = []
        return
      }
      this.nodesYesNo = cell.edges
        .filter(e => e.source == cell && e.target)
        .map(({ id, target }) => {
          return {
            id,
            name: target.flow == 'terminal' ? '结束' : this.getValue(target)
          }
        })
      let old
      if (this.tempTasks && this.tempTasks['task' + cell.id]) {
        old = this.tempTasks['task' + cell.id]
        // 编辑后更新决策节点的缓存数据
        if (!this.nodesYesNo.some(e => e.id == old.agree)) {
          old.agree = ''
        }
        if (!this.nodesYesNo.some(e => e.id == old.reject)) {
          old.reject = ''
        }
      }
    },

    getValue(target) {
      let div = document.createElement('div')
      div.innerHTML = target.value
      return div.textContent || div.innerText
    },

    validErrors() {
      let keys = Object.keys(this.cellErrors)
      if (keys.length <= 0) {
        return false
      } else {
        let message = []
        let cells = []
        let n = 1
        keys.forEach(k => {
          let err = this.cellErrors[k]
          err.forEach(e => {
            let name = ''
            if (e.cell) {
              name =
                '【' +
                (e.cell.flow == 'start'
                  ? '开始'
                  : e.cell.flow == 'terminal'
                  ? '结束'
                  : this.getValue(e.cell)) +
                '】'
            }
            message.push(n + '. ' + name + e.message)
            n += 1
            if (e.cell) {
              cells.push(e.cell)
            }
          })
        })
        this.$message.error({
          dangerouslyUseHTMLString: true,
          duration: 7000,
          message: message.join('<br/><br/>')
        })
        if (cells.length > 0) {
          this.setCellStyle(cells, '#ED4040')
          //this.graph.setSelectionCells(cells)
          let ids = cells.map(c => c.id)
          this.setCellStyle(
            FlowHelper.getCells(this.graph).filter(
              c => ids.indexOf(c.id) < 0 && c.flow
            )
          )
        }
        if (keys.length == 1 && keys[0] == 'template') {
          this.setCellStyle(FlowHelper.getCells(this.graph).filter(c => c.flow))
        }
        this.cellErrors = {}
        return true
      }
    },

    makeErrors({ key, message, cell, type }) {
      this.cellErrors[key] = this.cellErrors[key] || []
      this.cellErrors[key].push({ message, cell, type })
    },

    setCellStyle(cells, color = '#999') {
      cells = cells || FlowHelper.getCells(this.graph).filter(c => c.flow)
      this.graph.setCellStyles('strokeColor', color, cells)
    }
  }
}

代码中flow.js是个人精心整理的快速接入mxclient的落地工具函数,鉴于篇幅原因,不做展示。其应用场景可用于各种时序图、流程图、电路图、作业图场景,支持动效果,可独立二开。有需要或者疑问的小伙伴们欢迎唾弃~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值