仿阿里百炼的流程管理,如果只想看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
二、关于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
准备工作:
- 下载Vue Flow:npm install @vue-flow/core
- 引入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>