当前位置:首页 > 技术分析 > 正文内容

Vue3 流程图组件库 :Vue Flow

ruisui882周前 (04-09)技术分析8

Vue Flow 是一个轻量级的 Vue 3 组件库,它允许开发者以简洁直观的方式创建动态流程图。本篇文章记录一下Vue Flow的基本用法


安装

npm add @vue-flow/core


流程图的构成

Nodes、Edges、Handles

主题

默认样式

通过导入样式文件应用

/* these are necessary styles for vue flow */
@import '@vue-flow/core/dist/style.css';

/* this contains the default theme, these are optional styles */
@import '@vue-flow/core/dist/theme-default.css';


对默认主题进行调整

1.可以使用css类名去覆盖

    .vue-flow__node-custom {
        background: purple;
        color: white;
        border: 1px solid purple;
        border-radius: 4px;
        box-shadow: 0 0 0 1px purple;
        padding: 8px;
    }


2.可以在组件上使用style或者class属性进行替换

    


3.通过在全局的css文件中对组件的样式变量进行覆盖

    :root {
        --vf-node-bg: #fff;
        --vf-node-text: #222;
        --vf-connection-path: #b1b1b7;
        --vf-handle: #555;
    }


具体的css类名和变量名可以通过查阅官方文档确认 Theming | Vue Flow

Nodes

Nodes是流程图中的一个基本组件,可以在图表中可视化任何类型的数据,独立存在并通过edges互连从而创建数据映射

1.展示节点

节点的渲染是通过给VueFlow组件的nodes参数传入一个数组实现的


<script setup>
import { ref, onMounted } from 'vue'
import { VueFlow, Panel } from '@vue-flow/core'

const nodes = ref([
  {
    id: '1',
    position: { x: 50, y: 50 },
    data: { label: 'Node 1', },
  }
]);

function addNode() {
  const id = Date.now().toString()
  
  nodes.value.push({
    id,
    position: { x: 150, y: 50 },
    data: { label: `Node ${id}`, },
  })
}
</script>



2.节点的增删

对于节点的增加和删除,我们可以通过直接改变nodes参数来实现,也可以使用 useVueFlow 提供的方法addNodes 和removeNodes直接改变组件内部的状态实现

3.节点的更新

节点的更新同样可以使用改变nodes参数来实现,也可以使用useVueFlow得到的实例instance上的updateNodeData,传入对应组件的id和数据对象来更新;

instance.updateNode(nodeId, { selectable: false, draggable: false })


通过对实例的findNode方法拿到的节点实例直接修改组件state同样能够起到更新节点的效果

const node = instance.findNode(nodeId) 
node.data = { ...node.data, hello: 'world', }


4.节点的类型

节点的类型通过nodes数组中对应节点项的type属性确定

默认节点(type:'default')

input节点(type:'input')

output节点(type:'output')

自定义节点(type:'custom', type:'special',...)

除了默认的节点类型,用户也可以创建自定义的节点类型

模板插槽模式

<script setup>
import { ref } from 'vue'
import { VueFlow } from '@vue-flow/core'

import CustomNode from './CustomNode.vue'
import SpecialNode from './SpecialNode.vue'

export const nodes = ref([
  {
    id: '1',
    data: { label: 'Node 1' },
    // this will create the node-type `custom`
    type: 'custom',
    position: { x: 50, y: 50 },
  },
  {
    id: '1',
    data: { label: 'Node 1' },
    // this will create the node-type `special`
    type: 'special',
    position: { x: 150, y: 50 },
  }
])
</script>


<script setup>
import { ref } from 'vue'
import { VueFlow } from '@vue-flow/core'

import CustomNode from './CustomNode.vue'
import SpecialNode from './SpecialNode.vue'

