vue使用logicflow实现流程图

使用背景:

最近项目中需要实现一个流程配置图的功能,就是图片种效果。

刚开始也是一头雾水,后来在网上开了一些方案,最终决定使用 logicflow 来实现这个效果。

下面是我的实现之后的效果图,基本满足了我需要的功能。

什么是 LOgicFlow?

官网给出的解释:

LogicFlow是一款流程图编辑框架,提供了一系列流程图交互、编辑所必需的功能和灵活的节点自定义、插件等拓展机制。LogicFlow支持前端自定义开发各种逻辑编排场景,如流程图、ER图、BPMN流程等。在工作审批流配置、机器人逻辑编排、无代码平台流程配置都有较好的应ten

特性:

  • 可视化模型:通过 LogicFlow 提供的直观可视化界面,用户可以轻松创建、编辑和管理复杂的逻辑流程图。
  • 高可定制性:用户可以根据自己的需要定制节点、连接器和样式,创建符合特定用例的定制逻辑流程图。
  • 自执行引擎:执行引擎支持浏览器端执行流程图逻辑,为无代码执行提供新思路。

LogicFlow是 滴滴 推出的一款流程配置插件,可以快速实现一套流程配置方案,接下来开始介绍如何使用。

2、安装

npm install @logicflow/core --save

// 插件包,建议也一起安装,因为会用到其中的很多插件功能,根据自己的实际情况去选择是否安装
npm install @logicflow/extension --save

3、用法

这里我们是以 vue3 为例介绍其用法,其他框架的用法基本都大差不差,可以参考官网中的其他用法,官网地址

下面是我在项目中的详细写法:

注意:删除节点这个api,官网里面有一个大坑,删除节点 deleteNode,他写的是 deletaNode,当时真把我整抑郁了。

还有就是,自定义的html节点样式,不能加scoped,不然样式会不生效。

 <div class="container" ref="container"></div>
 <!-- 自定义鼠标右键菜单 -->
    <div id="menu">
      <ul>
        <li @click="handleDelete">删除</li>
      </ul>
    </div>
