vueflow
自定义节点,自定义线,具体细节还未完善,实现效果:
1.安装vueflow
2.目录如下
3.
index.vue
<script setup>
import { ref } from 'vue'
import { VueFlow, useVueFlow } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import { ControlButton, Controls } from '@vue-flow/controls'
import { MiniMap } from '@vue-flow/minimap'
import { MarkerType } from '@vue-flow/core'
import useDragAndDrop from './components/useDnD'
import Sidebar from './components/Sidebar.vue'
const { onInit, onNodeDragStop, onConnect, addEdges, setViewport, toObject, addNodes, project } = useVueFlow()
const { onDragStart, onDragOver, onDrop, onDragLeave, isDragOver } = useDragAndDrop()
import EdgeWithButton from './components/EdgeWithButton.vue'
import { toPng, toJpeg, toBlob } from 'html-to-image'
// const flowContainer = ref(null)
// 导入自定义节点
import DataSetNode from './components/DataSetNode.vue'//数据集
import ConditionNode from './components/ConditionNode.vue'//条件
import AlgorithmsLibraryNode from './components/AlgorithmsLibraryNode.vue'//算法
// 节点
const nodes = ref([])
// 线
const edges = ref([])
// var drawer = ref(false)
// 线的默认颜色
const edgesStyle = {style: {// stroke: '#6366f1',strokeWidth: 1, // 设置线宽 },markerEnd: {type: MarkerType.ArrowClosed,// color: '#6366f1',// width: 6, // 箭头宽度// height: 12, // 箭头高度}}
// 初始化
onInit((vueFlowInstance) => {vueFlowInstance.fitView()
})
// 链接线
onConnect((connection) => {addEdges({...connection, // 保留原始连接属性type: 'button',...edgesStyle})
})
// 双击事件
// const handleNodeDoubleClick = (event, node) => {
// drawer.value = true
// }
// 阻止右键事件
const showContextMenu = (e) => {// e.preventDefault()
}
// 保存按钮
const saveNodes = () => {console.log(nodes.value)console.log(edges.value)edges.value.map(val => {val.type = null})console.log("保存")
}
</script><template><div class="dndflow" @drop="onDrop" @click.right.native="showContextMenu($event)"><!-- 顶部的按钮 --><div class="top-title-button"><div class="top-title">算法流程编辑</div><el-button type="primary" class="ybutton">运行</el-button><el-button type="success" class="ybutton" @click="saveNodes">保存</el-button></div><div ref="flowContainer" class="flow-container"><!-- @node-double-click="handleNodeDoubleClick" --><VueFlow v-model:nodes="nodes" v-model:edges="edges" class="basic-flow" :default-viewport="{ zoom: 1.5 }":min-zoom="0.2" :max-zoom="4" @dragover="onDragOver" @dragleave="onDragLeave"><template #edge-button="buttonEdgeProps"><!-- 删除线的删除按钮 --><EdgeWithButton :id="buttonEdgeProps.id" :source-x="buttonEdgeProps.sourceX":source-y="buttonEdgeProps.sourceY" :target-x="buttonEdgeProps.targetX" :target-y="buttonEdgeProps.targetY":source-position="buttonEdgeProps.sourcePosition" :target-position="buttonEdgeProps.targetPosition":marker-end="buttonEdgeProps.markerEnd" :style="buttonEdgeProps.style" /></template><template #node-data-set="props"><!-- 数据集节点 --><DataSetNode :id="props.id" :data="props.data"></DataSetNode></template><template #node-algorithms-library="props"><!-- 算法库节点 --><AlgorithmsLibraryNode :id="props.id" :data="props.data"></AlgorithmsLibraryNode></template><template #node-condition="props"><!-- 条件节点 --><ConditionNode :id="props.id" :data="props.data"></ConditionNode></template><!-- 背景 --><Background :gap="16" /><!-- 小地图 --><MiniMap /><!-- 小按钮 --><Controls position="bottom-center" /></VueFlow></div><!-- 左侧拖动面板 --><Sidebar /></div>
</template>
<style>
@import './main.css';
</style>
main.css
/* import the necessary styles for Vue Flow to work */
@import "@vue-flow/core/dist/style.css";/* import the default theme, this is optional but generally recommended */
@import "@vue-flow/core/dist/theme-default.css";html,
body,
#app {margin: 0;height: 100%;
}#app {text-transform: uppercase;font-family: 'JetBrains Mono', monospace;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;text-align: center;color: #2c3e50;
}.clearfix:after {content: "";display: block;clear: both;
}/* 最外层div样式 */
.dndflow {flex-direction: column;display: flex;height: 100%;width: calc(100% - 200px);position: absolute;left: 200px;
}.flow-container {width: 100%;height: calc(100% - 60px);background: white;border: 1px solid #ddd;
}/* 小地图 */
.vue-flow__minimap {transform: scale(75%);transform-origin: bottom right;
}/* 顶部标题及运行和保存按钮 */
.top-title-button {height: 60px;text-align: left;line-height: 60px;
}.top-title {display: inline-block;font-size: 30px;font-weight: 800;padding-left: 20px;font-weight: bold;/* color: #0f6cd6; */text-shadow:-2px -2px 0 #000;/* 1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000; */background-image: -webkit-linear-gradient(bottom, red, #fd8403, yellow);-webkit-background-clip: text;-webkit-text-fill-color: transparent;
}.ybutton {margin: 20px 10px 0;float: right;
}/* 工具行样式 */
.basic-flow .vue-flow__controls .vue-flow__controls-button svg {height: 16px;width: 16px;padding: 2px;
}/* 在 handle 内部添加 + 号 */
.vue-flow__handle {height: 12px;width: 12px;border-radius: 50%;
}.vue-flow__handle::after {content: "+";position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);font-size: 14px;color: #fff;pointer-events: none;/* 避免干扰拖拽事件 */
}/* 左侧面板 */
.left-panal {position: fixed;bottom: 0;left: 0;top: 0;margin: 0;background: linear-gradient(to left, #ba8beb, #c1e9e9);z-index: 5;width: 200px;
}.left-panal>div {margin: 10px auto;cursor: grab;
}/*左侧按钮 */
.vue-flow__node-default {/* border-width: 3px; */padding: 0;border: 1px solid #ca9fed;padding: 5px 10px;font-size: 16px;display: flex;align-items: center;justify-content: center;
}.vue-flow__node-default .el-icon {margin-right: 5px;
}/* 删除按钮 */
.edgebutton {width:15px;height:15px;line-height:15px;font-size: 12px;border: 1px solid #b0dee7;background: #ffffff;border-radius: 50%;cursor: pointer;color: #aaa;
}.edgebutton:hover {transform: scale(1.1);transition: all ease .5s;box-shadow: 0 0 0 1px #a8ddcb80, 0 0 0 2px #c0e4e4
}/* 节点样式 */
.custom-node {width: 180px;box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);position: relative;text-align: left;border: 1px solid #ddd;background: #fff;border-radius: 5px;padding:10px;
}.node-header {font-weight: bold;/* border-bottom: 1px solid #eee; */padding-bottom: 4px;
}.vue-flow__node.selected .custom-node {box-shadow: 0 1px 3px #6366f1 !important;border: 1px solid #6366f1 !important;
}.deletebtn {position: absolute;right: 5px;top: 0;cursor: pointer;
}.deletebtn .el-icon {margin: 5px 5px;width: 12px;height: 12px;
}.del-icon {color: #f00;
}.copy-icon {color: rgb(13, 67, 227);
}.edit-icon {color: rgb(10, 236, 232);
}.yxjgbtn {float: right;color: #6366f1;font-size: 14px;cursor: pointer;
}/* .btnList{cursor: pointer;}.btnList>p{cursor: pointer;text-align: center;font-size: 16px;border-bottom:1px solid #eee;margin: 0;padding: 5px 0;}.btnList>p:last-child{border: 0;} */
Sidebar.vue
<!-- 左侧拖动节点栏 -->
<script setup>
import useDragAndDrop from './useDnD'
const { onDragStart } = useDragAndDrop()
</script><template><aside class="left-panal"><!-- <div class="vue-flow__node-input" :draggable="true"@dragstart="(event) => onDragStart(event, { type: 'input', label: '开始' })">开始</div><div class="vue-flow__node-output" :draggable="true"@dragstart="(event) => onDragStart(event, { type: 'output', label: '结束' })">结束</div> --><div class="vue-flow__node-default" :draggable="true"@dragstart="(event) => onDragStart(event, 'algorithms-library')"><el-icon style="color: #532ff3;"><Memo /></el-icon>算法</div><div class="vue-flow__node-default" :draggable="true" @dragstart="(event) => onDragStart(event, 'data-set')"><el-icon style="color: #f34033;"><Files /></el-icon>数据集</div><div class="vue-flow__node-default" :draggable="true" @dragstart="(event) => onDragStart(event, 'condition')"><el-icon style="color: #077215;"><Connection /></el-icon>条件</div></aside>
</template>
useDnD.js
import { useVueFlow } from '@vue-flow/core'
import { ref, watch } from 'vue'/*** @returns {string} - A unique id.*/
function getId() {let id = Date.now();return `dndnode_${id}`
}/*** In a real world scenario you'd want to avoid creating refs in a global scope like this as they might not be cleaned up properly.* @type {{draggedType: Ref<string|null>, isDragOver: Ref<boolean>, isDragging: Ref<boolean>}}*/
const state = {/*** The type of the node being dragged.*/draggedType: ref(null),isDragOver: ref(false),isDragging: ref(false),
}export default function useDragAndDrop() {const { draggedType, isDragOver, isDragging } = stateconst { addNodes, screenToFlowCoordinate, onNodesInitialized, updateNode } = useVueFlow()watch(isDragging, (dragging) => {document.body.style.userSelect = dragging ? 'none' : ''})function onDragStart(event, type) {if (event.dataTransfer) {event.dataTransfer.setData('application/vueflow', type)event.dataTransfer.effectAllowed = 'move'}draggedType.value = typeisDragging.value = truedocument.addEventListener('drop', onDragEnd)}/*** Handles the drag over event.** @param {DragEvent} event*/function onDragOver(event) {event.preventDefault()if (draggedType.value) {isDragOver.value = trueif (event.dataTransfer) {event.dataTransfer.dropEffect = 'move'}}}function onDragLeave() {isDragOver.value = false}function onDragEnd() {isDragging.value = falseisDragOver.value = falsedraggedType.value = nulldocument.removeEventListener('drop', onDragEnd)}/*** Handles the drop event.** @param {DragEvent} event*/function onDrop(event) {const position = screenToFlowCoordinate({x: event.clientX,y: event.clientY,})const nodeId = getId()const newNode = {id: nodeId,type: draggedType.value,position,data: { label: nodeId },}/*** Align node position after drop, so it's centered to the mouse** We can hook into events even in a callback, and we can remove the event listener after it's been called.*/const { off } = onNodesInitialized(() => {updateNode(nodeId, (node) => ({position: { x: node.position.x - node.dimensions.width / 2, y: node.position.y - node.dimensions.height / 2 },}))off()})addNodes(newNode)}return {draggedType,isDragOver,isDragging,onDragStart,onDragLeave,onDragOver,onDrop,}
}
AlgorithmsLibraryNode.vue
<!-- CustomNode.vue -->
<template><div class="custom-node clearfix"><div class="deletebtn"><el-popconfirm class="box-item" title="确定删除该节点吗?" placement="top-start" @confirm="deleteNode(id)"><template #reference><el-icon class="del-icon"><Delete /></el-icon></template></el-popconfirm><el-icon class="copy-icon" @click="duplicateNode(id)"><DocumentCopy /></el-icon><el-icon class="edit-icon" @click="xgjd(id)"><EditPen /></el-icon><!-- <el-popover class="box-item" placement="top-start"><template #reference><el-icon><MoreFilled /></el-icon></template><div class="btnList"><p @click="deleteNode">删除</p><p>复制</p></div></el-popover> --></div><div class="node-header">算法</div><div @click="yxjg()" class="yxjgbtn">运行结果</div><Handle type="source" position="right" /><Handle type="target" position="left" /></div><!-- 运行结果 --><el-drawer v-model="draweryx" :with-header="false" size="20%" append-to-body><span>运行结果</span></el-drawer><!-- 点击节点弹出的弹出框 --><el-drawer v-model="drawerjd" :with-header="false" size="20%" append-to-body><span>修改节点</span></el-drawer></template><script setup>import { Handle } from '@vue-flow/core'import { useVueFlow } from '@vue-flow/core'const { removeNodes, getNodes, addNodes } = useVueFlow()var draweryx = ref(false)var drawerjd = ref(false)const props = defineProps({id: String,data: Object,selected: Boolean})// 运行结果事件const yxjg = (id) => {draweryx.value = true}// 修改节点事件const xgjd = (id) => {drawerjd.value = true}// 删除单个节点const deleteNode = (nodeId) => {removeNodes(nodeId)}// 复制指定节点const duplicateNode = (nodeId) => {const originalNode = getNodes.value.find(n => n.id === nodeId)if (!originalNode) return// 创建新节点(修改ID和位置)const newNode = {...originalNode,id: `${originalNode.id}-copy-${Date.now()}`, // 确保ID唯一position: {x: originalNode.position.x + 50, // 偏移位置y: originalNode.position.y + 50},selected: false // 取消选中状态}addNodes(newNode)}</script>
ConditionNode.vue
<!-- CustomNode.vue -->
<template><div class="custom-node clearfix" ><div class="deletebtn"><el-popconfirm class="box-item" title="确定删除该节点吗?" placement="top-start" @confirm="deleteNode(id)"><template #reference><el-icon class="del-icon"><Delete /></el-icon></template></el-popconfirm><el-icon class="copy-icon" @click="duplicateNode(id)"><DocumentCopy /></el-icon><el-icon class="edit-icon" @click="xgjd(id)"><EditPen /></el-icon></div><div class="node-header">条件</div><div v-for="(item, index) in data.conditions" v-if="data.conditions" class="conditionsNode"><p v-if="index == 0"><span>Case{{ index + 1 }}</span> <span class="caseif">If</span></p><p v-if="index != 0 && index != data.conditions.length - 1"><span>Case{{ index + 1 }}</span><spanclass="caseif">ElseIf</span></p><p v-if="index == data.conditions.length - 1"><span class="caseif">Else</span></p><div class="paramList" v-if="index != data.conditions.length - 1"><div v-for="(d, num) in item.rules"><p class="param"> {{ d.param }}{{ d.operator }}{{ d.value }}</p><p v-if="item.rules.length > 1 && item.rules.length - 1 != num" class="operator">{{ item.operator }}</p></div></div><Handle :position="Position.Right" type="source" :id="item.id + 'right_' + index"class="conditionsHandleNode"></Handle></div><Handle type="target" position="left" /><!-- <Handle v-for="(item, index) in conditions" :position="Position.Right" type="source" :id="'right_' + index":style="getDynamicHandlePos(item, index)"></Handle> --></div><!-- 点击节点弹出的弹出框 --><el-drawer v-model="drawerjd" size="20%" append-to-body :with-header="false"><div class="drawerTitle"><el-icon style="color: blueviolet;margin-right: 5px;"><Edit /></el-icon>条件节点</div><p class="nodedescribe">该组件用于根据前面的组件输出相应的引导执行流程,通过定义各种情况并指定操作,或不满足条件时采取默认操作,实现复杂的分支逻辑</p><div v-for="(item, index) in data.conditions" class="drawerCase" v-show="index!=data.conditions.length-1"><el-select v-model="item.operator" placeholder="选择" size="large"><el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" /></el-select><div></div></div><el-button type="success">Add Condition</el-button><!-- <div class="addcondition">Add Condition</div> --><el-button type="primary">Add Case</el-button><!-- <div class="addcase">Add Case</div> --></el-drawer>
</template><script setup>
import { Position, Handle } from '@vue-flow/core'
import { useVueFlow } from '@vue-flow/core'
import { onMounted } from 'vue'
const { removeNodes, getNodes, addNodes, updateNode } = useVueFlow()
var draweryx = ref(false)
var drawerjd = ref(false)
const options = [{value: 'AND',label: '与',},{value: 'OR',label: '或',},
]const props = defineProps({id: String,data: Object,selected: Boolean
})
const initconditions = () => {if (props.data.conditions) returnprops.data.conditions = [{operator: 'AND',rules: [{param: 'ceshi',operator: '>',value: '13',}, {param: 'ceshi',operator: '>',value: '13',}, {param: null,operator: null,value: null,}]}, {operator: null,rules: null}]
}
// 修改节点事件
const xgjd = (id) => {drawerjd.value = true
}
// 删除单个节点
const deleteNode = (nodeId) => {removeNodes(nodeId)
}
// 复制指定节点
const duplicateNode = (nodeId) => {const originalNode = getNodes.value.find(n => n.id === nodeId)if (!originalNode) return// 创建新节点(修改ID和位置)const newNode = {...originalNode,id: `${originalNode.id}-copy-${Date.now()}`, // 确保ID唯一position: {x: originalNode.position.x + 50, // 偏移位置y: originalNode.position.y + 50},selected: false // 取消选中状态}addNodes(newNode)
}
onMounted(() => {initconditions()const originalNode = getNodes.value.find(n => n.id === props.id)console.log(originalNode)
})
</script>
<style scoped>
.conditionsNode {width: 100%;position: relative;/* text-align: right; */
}.conditionsNode p {font-size: 14px;margin: 5px 0;
}.conditionsHandleNode {position: absolute;top: 10px;right: -10px;
}.caseif {float: right;
}.paramList {padding: 5px;background: #f8f6fe;
}.paramList .param {padding: 5px;background: #e2d6ff;
}.paramList .operator {text-align: center;font-size: 12px;font-weight: 800;
}.drawerTitle {font-size: 16px;font-weight: 800;display: flex;align-items: center;
}.nodedescribe {color: #666;font-size: 12px;
}
.drawerCase{background: #f8f6fe;padding: 5px;
}
</style>
DataSetNode.vue
<!-- CustomNode.vue -->
<template><div class="custom-node clearfix" ><div class="deletebtn"><el-popconfirm class="box-item" title="确定删除该节点吗?" placement="top-start" @confirm="deleteNode(id)"><template #reference><el-icon class="del-icon"><Delete /></el-icon></template></el-popconfirm><el-icon class="copy-icon" @click="duplicateNode(id)"><DocumentCopy /></el-icon><el-icon class="edit-icon" @click="xgjd(id)"><EditPen /></el-icon><!-- <el-popover class="box-item" placement="top-start"><template #reference><el-icon><MoreFilled /></el-icon></template>
<div class="btnList"><p @click="deleteNode">删除</p><p>复制</p>
</div>
</el-popover> --></div><div class="node-header">数据集</div><div @click="yxjg()" class="yxjgbtn">运行结果</div><Handle type="source" position="right" /><Handle type="target" position="left" /></div><!-- 运行结果 --><el-drawer v-model="draweryx" :with-header="false" size="20%" append-to-body><span>运行结果</span></el-drawer><!-- 点击节点弹出的弹出框 --><el-drawer v-model="drawerjd" :with-header="false" size="20%" append-to-body><span>修改节点</span></el-drawer>
</template><script setup>
import { Handle } from '@vue-flow/core'
import { useVueFlow } from '@vue-flow/core'
const { removeNodes, getNodes, addNodes } = useVueFlow()
var draweryx = ref(false)
var drawerjd = ref(false)const props = defineProps({id: String,data: Object,selected: Boolean
})// 运行结果事件
const yxjg = (id) => {draweryx.value = true
}
// 修改节点事件
const xgjd = (id) => {drawerjd.value = true
}
// 删除单个节点
const deleteNode = (nodeId) => {removeNodes(nodeId)
}
// 复制指定节点
const duplicateNode = (nodeId) => {const originalNode = getNodes.value.find(n => n.id === nodeId)if (!originalNode) return// 创建新节点(修改ID和位置)const newNode = {...originalNode,id: `${originalNode.id}-copy-${Date.now()}`, // 确保ID唯一position: {x: originalNode.position.x + 50, // 偏移位置y: originalNode.position.y + 50},selected: false // 取消选中状态}addNodes(newNode)
}</script>
EdgeWithButton.vue
<script setup>
import { BaseEdge, EdgeLabelRenderer, getBezierPath, useVueFlow } from '@vue-flow/core'
import { computed } from 'vue'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,},
})const { removeEdges } = useVueFlow()const path = computed(() => getBezierPath(props))
</script><script>
export default {inheritAttrs: false,
}
</script><template><!-- You can use the `BaseEdge` component 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"><div class="edgebutton" @click="removeEdges(id)">×</div></div></EdgeLabelRenderer>
</template>
<style scoped></style>