export const nodes = ref([
  {
    id: '1',
    data: { label: 'Node 1' },
    // this will create the node-type `custom`
    type: 'custom',
    position: { x: 50, y: 50 },
  },
  {
    id: '1',
    data: { label: 'Node 1' },
    // this will create the node-type `special`
    type: 'special',
    position: { x: 150, y: 50 },
  }
])
</script>



在配置了自定义组件后,VueFlow会将节点类型字段和插槽名字进行动态匹配,从而正确渲染。

Node-types对象模式

直接将引入的组件对象通过VueFlow的nodeTypes参数传入,需要注意的是要去除组件对象的响应式

<script setup>
import { markRaw } from 'vue'
import CustomNode from './CustomNode.vue'
import SpecialNode from './SpecialNode.vue'

const nodeTypes = {
  custom: markRaw(CustomNode),
  special: markRaw(SpecialNode),
}

const nodes = ref([
  {
    id: '1',
    data: { label: 'Node 1' },
    type: 'custom',
  },
  {
    id: '1',
    data: { label: 'Node 1' },
    type: 'special',
  }
])
</script>



节点事件

参考:Nodes | Vue Flow

Edges

Edges就是节点之间的连线部分,每一条连线都是从一个handle到另一个handle,其拥有独立的id;

展示Edges

Edges的渲染是通过给VueFlow组件的edges参数传入一个数组实现的,需要配合nodes一起确定节点之间的连线关系;

<script setup>
import { ref, onMounted } from 'vue'
import { VueFlow } from '@vue-flow/core'

const nodes = ref([
  {
    id: '1',
    position: { x: 50, y: 50 },
    data: { label: 'Node 1', },
  },
  {
    id: '2',
    position: { x: 50, y: 250 },
    data: { label: 'Node 2', },
  }
]);

const edges = ref([
  {
    id: 'e1->2',
    source: '1',
    target: '2',
  }
]);
</script>



增删和更新Edges

和节点的类似,可以通过直接改变edges传参实现,同时useVueFlow也提供了对Edges的操作方法[addEdges],(vueflow.dev/typedocs/in…) removeEdges

Edges的更新

同样和节点类型类似,可以通过useVueFlow拿到实例,使用实例的updateEdgeData方法进行更新,也可以使用findEdge拿到的edge直接修改对应的state进行更新

instance.updateEdgeData(edgeId, { hello: 'mona' }) 
edge.data = { ...edge.data, hello: 'world', }


Edges类型

默认连线(type:'default')

阶梯连线(type:'step')

直线连接(type:'straight')

自定义连接

用法和自定义节点类似,只是插槽名变为edge-开头,参数名由nodeTypes变为edgeTypes

edge事件

参考:Edges | Vue Flow

Handles

节点边缘上的小圆圈,使用拖拽的方式进行节点之间的连接

使用Handle

Handle是以组件的方式在节点中引入的

<script setup>
import { Handle } from '@vue-flow/core'
  
defineProps(['id', 'sourcePosition', 'targetPosition', 'data'])
</script>



Handle 位置

可以通过Handle组件的position参数来调整其位置

 


多个Handle使用时需要注意组件需要有唯一id

 


多个Handle在同一侧时需要手动调整位置防止重叠

 


Handle的隐藏

需要使用样式opacity,不能使用v-if和v-show



是否限制连接可以使用Handle组件的connectable参数,传入一个布尔值



连接模式



配置了ConnectionMode.Strict后只允许在相同类型的Handle之间进行连接

动态位置

在需要动态处理Handle的位置时,需要调用updateNodeInternals方法传入需要更新的节点id数组去应用,防止边缘未对其的情况出现。

import { useVueFlow } from '@vue-flow/core'

const { updateNodeInternals } = useVueFlow()

const onSomeEvent = () => {
  updateNodeInternals(['1'])
}


Composables

Vue Flow提供了一些用于获取流程图及其内部组件相关数据的API,可以参考文档 Composables | Vue Flow

Controlled Flow

Vue Flow同样提供了一些API用于对流程图的更新过程进行手动控制并且监听对应事件 受控流量 |Vue 流程 (vueflow.dev)