import { Control, DndPanel, Group, SelectionSelect } from '@logicflow/extension'
import LogicFlow from '@logicflow/core'
import '@logicflow/core/dist/index.css'
import MyGroup from './MyGroup'  // 自定义的节点配置,下面会详细的去介绍。
LogicFlow.use(Control)
// 流程图的dom
const container = ref()
// 流程图的dom实例
const lf = ref<LogicFlow>()
// 流程图 左侧默认菜单
const patternItems = [
  {
    type: 'rect',
    text: '开始',
    label: '开始节点',
    icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAAmCAYAAACoPemuAAAAAXNSR0IArs4c6QAABQVJREFUWEfNWF1oHFUU/s5sUhVTyc5GUBqlpDObWol9UYr2webNIqQgQrA1LdZgsjNNpBZbsQHrTy2VaqXdnU2qVhEsgpW2CmkFSQtqffGvNBKT2QYLUhCzs9EoKN2ZI3d2NzvZ7M9ksxEH9mXvOd/55tw7557vEKp8moZ+vt120i0SuIWZwyBaDYcCAE8yMEmgyfrlDV/8uvW2v6oJQQtxajz8fSMtu7kTRJ0EtPvyJRqGg/NSHY9M9ajf+fIB4ItY4+DkWsm2uyChE4xmD/iomx3GFEk0Jf5nh5uY0ESglQDf47FNA4jaaUR/71evVCJYkVjIMHUmvASG7AYGvpWYTzmSdCYVUUbLBbg1llAcwkYGbwQgfgIhCeaopbfuK+dblpgcT5wA82M5QgAdS2nKsUpvW2xdjo13gqQ+AOvFOgGfJjW1oxRWSWJyPHEVzHe6IEwDSV3ZXw2hQp9QLLGXiV/J/n/N0tQVxXCLEpONiR8AWpvJFG9JaeETtSCVwwgaE5sJ9EFmZ/knSw/fVYg/j5gcN3eC8YYwtDS14hlcDGHZMDnr/6alqTu9WHMChwbNTezgtGvg4AFrh/r1YgJX8pWj5v2QcDGTOedF7wcxSyw4dKWN0s4wCM0gPGNF1MOVgGuxnt8hTtppWpcrJbPE5Jh5CIRdohykNPXeWgT1iyEb5pfZr3V2S11ioqJLNzZcFsWTQT1+SkJjfGwloW4Xk/P2dG/rJb8kypSSDwGkpQDWiRvCJRaMTfQQ0eBCsiXHxveBpBcATDI721J6q3jrqh/ZMM8CeChXmjLEDHNE3H3MvD+lhwf8oHuIiWL5GzNttXTlnB/fYjbBmPkcEQ4wcCGlqe0Uio+vYJZ+EcaSJN031bvqGz/gXmJZ+78hBbqs3paTfvwLbeRBcw0c/OjymLFvocaj5gYpgPMAxixNXeMXtAixrCtvt7Twu35xvHayYYq7924wbSTZMLsBvMXAJylN3eQXsDQxcQ9Sf1JTjvrFytkFDfMMAR3M0CkUmzjIRLvBOG7p6pN+wcoRExgEPJ/U1AN+8YSdHDPfAWE7GK+TbCQ+BvgRInotGVH2+AWqREzgODbap/vUC34xQ/HEQWbeDaZTJMfNj8B4dCmIEfMTST38XnXEjImXARqo9VaC6HK6/oYNf3TfYfklNmcrQ3Gzixnv1/LwA7joLAtsnu5uueqXVLae5g9/MJ5YT8yiao9amtrmF6jMGfvsunPTlpkdzUm/WDk72UhccnWCKBdChjn29WtikYnaKvXxsyD5Kykfn/mkbNPjiX71n4WSEvrAJjaFX33D8oY5VxIxDyT1sK8Wel7GCMetiP9yU0g8ZCT6GHwERMNWRHm4Fpc4QHTEiihPLzRLBVV/2FVSTM9aunKo6rYnaCSeIvAQE72aiih7F0Uqo6Dmtz3Zqvv/axQFMVdtsz0ihG0t5VqlTOblXInWWgC4qhuIul/oEsi2QpJzZVwJMTJbBjzq+7+Sb8VUeXHB61HhSyHj5sg2oKgaLz0i8KjxWso5r6AupcKzbVPp4+kFEUKllkMVAPPUt5dJxRGAq85tRF0hnB1DgflcIBA4XUkfiD6ebXQQ4UGhgDKBazCGyr1BVqVvKzK4G2PALD64wyq3f88/tR3cedNbzahTyDGJ6XOq47M1H3UWO4VC9tlpSZUCUIhZZZIUsGj188Nh6c/0V1N7Vs9UKrLF1v8FJ5yEXcWLNvkAAAAASUVORK5CYII=',
  },
  {
    type: 'rect',
    label: '请求节点',
    icon: 'data:image/png;base64,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',
  },
  {
    type: 'rect',
    text: '结束',
    label: '结束节点',
    icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAAmCAYAAACoPemuAAAAAXNSR0IArs4c6QAAA+ZJREFUWEfNmF+oFHUUx7/nt3rpIeTO3MQMfLGZNRBS0JesBy1D8soVMyqKnrypO5tCPSRk4A1UsIcCb/tb/FMPgoJRkaXlg5ebgvmSiILi3d/US0Rl985chB5aunNidnZ2Z9bdnb17dZuFfdj9nT+fOef3O+fMj5DSD6WUC3MC6x9VazOCdSaqfOF5IEGTgjHpCdjOTvNWtw8+azDtk4mNRJkhCAyBeXGC45tE+JY9uujkjfOzgewYTCvaTxNjF8CvzMZBXZZOM2HUzRmXO9HvCEwrlvYT094Gg3cBXCHgEjP+ZBJ/+OvE3qNEWMTgNSB6Hoy+mB7jYydvvpMElwg2INVJBl6rGSIa85gPTz9SPo+Xl5fbOVhw/Fc9U/7nJYC3E7CqbgNXnJy5pp1uWzBdKm5QftOxzONJT9tsXZP2dgIfia45ltnSf8sFvah+BOOp0BAjs8K1lt7oBirU0aT9AoG/C38TcGrKMl9vZrMpmF5QH4HwdqjQ7sm6AY1mgokPuLns+4127gHTZGmQQGfrkaKNrmV83w1AKx1N/vIkYeZ6zQfRM42nNQ42zvP0W/YlUJBCBu1wLeNoKwd6YWIEQjA8j5z8spHZwMf3HJ12LOPVqH4MrOKIxL4AClddy1zdzpkm1TgBa32ZbtKtSfVTeFoZ8czEwDSpzhAw1Em0fJm5g9VPKoNHXSu7O3Iw6jHRpXIB9INQ/nf+Q4vvDi9xHmTEHj6sFvbNw28A5jNgu5Zp3gPWXyitFETXggU+51jZTUl7Zq4R8+3rUo0BeDbI0swK13qiUpJqqRwoqjeYcaL6594pyzzYC7ABqd5j4IDvSwCbJy3zmxiYVrT3E3PQDxnDTt78tBdgekFtAyHoJoRtTs78LAamy9IXAG0NuMQm13r8XC/ANPnzIMGr1E1m3uPmsx82gNlfAvzi/wpGtMfNGXGwgULpEBO9220qk6LrzWDd9C7zh0a5WCqB2pBQ2/y6VMMAjnW7+bsFi25+Yt46lc9+FUulP7+LDMaDVNLXrmVsSXLml4skmXCdZ/BB04hJ5Y/cG3y5aFTrld/vk7ftv6sT57Rzp7wII+0HwU6hWsp9frNPn+z7C8ACEP3u5IzHQtl4r5T2WYAHK/TA5ulqTZkzQAsD/VINCeBMsK/piJM3djYHizRxEI05OWP9g4Ly7epF+wKYn6twed6g+9ay6BAZcd0w9iBySu43YPSwAQljj+88nYNiNSypHK3DlKXyZaQGl8bXtxAulS+8IVwqrwjqcCm8VInWsNRdQzUrsNXGbxCzySQMwGMmcVuAJ5hwtacXd/e7A7Syl3gN1SuQRj//ARDQB0UouH96AAAAAElFTkSuQmCC',
  },
]

