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

news2025/4/20 12:10:17

本文是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()}`, // 动态生成唯一 id
    data: {
      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">
            <div
              class="drag-item start-node"
              draggable="true"
              @dragstart="onDragStart($event, '开始')"
              @dragend="onDragEnd"
            >
              <span>开始</span>
            </div>
          </ElCol>
          <ElCol :span="12">
            <div
              class="drag-item end-node"
              draggable="true"
              @dragstart="onDragStart($event, '结束')"
              @dragend="onDragEnd"
            >
              <span>结束</span>
            </div>
          </ElCol>
        </ElRow>
        <ElScrollbar height="75%">
          <div
            class="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>
        <ElInput
          v-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 定义 -->
    <Handle
      type="source"
      :position="Position.Top"
      :id="'top-' + id"
      :style="{ background: '#4a5568' }"
    />
    <Handle
      type="source"
      :position="Position.Left"
      :id="'left-' + id"
      :style="{ background: '#4a5568' }"
    />
    <Handle
      type="source"
      :position="Position.Right"
      :id="'right-' + id"
      :style="{ background: '#4a5568' }"
    />
    <Handle
      type="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.coloradmin.cn/o/2308714.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何通过 LlamaIndex 将数据导入 Elasticsearch

作者&#xff1a;来自 Elastic Andre Luiz 逐步介绍如何使用 RAG 和 LlamaIndex 提取数据并进行搜索。 在本文中&#xff0c;我们将使用 LlamaIndex 来索引数据&#xff0c;从而实现一个常见问题搜索引擎。 Elasticsearch 将作为我们的向量数据库&#xff0c;实现向量搜索&am…

Boosting

Boosting 学习目标 知道boosting集成原理和实现过程知道bagging和boosting集成的区别知道AdaBoost集成原理 Boosting思想 Boosting思想图 每一个训练器重点关注前一个训练器不足的地方进行训练通过加权投票的方式&#xff0c;得出预测结果串行的训练方式 1 什么是boosting 随着…

【通俗讲解电子电路】——从零开始理解生活中的电路(一)

导言&#xff1a;电子电路为什么重要&#xff1f; ——看不见的“魔法”&#xff0c;如何驱动你的生活&#xff1f; 清晨&#xff0c;当你的手机闹钟响起时&#xff0c;你可能不会想到&#xff0c;是电子电路在精准控制着时间的跳动&#xff1b;当你用微波炉加热早餐时&#…

LeetCode72编辑距离(动态规划)

给你两个单词 word1 和 word2&#xff0c; 请返回将 word1 转换成 word2 所使用的最少操作数 。 你可以对一个单词进行如下三种操作&#xff1a; 插入一个字符 删除一个字符 替换一个字符 示例 1&#xff1a; 输入&#xff1a;word1 “horse”, word2 “ros” 输出&#xf…

【K8S】Kubernetes 基本架构、节点类型及运行流程详解(附架构图及流程图)

Kubernetes 架构 k8s 集群 多个 master node 多个 work nodeMaster 节点&#xff08;主节点&#xff09;&#xff1a;负责集群的管理任务&#xff0c;包括调度容器、维护集群状态、监控集群、管理服务发现等。Worker 节点&#xff08;工作节点&#xff09;&#xff1a;实际运…

Windows版FFmpeg使用及B站视频下载示例python源码

Windows版FFmpeg使用及B站视频下载示例python源码 FFmpeg介绍和下载 FFmpeg 是一个功能强大、灵活且广泛使用的多媒体处理工具&#xff0c;无论是在专业领域还是日常使用中&#xff0c;都能满足各种多媒体处理需求。FFmpeg 是一个开源项目&#xff0c;遵循 LGPL 或 GPL 许可。…

飞书考勤Excel导入到自己系统

此篇主要用于记录Excel一行中&#xff0c;单条数据的日期拿取&#xff0c;并判断上下班打卡情况。代码可能满足不了大部分需求&#xff0c;目前只够本公司用&#xff0c;如果需要&#xff0c;可以参考。 需要把飞书月度汇总的考勤表导入系统中可以参考下。 下图为需要获取的年…

【leetcode hot 100 560】和为K的子数组

解法一&#xff1a;用左右指针寻找字串&#xff0c;如果和>k&#xff0c;则减少一个数&#xff08;left&#xff09;&#xff1b;如果和<k&#xff0c;则加上一个数&#xff08;right&#xff09;。 class Solution {public int subarraySum(int[] nums, int k) {int nu…

EGO-Planner的无人机视觉选择(yolov5和yolov8)

EGO-Planner的无人机视觉选择&#xff08;yolov5和yolov8&#xff09; 效果 yolov5检测效果 yolov8检测效果 一、YOLOv8 vs YOLOv5&#xff1a;关键差异解析 1. 训练效率&#xff1a;为何YOLOv8更快&#xff1f; 架构轻量化 YOLOv8采用C2f模块&#xff08;Cross Stage Partia…

性能测试分析和调优

步骤 性能调优的步骤 性能调优的步骤&#xff1a; 1.确定问题&#xff1a;根据性能测试的结果来分析确定bug。–测试人员职责 2.分析原因&#xff1a;分析问题产生的原因。----开发人员职责 3.给出解决方案&#xff1a;可以是修改软件配置、增加硬件资源配置、修改代码等----…

阿里云oss文件上传springboot若依java

一、第一步 引入依赖 <!-- 阿里云OSS --> <dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId> </dependency> 二、第二步 application.yml #阿里云oss服务配置 aliyun:oss:endpoint: …

使用create_sql_query_chain工具根据自然语言问题生成SQL查询,踩坑版

1. 开启调试模式 from langchain import debugdebug True # 启用调试模式说明&#xff1a; 这里从 langchain 库中导入了一个名为 debug 的变量&#xff08;或模块&#xff09;&#xff0c;然后将它设置为 True。这通常用来启用调试模式&#xff0c;方便开发者在程序运行时看…

无人机自主导航与避障技术!

自主导航的实现 环境感知&#xff1a;通过传感器&#xff08;如摄像头、激光雷达、超声波传感器等&#xff09;获取周围环境信息。 地图构建&#xff1a;利用SLAM&#xff08;同步定位与地图构建&#xff09;技术&#xff0c;实时生成环境地图并确定无人机的位置。 路径规划…

密码学(哈希函数)

4.1 Hash函数与数据完整性 数据完整性&#xff1a; 检测传输消息&#xff08;加密或未加密&#xff09;的修改。 密码学Hash函数&#xff1a; 构建某些数据的简短“指纹”&#xff1b;如果数据被篡改&#xff0c;则该指纹&#xff08;以高概率&#xff09;不再有效。Hash函数…

嵌入式开发工程师笔试面试指南-HR面试常见问题汇总

在嵌入式领域的招聘面试中,HR 通过一系列精心设计的问题,全面考察候选人的综合素质、专业能力以及与岗位的匹配度。以下从多个关键方面汇总了 HR 在嵌入式面试中常见的问题。 ** 一、语言表达方面 请简单介绍一下你自己这是面试开场常见问题,旨在让候选人做一个自我展示,…

Docker 搭建 Gitlab 服务器 (完整详细版)

参考 Docker 搭建 Gitlab 服务器 (完整详细版)_docker gitlab-CSDN博客 Docker 安装 (完整详细版)_docker安装-CSDN博客 Docker 日常命令大全(完整详细版)_docker命令-CSDN博客 1、Gitlab镜像 # 查找Gitlab镜像 docker search gitlab # 拉取Gitlab镜像 docker pull gitlab/g…

MongoDB安全管理

MongoDB如何鉴权 保证数据的安全性是数据库的重大职责之一。与大多数数据库一样&#xff0c;MongoDB内部提供了一套完整的权限防护机制。如下例所示&#xff1a; mongo --host 127.0.0.1 --port 27017 --username someone --password errorpass --authenticationDatabasestor…

架构案例:从初创互联网公司到分布式存储与反应式编程框架的架构设计

文章目录 引言一、初创互联网公司架构演化案例1. 万级日订单级别架构2. 十万级日订单级别架构3. 百万级日订单级别架构 二、分布式存储系统 Doris 架构案例三、反应式编程框架架构案例总结 引言 分布式架构 今天我们将探讨三种不同类型的架构案例&#xff0c;分别探讨 一个初…

神经网络之CNN图像识别(torch api 调用)

1.简介 CNN 是受生物学上感受野机制启发而提出的。它通过卷积操作自动提取数据中的特征&#xff0c;避免了传统机器学习方法中复杂的特征工程过程&#xff0c;能够自动学习到数据中的有效特征&#xff0c;从而进行分类、识别等任务。 2.结构 2.1卷积&#xff1a; 假设你有一…

使用Truffle、Ganache、MetaMask、Vue+Web3完成的一个简单区块链项目

文章目录 概要初始化Truffle项目创建编写合约编译合约配置Ganache修改truffle-config.js文件编写迁移文件部署合约使用Truffle 控制台使用MetaMask和VueWeb3与链交互 概要 使用Truffle、Ganache、MetaMask、VueWeb3完成的一个简单区块链项目。 初始化Truffle项目 安装好truf…