来看一下官方文档Demo

Layouting | Vue Flow 这个demo较全的使用到了Vue Flow中的一些基本用法:

1.主流程:App.vue:

import "@vue-flow/core/dist/style.css";
import "@vue-flow/core/dist/theme-default.css";
import { nextTick, ref } from "vue";
import { Panel, VueFlow, useVueFlow } from "@vue-flow/core";
import { Background } from "@vue-flow/background";
import Icon from "./icon.vue";
import ProcessNode from "./processNode.vue";
import AnimationEdge from "./animationEdge.vue";
import { initialEdges, initialNodes } from "./initialElements";
import { useRunProcess } from "./useRunProcess";
import { useShuffle } from "./useShuffle";
import { useLayout } from "./useLayout";

// 节点的初始化数据
const nodes = ref(initialNodes);

// 节点的连接关系
const edges = ref(initialEdges);

// 打乱节点之间的连接关系
const shuffle = useShuffle();

// useLayout 处理节点布局对齐等
const { graph, layout, previousDirection } = useLayout();

const { fitView } = useVueFlow();

// 将节点和连线随机化
async function shuffleGraph() {
  await stop();

  reset(nodes.value);

  edges.value = shuffle(nodes.value);

  nextTick(() => {
    layoutGraph(previousDirection.value);
  });
}

// 进行重新排版
async function layoutGraph(direction) {
  await stop();

  reset(nodes.value);

  nodes.value = layout(nodes.value, edges.value, direction);

  nextTick(() => {
    fitView();
    run(nodes.value);
  });
}




.layout-flow {
  background-color: #1a192b;
  height: 100%;
  width: 100%;
}

.process-panel,
.layout-panel {
  display: flex;
  gap: 10px;
}

.process-panel {
  background-color: #2d3748;
  padding: 10px;
  border-radius: 8px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
  display: flex;
  flex-direction: column;
}

.process-panel button {
  border: none;
  cursor: pointer;
  background-color: #4a5568;
  border-radius: 8px;
  color: white;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}

.process-panel button {
  font-size: 16px;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.checkbox-panel {
  display: flex;
  align-items: center;
  gap: 10px;
}

.process-panel button:hover,
.layout-panel button:hover {
  background-color: #2563eb;
  transition: background-color 0.2s;
}

.process-panel label {
  color: white;
  font-size: 12px;
}

.stop-btn svg {
  display: none;
}

.stop-btn:hover svg {
  display: block;
}

.stop-btn:hover .spinner {
  display: none;
}

.spinner {
  border: 3px solid #f3f3f3;
  border-top: 3px solid #2563eb;
  border-radius: 50%;
  width: 10px;
  height: 10px;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}


2.useShuffle.js

该文件提供的方法主要是用来随机打乱节点以及连线的关系

// 打乱数组的顺序
function shuffleArray(array) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
  }
}

// 根据节点数组生成一个可能的节点之间的映射关系
function generatePossibleEdges(nodes) {
  const possibleEdges = [];

  for (const sourceNode of nodes) {
    for (const targetNode of nodes) {
      if (sourceNode.id !== targetNode.id) {
        const edgeId = `e${sourceNode.id}-${targetNode.id}`;
        possibleEdges.push({
          id: edgeId,
          source: sourceNode.id,
          target: targetNode.id,
          type: "animation",
          animated: true
        });
      }
    }
  }

  return possibleEdges;
}

// 返回新的节点连接关系;
export function useShuffle() {
  return nodes => {
    const possibleEdges = generatePossibleEdges(nodes);
    shuffleArray(possibleEdges);

    const usedNodes = new Set();
    const newEdges = [];

    for (const edge of possibleEdges) {
      if (
        !usedNodes.has(edge.target) &&
        (usedNodes.size === 0 || usedNodes.has(edge.source))
      ) {
        newEdges.push(edge);
        usedNodes.add(edge.source);
        usedNodes.add(edge.target);
      }
    }

    return newEdges;
  };
}