// 流程图默认显示的节点
const graphData = reactive({
  nodes: [
    {
      id: 'fba7fc7b-83a8-4edd-b4be-21f694a5d490',
      type: 'customHtml',
      // text: '开始',
      x: 200,
      y: 200,
      properties: {
        name: '开始',
      },
    },
    {
      id: 'fba7fc7b-83a8-4edd-b4be-21f694a5d491',
      type: 'customHtml',
      // text: '请求节点',
      x: 400,
      y: 200,
      properties: {
        name: '',
        index: 1,
        path: '',
        isEdit: true,
      },
    },
    {
      id: '681035e6-11e3-43d7-9392-1deed852c01a',
      type: 'customHtml',
      // text: '结束',
      x: 800,
      y: 200,
      properties: {
        name: '结束',
      },
    },
  ],
  edges: [
    {
      sourceNodeId: 'fba7fc7b-83a8-4edd-b4be-21f694a5d490',
      targetNodeId: 'fba7fc7b-83a8-4edd-b4be-21f694a5d491',
      type: 'bezier',
    },
    {
      sourceNodeId: 'fba7fc7b-83a8-4edd-b4be-21f694a5d491',
      targetNodeId: '681035e6-11e3-43d7-9392-1deed852c01a',
      type: 'bezier',
    },
  ],
})

onMounted(() => {
  lf.value = new LogicFlow({
    // 通过选项指定了渲染的容器和需要显示网格
    container: container.value,
    // 连线的类型  line:起点和终点中间 poyline:最长线段中间(折角) bezier: 曲线
    edgeType: 'bezier',
    grid: true,
    plugins: [DndPanel, SelectionSelect, Group, Control],
  })

  // 监听点击事件
  lf.value.on('node:click, edge:click', (data) => {
    console.log(data)
  })
  // 监听拖拽事件
  lf.value.on('node:drag', (event) => {
    console.log('正在拖拽的节点:', event.data.x)
  })
  // 监听新的节点生成
  lf.value.on('node:dnd-add', (data) => {
    console.log('节点:', data)
    console.log(data.data.x)
    lf.value.render(graphData)
  })
  // 监听连线 结束点不可进行连线
  lf.value.on('connection:not-allowed', (msg) => { // 监听连线
    ElMessage.error(msg.msg)
  })
  // 节点鼠标右键
  lf.value.on('node:contextmenu', ({ e, data }) => {
    console.log('右键:',e)
     const { x, y } = data.e
    showContextMenu(x - 200, y - 200)
  })
  // 线的右键
  lf.value.on('edge:contextmenu', (data, e) => {
  console.log('右键:',e)
   const { x, y } = data.e
    showContextMenu(x - 200, y - 200)
  })
  lf.value.extension.dndPanel.setPatternItems(patternItems)
  lf.value.register(MyGroup)
  lf.value.render(graphData)
})

