Vue Flow实现流程编辑器

Vue Flow实现流程编辑器

仿阿里百炼的流程管理,如果只想看Vue Flow的使用,直接跳到第三步

一、技术栈

前端UI库:ant-design-vue 4.2.5        Ant Design of Vue - Ant Design Vue (antdv.***)

第三方库: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-***ponents/vite unplugin-vue-***ponents/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

准备工作:
  1. 下载Vue Flow:npm install @vue-flow/core
  2. 引入vue flow样式:

@import '@vue-flow/core/dist/style.css';

@import '@vue-flow/core/dist/theme-default.css';

使用
<template>
    <VueFlow 
        class="basic-flow"
        :style="basicFlow" 
        v-model:nodes="nodes" 
        v-model:edges="edges"
        :default-viewport="{ zoom: 1 }" 
        :min-zoom="0.2" 
        :max-zoom="4"
        :node-types="nodeTypes"
        :edge-types="edgeTypes"
        @connect="handleConnect"
        @edge-update="onEdgeUpdate"
        @nodeClick="nodeClick">
        <Panel class="toolbar-panel">
            <toolbar @flowListShow="flowListShowHandler"/>
        </Panel>
        <Panel class="aside-panel">
            <ItemPanel @addNode="HandlerAddNode" :flow="flow"></ItemPanel>
        </Panel>
        <!-- 画布 -->
        <Background :style="{ background: '#f9fafd' }"/>
        <!-- 小地图 -->
        <MiniMap />
        <!-- 控制器 -->
        <Controls position="bottom-center"/>
    </VueFlow>
</template>

<script setup name="VueFlowIndex">
    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 '@/***ponents/toolBar/index.vue'    //工具栏
    import ItemPanel from '@/***ponents/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(() => {
        
    })
</script>
<style lang="less">
    .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;
    }
    
</style>
<style scoped lang="less">
    .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;
    }

    
</style>

自定义节点

这里以开始节点为例

<!-- 开始节点 -->
<template>
    <div class="nodeContainer Start">
        <div class="custom-head">
            <span class="custom-head-title">
                <span class="icon iconfont icon-kaishi"></span>
                {{ ID }}
            </span>
            <div class="custom-head-actions" v-if="data.type !== 'Start'">
                <span class="id">ID</span>
                <span class="icon iconfont icon-fuzhi nodrag" @click="copyNode"></span>
                <span class="icon iconfont icon-shanchu nodrag" @click="deleteNode(ID)"></span>
            </div>
        </div>
        <div class="custom-content">
            <div class="custom-formGroup">
                <div class="formGroup-top">流程参数设置</div>
                <div class="formGroup-field" v-if="nodeData.startConfig.length">
                    <div style="width: 85px;">参数</div>
                    <div style="width: 85px;">来源</div>
                    <div style="width: 100px;">类型</div>
                </div>
                <div class="formGroup-line" v-for="(item,index) in nodeData.startConfig">
                    <a-input class="nodrag" placeholder="请输入参数" style="width: 85px;" v-model:value="item.field" />
                    <a-select class="nodrag" ref="select" v-model:value="item.source" style="width: 85px" placeholder="请选择来源">
                        <a-select-option value="1">模型识别</a-select-option>
                        <a-select-option value="2">业务透传</a-select-option>
                    </a-select>
                    <a-select class="nodrag" ref="typeSelect" v-model:value="item.type" style="width: 85px" placeholder="请选择类型">
                        <a-select-option value="string">string</a-select-option>
                        <a-select-option value="number">number</a-select-option>
                        <a-select-option value="boolean">boolean</a-select-option>
                    </a-select>
                    <a-button type="link" class="linkBtn nodrag" @click="deleteConfig(index)">删除</a-button>
                </div>
                <a-button type="primary" class="nodrag" @click="addConfig">
                    <span class="icon iconfont icon-zengjia"></span>增加输入参数
                </a-button>
            </div>
            <Handle type="source" :position="Position.Right" />
        </div>
    </div>