3.useLayout.js

使用dagre对节点进行排版,返回排版后的图数据;

import dagre from "dagre";
import { ref } from "vue";
import { Position, useVueFlow } from "@vue-flow/core";

export function useLayout() {
  const { findNode } = useVueFlow();

  const graph = ref(new dagre.graphlib.Graph());

  const previousDirection = ref("LR");

  function layout(nodes, edges, direction) {
    const dagreGraph = new dagre.graphlib.Graph();

    graph.value = dagreGraph;

    // 设置默认的边标签
    dagreGraph.setDefaultEdgeLabel(() => ({}));

    const isHorizontal = direction === "LR";

    // 设置图布局
    dagreGraph.setGraph({ rankdir: direction });

    previousDirection.value = direction;

    for (const node of nodes) {
      // 查找到节点的信息
      const graphNode = findNode(node.id);
      // 设置节点
      dagreGraph.setNode(node.id, {
        width: graphNode.dimensions.width || 150,
        height: graphNode.dimensions.height || 50
      });
    }

    // 设置边
    for (const edge of edges) {
      dagreGraph.setEdge(edge.source, edge.target);
    }

    // 排版
    dagre.layout(dagreGraph);

    // 排版结束后返回新的节点状态
    return nodes.map(node => {
      const nodeWithPosition = dagreGraph.node(node.id);

      return {
        ...node,
        targetPosition: isHorizontal ? Position.Left : Position.Top,
        sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
        position: { x: nodeWithPosition.x, y: nodeWithPosition.y }
      };
    });
  }

  return { graph, layout, previousDirection };
}


4.useRunProcess.js

用于模拟流程运行过程中的各种状态

import { ref, toRef, toValue } from "vue";
import { useVueFlow } from "@vue-flow/core";

export function useRunProcess({ graph: dagreGraph, cancelOnError = true }) {
  const { updateNodeData, getConnectedEdges } = useVueFlow();

  const graph = toRef(() => toValue(dagreGraph));

  // 是否正在运行
  const isRunning = ref(false);

  //已执行的节点
  const executedNodes = new Set();

  // 当前正在执行的节点
  const runningTasks = new Map();

  // 即将执行的节点
  const upcomingTasks = new Set();

  async function runNode(node, isStart = false) {
    if (executedNodes.has(node.id)) {
      return;
    }

    // 加入到即将执行的节点
    upcomingTasks.add(node.id);

    // 过滤出指向当前节点的连线
    const incomers = getConnectedEdges(node.id).filter(
      connection => connection.target === node.id
    );

    // 等待进入动画全部执行完成
    await Promise.all(
      incomers.map(incomer => until(() => !incomer.data.isAnimating))
    );

    // 清空
    upcomingTasks.clear();

    if (!isRunning.value) {
      return;
    }

    // 节点加入到已经执行的节点
    executedNodes.add(node.id);

    // 更新节点的状态
    updateNodeData(node.id, {
      isRunning: true,
      isFinished: false,
      hasError: false,
      isCancelled: false
    });

    const delay = Math.floor(Math.random() * 2000) + 1000;

    return new Promise(resolve => {
      const timeout = setTimeout(
        async () => {
          // 获取当前节点的所有后续子节点
          const children = graph.value.successors(node.id);

          // 随机抛出错误
          const willThrowError = Math.random() < 0.15 if isstart willthrowerror updatenodedatanode.id isrunning: false haserror: true if tovaluecancelonerror await skipdescendantsnode.id runningtasks.deletenode.id ts-expect-error resolve return updatenodedatanode.id isrunning: false isfinished: true runningtasks.deletenode.id if children.length> 0) {
            await Promise.all(children.map(id => runNode({ id })));
          }
          resolve();
        },
        isStart ? 0 : delay
      );
      // 将当前任务加入到运行任务
      runningTasks.set(node.id, timeout);
    });
  }

  // 从起始节点开始执行的情况
  async function run(nodes) {
    if (isRunning.value) {
      return;
    }

    reset(nodes);

    isRunning.value = true;

    // 过滤出起始节点
    const startingNodes = nodes.filter(
      node => graph.value.predecessors(node.id)?.length === 0
    );

    // 调用runNode从起始节点执行
    await Promise.all(startingNodes.map(node => runNode(node, true)));

    clear();
  }

  //重置
  function reset(nodes) {
    clear();

    for (const node of nodes) {
      updateNodeData(node.id, {
        isRunning: false,
        isFinished: false,
        hasError: false,
        isSkipped: false,
        isCancelled: false
      });
    }
  }

  async function skipDescendants(nodeId) {
    const children = graph.value.successors(nodeId);

    for (const child of children) {
      updateNodeData(child, { isRunning: false, isSkipped: true });
      await skipDescendants(child);
    }
  }

  // 暂停运行
  async function stop() {
    isRunning.value = false;

    for (const nodeId of upcomingTasks) {
      clearTimeout(runningTasks.get(nodeId));
      runningTasks.delete(nodeId);
      updateNodeData(nodeId, {
        isRunning: false,
        isFinished: false,
        hasError: false,
        isSkipped: false,
        isCancelled: true
      });
      await skipDescendants(nodeId);
    }

    for (const [nodeId, task] of runningTasks) {
      clearTimeout(task);
      runningTasks.delete(nodeId);
      updateNodeData(nodeId, {
        isRunning: false,
        isFinished: false,
        hasError: false,
        isSkipped: false,
        isCancelled: true
      });
      await skipDescendants(nodeId);
    }

    executedNodes.clear();
    upcomingTasks.clear();
  }

  function clear() {
    isRunning.value = false;
    executedNodes.clear();
    runningTasks.clear();
  }

  return { run, stop, reset, isRunning };
}