/**
 * 鼠标右键删除
 */
const handleDelete = () => {
  if (deleteType.value === 'NODE') {  // 节点删除
    lf.value.deleteNode(checkedLfId.value)
    graphData.nodes = graphData.nodes.filter((item) => {
      return item.id !== checkedLfId.value
    })
    console.log(graphData.nodes)
  }
  if (deleteType.value === 'LINE') { // 连线删除
    lf.value.deleteEdgeByNodeId({
      sourceNodeId: checkedItem.value.sourceNodeId,
      targetNodeId: checkedItem.value.targetNodeId,
    })
  }
}

/**
 * 鼠标右键菜单显示
 * @param x
 * @param y
 */
const showContextMenu = (x, y) => {
  const menu = document.getElementById('menu')
  menu.style.left = `${x}px`
  menu.style.top = `${y}px`
  menu.style.display = 'block'
}
// 点击空白处时隐藏右键菜单
document.addEventListener('click', () => {
  const menu = document.getElementById('menu')
  menu.style.display = 'none'
})
<style>
.container {
  width: 100%;
  height: 50vh;
}
/* 自定义右键菜单 */
#menu{
  display: none;
  position: absolute;
  width: 150px;
  border:1px solid #ccc;
  background: #eee;
}
#menu ul {
  margin: 5px 0;
}
#menu li{
  height: 30px;
  line-height: 30px;
  color: #21232E;
  font-size: 12px;
  text-align: center;
  cursor: default;
  list-style-type: none;
  border-bottom:1px dashed #cecece ;
}
#menu li:hover {
  background-color: #cccccc;
}
.uml-wrapper {
  background: #fff;
  width: 100%;
  height: 100%;
  border-radius: 10px;
  border: 1px solid #000;
  box-sizing: border-box;
}

.uml-head {
  text-align: center;
  line-height: 30px;
  font-size: 16px;
  font-weight: bold;
}

.uml-body {
  border-top: 1px solid #000;
  padding: 5px 10px;
  font-size: 12px;
}

.uml-footer {
  padding: 5px 10px;
  font-size: 14px;
}

.uml-body-default {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>

自定义 html类型节点

import { HtmlNode, HtmlNodeModel} from '@logicflow/core'
// 自定义 HTML 节点
class CustomHtmlNode extends HtmlNode {
  setHtml(rootEl) {
    const { properties } = this.props.model
    const el = document.createElement('div')
    el.className = 'uml-wrapper'
    if (properties.index) {
      el.innerHTML = `
            <div>
              <div class="uml-head">节点ID: REQUEST_${properties.index}</div>
              <div class="uml-body">
                <div>服务名:${properties.name}</div>
                <div>路径:${properties.path}</div>
              </div>
            </div>
          `
      rootEl.innerHTML = ''
      rootEl.appendChild(el)
    } else {
      el.innerHTML = `
            <div class="uml-body-default">
              <div>${properties.name}</div>
          </div>
            `
      rootEl.innerHTML = ''
      rootEl.appendChild(el)
    }

    window.setData = () => {
      const { graphModel, model } = this.props
      graphModel.eventCenter.emit('custom:button-click', model)
    }
  }
}

// 自定义 HTML节点样式
class CustomHtmlNodeModel extends HtmlNodeModel {
  // 判断节点连线结束节点不能进行连线操作
  initNodeData(data) {
    super.initNodeData(data)
    const circleOnlyAsTarget = {
      message: '终止节点不能作为连线的起点',
      validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => {
        return sourceNode.properties.name !== '结束'
      },
    }
    this.sourceRules.push(circleOnlyAsTarget)
  }

  // 节点的具体样式
  setAttributes() {
    console.log('this.properties', this.properties)
    const { width, height, radius, isEdit } = this.properties
    this.width = 100
    if (isEdit) {
      this.width = 200
    }
    this.text.editable = false
    if (radius) {
      this.radius = radius
    }
  }
}
export default {
  type: 'customHtml',
  view: CustomHtmlNode,
  model: CustomHtmlNodeModel,
}

这样就可以实现一个简单的流程图效果了。有任何问题可以随时联系。

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值