Vue-Flow绘制流程图(Vue3+ElementPlus+TS)简单案例

embedded/2025/3/1 23:00:26/

本文是vue3+Elementplus+ts框架编写的简单可拖拽绘制案例。

1.效果图:

2.Index.vue主代码:

<script lang="ts" setup>
import { ref, markRaw } from "vue";
import {VueFlow,useVueFlow,MarkerType,type Node,type Edge
} from "@vue-flow/core";
import { Background } from "@vue-flow/background";
import { Controls } from "@vue-flow/controls";
import { MiniMap } from "@vue-flow/minimap";
import "@vue-flow/core/dist/style.css";
import "@vue-flow/core/dist/theme-default.css";
import CustomNode from "./components/CusInfoNode.vue";
import {ElMessageBox,ElNotification,ElButton,ElRow,ElCol,ElScrollbar,ElInput,ElSelect,ElOption
} from "element-plus";const {onInit,onNodeDragStop,onConnect,addEdges,getNodes,getEdges,setEdges,setNodes,screenToFlowCoordinate,onNodesInitialized,updateNode,addNodes
} = useVueFlow();const defaultEdgeOptions = {type: "smoothstep", // 默认边类型animated: true, // 是否启用动画markerEnd: {type: MarkerType.ArrowClosed, // 默认箭头样式color: "black"}
};// 节点
const nodes = ref<Node[]>([{id: "5",type: "input",data: { label: "开始" },position: { x: 235, y: 100 },class: "round-start"},{id: "6",type: "custom", // 使用自定义类型data: { label: "工位:流程1" },position: { x: 200, y: 200 },class: "light"},{id: "7",type: "output",data: { label: "结束" },position: { x: 235, y: 300 },class: "round-stop"}
]);const nodeTypes = ref({custom: markRaw(CustomNode) // 注册自定义节点类型
});// 线
const edges = ref<Edge[]>([{id: "e4-5",type: "straight",source: "5",target: "6",sourceHandle: "top-6",label: "测试1",markerEnd: {type: MarkerType.ArrowClosed, // 使用闭合箭头color: "black"}},{id: "e4-6",type: "straight",source: "6",target: "7",sourceHandle: "bottom-6",label: "测试2",markerEnd: {type: MarkerType.ArrowClosed, // 使用闭合箭头color: "black"}}
]);onInit(vueFlowInstance => {vueFlowInstance.fitView();
});onNodeDragStop(({ event, nodes, node }) => {console.log("Node Drag Stop", { event, nodes, node });
});onConnect(connection => {addEdges(connection);
});const pointsList = ref([{ name: "测试1" }, { name: "测试2" }]);
const updateState = ref("");
const selectedEdge = ref<{id: string;type?: string;label?: string;animated?: boolean;
}>({ id: "", type: undefined, label: undefined, animated: undefined });const onEdgeClick = ({ event, edge }) => {selectedEdge.value = edge; // 选中边updateState.value = "edge";console.log("选中的边:", selectedEdge.value);
};function updateEdge() {// 获取当前所有的边const allEdges = getEdges.value;// 切换边类型:根据当前类型来切换const newType =selectedEdge.value.type === "smoothstep" ? null : "smoothstep";// 更新选中边的类型setEdges([...allEdges.filter(e => e.id !== selectedEdge.value.id), // 移除旧的边{...selectedEdge.value,type: selectedEdge.value.type,label: selectedEdge.value.label} as Edge // 更新边的类型]);
}function removeEdge() {ElMessageBox.confirm("是否要删除该连线?", "删除连线", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {const allEdges = getEdges.value;setEdges(allEdges.filter(e => e.id !== selectedEdge.value.id));ElNotification({type: "success",message: "连线删除成功"});updateState.value = null;selectedEdge.value = { id: "", type: undefined, label: undefined };});
}const selectedNode = ref<{id: string;data: { label: string };type: string;position: { x: number; y: number };class: string;
}>({id: "",data: { label: "" },type: "",position: { x: 0, y: 0 },class: ""
});const onNodeClick = ({ event, node }) => {selectedNode.value = node; // 更新选中的节点updateState.value = "node";console.log("选中的节点:", node);
};function removeNode() {ElMessageBox.confirm("是否要删除该点位?", "删除点位", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {const allNodes = getNodes.value;setNodes(allNodes.filter(e => e.id !== selectedNode.value.id));const allEdges = getEdges.value;setEdges(allEdges.filter(e =>e.source !== selectedNode.value.id &&e.target !== selectedNode.value.id));ElNotification({type: "success",message: "点位删除成功"});updateState.value = null;selectedNode.value = {id: "",data: { label: "" },type: "",position: { x: 0, y: 0 },class: ""};});
}const dragItem = ref<Node>(null);// 拖拽开始时设置拖拽的元素
function onDragStart(event, state) {dragItem.value = {id: `node-${Date.now()}`, // 动态生成唯一 iddata: {label:state === "开始" ? "开始" : state === "结束" ? "结束" : "工位:" + state},type: state === "开始" ? "input" : state === "结束" ? "output" : "custom",position: { x: event.clientX, y: event.clientY },class:state === "开始"? "round-start": state === "结束"? "round-stop": "light"};
}// 拖拽结束时清除状态
function onDragEnd() {dragItem.value = null;
}// 拖拽目标画布区域时允许放置
function onDragOver(event) {console.log("onDragOver事件:", event);event.preventDefault();
}function onDrop(event) {console.log("onDrop事件:", event);const position = screenToFlowCoordinate({x: event.clientX,y: event.clientY});const newNode = {...dragItem.value,position};const { off } = onNodesInitialized(() => {updateNode(dragItem.value?.id, node => ({position: {x: node.position.x - node.dimensions.width / 2,y: node.position.y - node.dimensions.height / 2}}));off();});// 更新节点数据dragItem.value = null;addNodes(newNode); //这里是画布上增加updateNodeData(newNode); //更新后端数据console.log("新节点:", newNode);console.log("新节点后List", nodes.value);
}const saveFlow = () => {console.log("保存数据nodes:", nodes.value);console.log("保存数据edges", edges.value);
};function updateNodeData(node: Node) {//更新后端数据console.log("更新后端数据:", node);nodes.value.push(node);
}
</script><template><div class="flow-container"><VueFlow:nodes="nodes":edges="edges":default-viewport="{ zoom: 1 }":min-zoom="0.2":max-zoom="4"@node-click="onNodeClick"@edge-click="onEdgeClick"@drop="onDrop"@dragover="onDragOver":node-types="nodeTypes":default-edge-options="defaultEdgeOptions":connect-on-click="true"><Background pattern-color="#aaa" :gap="16" /><MiniMap /></VueFlow><div class="top-container"><Controls class="controls" /><div class="save-btn"><ElButton type="primary" class="mr-2" @click="saveFlow">保存</ElButton></div></div><div class="left-panel"><div class="drag-items"><ElRow :gutter="10"><ElCol :span="12"><divclass="drag-item start-node"draggable="true"@dragstart="onDragStart($event, '开始')"@dragend="onDragEnd"><span>开始</span></div></ElCol><ElCol :span="12"><divclass="drag-item end-node"draggable="true"@dragstart="onDragStart($event, '结束')"@dragend="onDragEnd"><span>结束</span></div></ElCol></ElRow><ElScrollbar height="75%"><divclass="drag-item custom-node"draggable="true"@dragstart="onDragStart($event, item.name)"@dragend="onDragEnd"v-for="(item, index) in pointsList":key="index"><span>{{ item.name }}</span></div></ElScrollbar></div></div><div class="right-panel" v-if="updateState"><div class="panel-header"><span>{{updateState === "edge" ? "连接线规则配置" : "点位规则配置"}}</span><ElButton circle class="close-btn" @click="updateState = ''">×</ElButton></div><div class="panel-content" v-if="updateState === 'edge'"><ElInput v-model="selectedEdge.label" placeholder="线名称" clearable /><ElSelect v-model="selectedEdge.type" placeholder="线类型"><ElOption label="折线" value="smoothstep" /><ElOption label="曲线" value="default" /><ElOption label="直线" value="straight" /></ElSelect><ElSelect v-model="selectedEdge.animated" placeholder="线动画"><ElOption label="开启" :value="true" /><ElOption label="关闭" :value="false" /></ElSelect><ElButton type="primary" @click="updateEdge">修改</ElButton><ElButton type="danger" @click="removeEdge">删除</ElButton></div><div class="panel-content" v-else><ElInputv-model="selectedNode.data.label"placeholder="点位名称"clearable/><ElButton type="danger" @click="removeNode">删除</ElButton></div></div></div>
</template><style scoped>
.flow-container {position: relative;height: 100vh;
}.top-container {position: absolute;top: 0;width: 100%;display: flex;justify-content: space-between;padding: 10px;border-bottom: 1px solid #e4e7ed;
}.left-panel {position: absolute;left: 0;top: 120px;width: 200px;padding: 10px;background: rgba(245, 247, 250, 0.9);border-right: 1px solid #e4e7ed;
}.right-panel {position: absolute;right: 0;top: 60px;width: 200px;padding: 10px;background: rgba(245, 247, 250, 0.9);border-left: 1px solid #e4e7ed;
}.drag-item {padding: 8px;margin: 5px 0;border-radius: 4px;text-align: center;cursor: move;
}.start-node {background-color: rgba(103, 194, 58, 0.8);color: white;
}.end-node {background-color: rgba(245, 108, 108, 0.8);color: white;
}.custom-node {background-color: rgba(64, 158, 255, 0.8);color: white;
}.panel-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 15px;
}.panel-content {display: grid;gap: 10px;
}.controls {position: relative;top: -4px;left: -10px;
}
</style>

3. CusInfoNode.vue自定义客户Node

<script setup lang="ts">
import { defineProps } from "vue";
import { Handle, Position } from "@vue-flow/core";defineProps({id: String,data: Object
});
</script><template><div class="custom-node"><div class="node-header">{{ data.label }}</div><!-- Handle 定义 --><Handletype="source":position="Position.Top":id="'top-' + id":style="{ background: '#4a5568' }"/><Handletype="source":position="Position.Left":id="'left-' + id":style="{ background: '#4a5568' }"/><Handletype="source":position="Position.Right":id="'right-' + id":style="{ background: '#4a5568' }"/><Handletype="source":position="Position.Bottom":id="'bottom-' + id":style="{ background: '#4a5568' }"/></div>
</template><style scoped>
.custom-node {width: 120px;height: 40px;border-radius: 3px;background-color: #4a5568;color: white;position: relative;display: flex;justify-content: center;align-items: center;
}.node-header {font-size: 14px;font-weight: bold;
}
</style>


http://www.ppmy.cn/embedded/169171.html

相关文章

Python 爬虫与网络安全有什么关系

Python爬虫和网络安全之间存在密切的关系。爬虫是一种用于自动化从网络上获取信息的程序&#xff0c;而网络安全是保护计算机网络和系统免受未经授权的访问、攻击和数据泄露的实践。本文将探讨Python爬虫与网络安全之间的关系以及如何在爬虫开发中注意网络安全。 爬虫的作用和…

网络安全与认知安全的区别 网络和安全的关系

前言 说说信息安全 与网络安全 的关系 一、包含和被包含的关系 信息安全包括网络安全&#xff0c;信息安全还包括操作系统安全&#xff0c;数据库安全 &#xff0c;硬件设备和设施安全&#xff0c;物理安全&#xff0c;人员安全&#xff0c;软件开发&#xff0c;应用安全等。…

3、HTTP请求报文和响应报文是怎样的,有哪些常见的字段?【中高频】

HTTP请求报文主要是由 请求行、请求头部、空行和请求体 四部分组成&#xff08;第一行必须是一个请求行&#xff08;request line&#xff09;&#xff0c;用来说明请求类型、要访问的 资源 以及使用的HTTP版本。紧接着是一个首部&#xff08;header&#xff09;小节&#xff0…

深度探索:DeepSeek与鸿蒙HarmonyOS应用开发的深度融合

文章目录 一、概述1.1 什么是DeepSeek&#xff1f;1.2 鸿蒙HarmonyOS的特点 二、技术优势与应用场景2.1 技术优势2.2 应用场景 三、开发指南3.1 环境搭建3.2 集成AI模型3.3 分布式任务调度 四、实际案例分析4.1 智能家居控制4.2 智能健康监测 五、未来展望《AI智能化办公&#…

【愚公系列】《Python网络爬虫从入门到精通》033-DataFrame的数据排序

标题详情作者简介愚公搬代码头衔华为云特约编辑,华为云云享专家,华为开发者专家,华为产品云测专家,CSDN博客专家,CSDN商业化专家,阿里云专家博主,阿里云签约作者,腾讯云优秀博主,腾讯云内容共创官,掘金优秀博主,亚马逊技领云博主,51CTO博客专家等。近期荣誉2022年度…

9、HTTP/2与HTTP/1.1的区别?【高频】

二进制协议&#xff1a; HTTP/2 不再像 HTTP/1.1 里的纯文本形式的报文&#xff0c;而是全面采用了二进制格式&#xff0c;报文头部和数据体都是二进制&#xff0c;并且统称为帧&#xff08;frame&#xff09;&#xff1a;头信息帧&#xff08;Headers Frame&#xff09;和数据…

BigDecimal 为什么可以不丢失精度?

本文已收录至Java面试网站&#xff1a;https://topjavaer.cn 大家好&#xff0c;今天咱们来聊聊 Java 中的 BigDecimal。在金融领域&#xff0c;数据的精确性相当重要&#xff0c;一个小数点的误差可能就意味着几百万甚至几千万的损失。而 BigDecimal 就是专门用来解决这种高精…

SQL Server 链接服务器 MySQL 详细步骤

目录 前言 一、准备工作 1. 确认需求 2. 获取权限 二、安装必要的驱动程序和工具 1.下载并安装MySQL ODBC驱动&#xff1a; 2.安装 SQL Server 和 MySQL 的管理工具&#xff1a; 三、配置 SQL Server 以连接到MySQL 1.执行创建链接服务器的T-SQL语句&#xff1a; 2.配…