// 等待直到condition为true
async function until(condition) {
  return new Promise(resolve => {
    const interval = setInterval(() => {
      if (condition()) {
        clearInterval(interval);
        resolve();
      }
    }, 100);
  });
}


5.processNode.js

流程图节点组件,根据节点状态显示不同的样式

import { computed, toRef } from 'vue'
import { Handle, useHandleConnections } from '@vue-flow/core'

const props = defineProps({
  data: {
    type: Object,
    required: true,
  },
  sourcePosition: {
    type: String,
  },
  targetPosition: {
    type: String,
  },
})

const sourceConnections = useHandleConnections({
  type: 'target',
})

const targetConnections = useHandleConnections({
  type: 'source',
})

// 判断是发送节点还是接收节点
const isSender = toRef(() => sourceConnections.value.length <= 0 const isreceiver='toRef(()'> targetConnections.value.length <= 0 const bgcolor='computed(()'> {
  if (isSender.value) {
    return '#2563eb'
  }

  if (props.data.hasError) {
    return '#f87171'
  }

  if (props.data.isFinished) {
    return '#42B983'
  }

  if (props.data.isCancelled) {
    return '#fbbf24'
  }

  return '#4b5563'
})

const processLabel = computed(() => {
  if (props.data.hasError) {
    return ''
  }

  if (props.data.isSkipped) {
    return ''
  }

  if (props.data.isCancelled) {
    return ''
  }

  if (isSender.value) {
    return ''
  }

  if (props.data.isFinished) {
    return ''
  }

  return ''
})
</script>







总结

文章主要介绍了如何使用 Vue Flow 库的基本概念和使用:

1.安装

2.基础组件:

  • Nodes:图中的基本单元,用于表示数据。
  • Edges:连接节点的连线。
  • Handles:节点上的小圆圈,用于连接节点。

3.主题定制

