Vue Flow实现流程编辑器_vue-flow
仿阿里百炼的流程管理,如果只想看Vue Flow的使用,直接跳到第三步
一、技术栈
前端UI库:ant-design-vue 4.2.5 Ant Design of Vue - Ant Design Vue (antdv.com)
第三方库:Vue Flow 1.41.4 Introduction | Vue Flow
vue3 简介 | Vue.js (vuejs.org)
二、关于ant-design-vue
说明:使用的vue3 + js,没有使用ts
vite.config.js中配置
- 使用自动按需引入组件,避免项目打包体积过大
下载插件:作用【在项目中就无需一个个手动按需引入】
npm install unplugin-vue-components/vite unplugin-vue-components/resolvers -D
- 自动导入vue中hook reactive ref等
下载插件:作用【vue3中的方法自动导入】
npm install unplugin-auto-import/vite -D
三、Vue Flow的使用
版本说明:
Node.js v20 或更高版本、Vue 3.3 或更高版本,我使用的是:node 20.18.0,vue 3.5.12
准备工作:
- 下载Vue Flow:npm install @vue-flow/core
- 引入vue flow样式:
@import \'@vue-flow/core/dist/style.css\';
@import \'@vue-flow/core/dist/theme-default.css\';
使用
import { onMounted, markRaw } from \'vue\' import { VueFlow, useVueFlow, Panel } from \'@vue-flow/core\' import { Background } from \'@vue-flow/background\' //背景 import { Controls } from \'@vue-flow/controls\' //控制(自带的缩放、居中、加锁功能) import { MiniMap } from \'@vue-flow/minimap\' //缩略图 import toolbar from \'@/components/toolBar/index.vue\' //工具栏 import ItemPanel from \'@/components/ItemPanel/index.vue\' //节点面板 // 自定义节点 import StartNode from \'./node/StartNode.vue\' import EndNode from \'./node/EndNode.vue\' import JudgeNode from \'./node/JudgeNode.vue\' // 自定义边 import ButtonEdge from \'./edge/ButtonEdge.vue\' //引入hooks import useVueFlowHooks from \'@/hooks/useVueFlow\' import { initialEdges, initialNodes } from \'./initial-elements\' // 定义emit函数 const emit = defineEmits([\'addNode\',\'flowListShow\',\'custom-event\']) // 接收数据 const props = defineProps({ layoutHeight: { type: Number, default: 54 }, tabsHeight: { type: Number, default: 50 }, layoutFooter: { type: Number, default: 30 }, flow: { type: Object, default: () => ({}) } }) //数据 let nodes = ref([]) let edges = ref([]) const { basicFlow, HandlerAddNode, handleConnect, onEdgeUpdate, flowListShowHandler, nodeClick } = useVueFlowHooks(nodes, edges, emit, props) const { fromObject } = useVueFlow() const nodeTypes = { // 将节点类型名称映射到组件定义/名称的对象 Start: markRaw(StartNode), End: markRaw(EndNode), Judge: markRaw(JudgeNode) } const edgeTypes = { // 将边类型名称映射到组件定义/名称的对象 Button: markRaw(ButtonEdge) } /** * 生命周期 */ // 挂载完毕 onMounted(() => { fromObject(props.flow) }) // 销毁前 onBeforeUnmount(() => { }) .vue-flow__node{ background: var(--vf-node-bg); border-radius: 8px; } .vue-flow__node.selected{ border:1px dashed var(--node-border-color); } .vue-flow__handle{ width: 10px; height: 10px; } .vue-flow__minimap{ background-color: #fff; } .vue-flow__controls{ display: flex; } :deep(.toolbar-panel){ margin: 0; width: 100%; } :deep(.aside-panel){ margin: 62px 0; } .nodeContainer{ background: var(--vf-node-bg); border-radius: 8px; } .nodeContainer:hover{ box-shadow: 0 0 10px 10px rgba(0, 0, 0, .05); transition-duration: .2s; }
自定义节点
这里以开始节点为例
{{ ID }} ID 流程参数设置 参数 来源 类型 模型识别 业务透传 string number boolean 删除 增加输入参数 import { Position, Handle, } from \'@vue-flow/core\' import { ref, onMounted, onUpdated, onBeforeUnmount } from \'vue\' import useStartNode from \'@/hooks/nodeHooks/useStartNode\' // 接收父组件传来的数据 const props = defineProps({ id: { type: String, }, data: { type: Object, }, }) // 数据 const { deleteNode, copyNode, addConfig, deleteConfig } = useStartNode(props.data) let ID = ref(props.id) let nodeData = ref(props.data) // console.log(\'【开始节点】接收————————ID:\', ID.value,) // console.log(\'【开始节点】接收————————data:\', data.value) //挂载完毕 onMounted(() => { }) // 更新完毕 onUpdated(() => { }) // 销毁前 onBeforeUnmount(() => { })@import \"@/assets/css/nodeHeader.css\";.Start{ border: 1px dashed transparent; font-size: 14px; width: 360px; gap: 4px;}.custom-content{ border-radius: 0 0 8px 8px; padding: 0 0 12px; .custom-formGroup{ background: #f8fafc; padding: 6px 12px; .formGroup-top{ margin-bottom: 6px; display: flex; align-items: center; } .formGroup-field{ align-items: center; display: flex; gap: 12px; margin-bottom: 6px; } .formGroup-line{ align-items: center; display: flex; gap: 12px; margin-bottom: 6px; } .linkBtn{ padding: 4px; } .ant-btn{ display: flex; gap: 8px; align-items: center; } }}
自定义边
<!-- Use the `EdgeLabelRenderer` to escape the SVG world of edges and render your own custom label in a `` ctx --> export default { //禁止 Vue 自动将非 props 属性添加到组件的根元素上 inheritAttrs: false, } import { BaseEdge, EdgeLabelRenderer, getBezierPath, useVueFlow } from \'@vue-flow/core\' import { computed } from \'vue\' // 接收props const props = defineProps({ id: { type: String, required: true, }, sourceX: { type: Number, // required: true, }, sourceY: { type: Number, // required: true, }, targetX: { type: Number, // required: true, }, targetY: { type: Number, // required: true, }, sourcePosition: { type: String, // required: true, }, targetPosition: { type: String, // required: true, }, markerEnd: { type: String, required: false, }, style: { type: Object, required: false, default: () => ({ // stroke: \'#1890ff\', }), }, }) const { removeEdges } = useVueFlow() const path = computed(() => getBezierPath(props)).ant-btn-circle{ border: none;}
侧边栏
import { watch } from \"vue\" import useItemPanel from \"@/hooks/useItemPanel.js\" // 定义 emit 函数 const emit = defineEmits([\"addNode\"]) // 接收父组件传值 const props = defineProps({ flow: { type: Object, default: () => ({}) } }) const { activeKey, itemPannel, refNodeList, baseNodelist, modelNodeList, functionNodeList, menuItemBorderLeftColor, getIconClass, handleDragStart, handleDragEnd, nodesData, isDisabledStartNode } = useItemPanel(props) // 监听 watch([nodesData.value], (val)=>{ emit(\"addNode\", nodesData.value) }) // 挂载完毕 onMounted(() => { // 进入页面判断是否有开始节点,如果有开始节点则禁用添加节点 isDisabledStartNode() }) // 销毁前 onBeforeUnmount(() => { }).ant-collapse{ background-color: #f8fafc!important; border: none;}.ant-collapse-item{ border-bottom: 1px solid #eee;}:deep(.ant-collapse-content){ border-top: 1px solid #eee;}.menuItem{ display: flex; gap: 8px; align-items: center; border: 1px dashed #d9d9d9; border-radius: 8px; padding: 0 16px; height: 40px; margin-bottom: 8px; background: rgba(255, 255, 255, 0.6); cursor: pointer;}.menuItem.disabled { pointer-events: none; opacity: 0.5; cursor: not-allowed;}.label{ flex: 1;}