</template>

<script setup name="StartNode">
    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(() => {
        
    })
</script>


<style lang="less" scoped>
@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;
        }
    }
}
</style>

自定义边

<template>
    <!-- You can use the `BaseEdge` ***ponent to create your own custom edge more easily -->
    <BaseEdge :id="id" :style="style" :path="path[0]" :marker-end="markerEnd" />

    <!-- Use the `EdgeLabelRenderer` to escape the SVG world of edges and render your own custom label in a `<div>` ctx -->
    <EdgeLabelRenderer>
        <div :style="{
            pointerEvents: 'all',
            position: 'absolute',
            transform: `translate(-50%, -50%) translate(${path[1]}px,${path[2]}px)`,
        }" class="nodrag nopan">
            <a-button shape="circle" @click="removeEdges(id)">
                <span class="icon iconfont icon-shanchu"></span>
            </a-button>
        </div>
    </EdgeLabelRenderer>
</template>
<script>
    export default {
        //禁止 Vue 自动将非 props 属性添加到组件的根元素上
        inheritAttrs: false,
    }
</script>
<script setup name="ButtonEdge">
    import { BaseEdge, EdgeLabelRenderer, getBezierPath, useVueFlow } from '@vue-flow/core'
    import { ***puted } 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 = ***puted(() => getBezierPath(props))

</script>
<style lang="less" scoped>
.ant-btn-circle{
    border: none;
}
</style>

侧边栏

<template>
    <a-collapse v-model:activeKey="activeKey" expandIconPosition="end">
        <a-collapse-panel v-for="(item,index) in itemPannel" :key="item.key" :header="item.header">
            <template v-if="item.nodeType === 'baseNode'">
                <div 
                    class="menuItem" 
                    :class="{'disabled': baseItem.draggable === false}"
                    :style="menuItemBorderLeftColor(baseItem)" 
                    v-for="(baseItem,index) in baseNodelist" :key="index"
                    :draggable="baseItem.draggable"
                    @dragstart="(e) => handleDragStart (e, baseItem)"
                    @dragend="(e) => handleDragEnd (e, baseItem)">
                    <span :class="getIconClass(baseItem)"></span>
                    <span class="label">{{ baseItem.data.name }}</span>
                    <span class="icon iconfont icon-catalog"></span>
                </div>
            </template>
            <template v-if="item.nodeType === 'modelNode'">
                <div 
                    class="menuItem" 
                    :style="menuItemBorderLeftColor(model)" 
                    v-for="(model,index) in modelNodeList" :key="index">
                    <span :class="getIconClass(model)"></span>
                    <span class="label">{{ model.data.name }}</span>
                    <span class="icon iconfont icon-catalog"></span>
                </div>
            </template>
            <template v-if="item.nodeType === 'refNode'">
                <div 
                    class="menuItem" 
                    :style="menuItemBorderLeftColor(refItem)" 
                    v-for="(refItem,index) in refNodeList" :key="index">
                    <span :class="getIconClass(refItem)"></span>
                    <span class="label">{{ refItem.data.name }}</span>
                    <span class="icon iconfont icon-catalog"></span>
                </div>
            </template>
            <template v-if="item.nodeType === 'functionNode'">
                <div 
                    class="menuItem" 
                    :style="menuItemBorderLeftColor(fItem)" 
                    v-for="(fItem,index) in functionNodeList" :key="index">
                    <span :class="getIconClass(fItem)"></span>
                    <span class="label">{{ fItem.data.name }}</span>
                    <span class="icon iconfont icon-catalog"></span>
                </div>
            </template>
        </a-collapse-panel>
    </a-collapse>
</template>

<script setup name="ItemPanel">
    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(() => {
        
    })

</script>
<style scoped lang="less">
.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;
}
</style>

转载请说明出处内容投诉
CSS教程网 » Vue Flow实现流程编辑器

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买