可以通过以下方式调整默认样式:

  • 覆盖 CSS 类名:通过 CSS 类名来自定义节点样式。
  • 组件属性:在 Vue 组件上使用 style 或 class 属性。
  • 全局 CSS 变量:在全局 CSS 文件中覆盖样式变量。

4.节点(Nodes)

  • 节点展示:通过传入 nodes 数组到 VueFlow 组件来展示节点。
  • 节点增删:可以通过改变 nodes 参数或使用 useVueFlow 提供的 addNodes 和 removeNodes 方法。
  • 节点更新:可以直接修改 nodes 参数或使用 updateNodeData 方法。
  • 节点类型:包括默认节点、输入节点、输出节点和自定义节点。

5.连线(Edges)

  • 连线展示:通过传入 edges 数组到 VueFlow 组件来展示连线。
  • 连线增删和更新:类似于节点,可以通过改变 edges 参数或使用 useVueFlow 提供的方法。
  • 连线类型:支持默认连线、阶梯连线、直线连线和自定义连线。

6.Handles

Handles 用于连接节点,可以自定义位置、多个 Handle 配置、动态更新和显示/隐藏等。


作者:Lumen丶
链接:
https://juejin.cn/post/7407683752712388608

扫描二维码推送至手机访问。

版权声明:本文由ruisui88发布,如需转载请注明出处。

本文链接:http://www.ruisui88.com/post/3392.html

标签: vue 流程图
分享给朋友:

“Vue3 流程图组件库 :Vue Flow” 的相关文章

快速上手React

web前端三大主流框架1、Angular大家眼里比较牛的框架,甚至有人说三大框架中只有它能称得上一个完整的框架,因为它包含的东西比较完善,包含模板,数据双向绑定,路由,模块化,服务,过滤器,依赖注入等所有功能。对于刚开始学习使用框架的小伙伴们,可以推荐这个框架,学会之后简直能颠覆之前你对前端开发的认...

如何在GitLab上回退指定版本的代码?GitLab回退指定版本问题分析

在Git中,回退到指定版本并不是删除或撤销之前的提交,而是创建一个新的提交,该提交包含指定版本的内容。这意味着您需要将当前代码更改与指定版本之间的差异进行比较,并将其合并到一个新的提交中。如果您没有更新本地代码,并且您希望将 GitLab 仓库回退到指定版本,您可以使用以下命令:git fetchg...

国产操作系统上Vim的详解03--安装和使用插件 | 统信 | 麒麟 | 中科方德

原文链接:国产操作系统上Vim的详解03--使用Vundle插件管理器来安装和使用插件 | 统信 | 麒麟 | 中科方德Hello,大家好啊!今天给大家带来一篇在国产操作系统上使用Vundle插件管理器来安装和使用Vim插件的详解文章。Vundle是Vim的一款强大的插件管理器,可以帮助我们轻松地安...

「云原生」Containerd ctr,crictl 和 nerdctl 命令介绍与实战操作

一、概述作为接替Docker运行时的Containerd在早在Kubernetes1.7时就能直接与Kubelet集成使用,只是大部分时候我们因熟悉Docker,在部署集群时采用了默认的dockershim。在V1.24起的版本的kubelet就彻底移除了dockershim,改为默认使用Conta...

15款测试html5响应式的在线工具

手机、平板灯手持设备的增多,网站要顺应变化,就必须要做响应式开发,响应式网站最大的特点在于可以在不同设备下呈现不同的布局,是基于html5+css3技术,目前越来越多的网站开始采用了响应式设计,而下面15款工具可以方便测试你的html5响应式效果。Responsinatorhttp://www.re...

双子座应用程序推出模型切换器以在Android上访问2.0

#头条精品计划# 快速导读谷歌推出了Gemini 2.0 Flash实验版,现已在其安卓应用中可用,之前仅在gemini.google.com网站上提供。新版本的15.50包含模型切换器,用户可以在设置中选择不同模型,包括1.5 Pro、1.5 Flash和2.0 Flash实验版。谷歌提醒,2.0...