likes
comments
collection
share

Vue3 流程图组件库 :Vue FlowVue Flow 是一个轻量级的 Vue 3 组件库,它允许开发者以简洁直观的

作者站长头像
站长
· 阅读数 21

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属性进行替换

    <VueFlow
    :nodes="nodes"
    :edges="edges"
    class="my-diagram-class"  
    :style="{ background: 'red' }"
    />

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>

<template>
  <VueFlow :nodes="nodes">
    <Panel>
      <button type="button" @click="addNode">Add a node</button>
    </Panel>
  </VueFlow>
</template>

2.节点的增删

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

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')

Vue3 流程图组件库 :Vue FlowVue Flow 是一个轻量级的 Vue 3 组件库,它允许开发者以简洁直观的

input节点(type:'input')

Vue3 流程图组件库 :Vue FlowVue Flow 是一个轻量级的 Vue 3 组件库,它允许开发者以简洁直观的

output节点(type:'output')

Vue3 流程图组件库 :Vue FlowVue Flow 是一个轻量级的 Vue 3 组件库,它允许开发者以简洁直观的

自定义节点(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>

<template>
  <VueFlow :nodes="nodes">
    <template #node-custom="customNodeProps">
      <CustomNode v-bind="customNodeProps" />
    </template>
    
    <template #node-special="specialNodeProps">
      <SpecialNode v-bind="specialNodeProps" />
    </template>
  </VueFlow>
</template>
<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>

<template>
  <VueFlow :nodes="nodes">
    <template #node-custom="customNodeProps">
      <CustomNode v-bind="customNodeProps" />
    </template>
    
    <template #node-special="specialNodeProps">
      <SpecialNode v-bind="specialNodeProps" />
    </template>
  </VueFlow>
</template>

在配置了自定义组件后,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>

<template>
  <VueFlow :nodes="nodes" :nodeTypes="nodeTypes" />
</template>

节点事件

参考: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>

<template>
  <VueFlow :nodes="nodes" :edges="edges" />
</template>

增删和更新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')

Vue3 流程图组件库 :Vue FlowVue Flow 是一个轻量级的 Vue 3 组件库,它允许开发者以简洁直观的

阶梯连线(type:'step')

Vue3 流程图组件库 :Vue FlowVue Flow 是一个轻量级的 Vue 3 组件库,它允许开发者以简洁直观的

直线连接(type:'straight')

Vue3 流程图组件库 :Vue FlowVue Flow 是一个轻量级的 Vue 3 组件库,它允许开发者以简洁直观的

自定义连接

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

edge事件

参考:Edges | Vue Flow

Handles

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

使用Handle

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

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

<template>
  <Handle type="source" :position="sourcePosition" />
  
  <span>{{ data.label }}</span>
  
  <Handle type="target" :position="targetPosition" />
</template>

Handle 位置

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

<Handle type="source" :position="Position.Right" /> 
<Handle type="target" :position="Position.Left" />

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

<Handle id="source-a" type="source" :position="Position.Right" /> <Handle id="source-b" type="source" :position="Position.Right" />

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

<Handle id="source-a" type="source" :position="Position.Right" style="top: 10px" /> 
<Handle id="source-b" type="source" :position="Position.Right" style="bottom: 10px; top: auto;" />

Handle的隐藏

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

<Handle type="source" :position="Position.Right" style="opacity: 0" />

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

<Handle type="source" :position="Position.Right" :connectable="handleConnectable" />

连接模式

<VueFlow :connection-mode="ConnectionMode.Strict" />

配置了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中的一些基本用法:

Vue3 流程图组件库 :Vue FlowVue Flow 是一个轻量级的 Vue 3 组件库,它允许开发者以简洁直观的

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);
  });
}
<template>
  <div class="layout-flow">
    <VueFlow :nodes="nodes" :edges="edges" @nodes-initialized="layoutGraph('LR')">
    <!--    以插槽方式传入节点和连线    -->
      <template #node-process="props">
        <ProcessNode 
        :data="props.data" 
        :source-position="props.sourcePosition" 
        :target-position="props.targetPosition" />
      </template>

      <template #edge-animation="edgeProps">
        <AnimationEdge
          :id="edgeProps.id"
          :source="edgeProps.source"
          :target="edgeProps.target"
          :source-x="edgeProps.sourceX"
          :source-y="edgeProps.sourceY"
          :targetX="edgeProps.targetX"
          :targetY="edgeProps.targetY"
          :source-position="edgeProps.sourcePosition"
          :target-position="edgeProps.targetPosition"
        />
      </template>

      <Background />

      <Panel class="process-panel" position="top-right">
        <div class="layout-panel">
          <button v-if="isRunning" class="stop-btn" title="stop" @click="stop">
            <Icon name="stop" />
            <span class="spinner" />
          </button>
          <button v-else title="start" @click="run(nodes)">
            <Icon name="play" />
          </button>

          <button title="set horizontal layout" @click="layoutGraph('LR')">
            <Icon name="horizontal" />
          </button>

          <button title="set vertical layout" @click="layoutGraph('TB')">
            <Icon name="vertical" />
          </button>

          <button title="shuffle graph" @click="shuffleGraph">
            <Icon name="shuffle" />
          </button>
        </div>

        <div class="checkbox-panel">
          <label>Cancel on error</label>
          <input v-model="cancelOnError" type="checkbox" />
        </div>
      </Panel>
    </VueFlow>
  </div>
</template>
.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) {
            updateNodeData(node.id, { isRunning: false, hasError: true });

            if (toValue(cancelOnError)) {
              // 跳过错误节点后续子节点的处理
              await skipDescendants(node.id);
              // 删除节点对应正在执行的任务
              runningTasks.delete(node.id);

              // @ts-expect-error
              resolve();
              return;
            }
          }

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

          runningTasks.delete(node.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>

<template>
  <div class="process-node" :style="{ backgroundColor: bgColor, boxShadow: data.isRunning ? '0 0 10px rgba(0, 0, 0, 0.5)' : '' }">
    <!-- 使用Handle组件处理连接点的样式 -->
    <Handle v-if="!isSender" type="target" :position="targetPosition">
      <span v-if="!data.isRunning && !data.isFinished && !data.isCancelled && !data.isSkipped && !data.hasError">📥 </span>
    </Handle>

    <Handle v-if="!isReceiver" type="source" :position="sourcePosition" />

    <div v-if="!isSender && data.isRunning" class="spinner" />
    <span v-else>
      {{ processLabel }}
    </span>
  </div>
</template>

<style scoped>
.process-node {
  padding: 10px;
  border-radius: 99px;
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.process-node .vue-flow__handle {
  border: none;
  height: unset;
  width: unset;
  background: transparent;
  font-size: 12px;
}

6.AnimationEdge.js

处理节点之间连线的动画效果

<script lang="ts" setup>
import { computed, nextTick, ref, toRef, watch } from "vue";
import { TransitionPresets, executeTransition } from "@vueuse/core";
import {
  Position,
  BaseEdge,
  useVueFlow,
  useNodesData,
  getSmoothStepPath,
  EdgeLabelRenderer
} from "@vue-flow/core";

const props = defineProps({
  id: {
    type: String,
    required: true
  },
  source: {
    type: String,
    required: true
  },
  target: {
    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,
    default: Position.Right
  },
  targetPosition: {
    type: String,
    default: Position.Left
  }
});

const { findEdge } = useVueFlow();

// 获取被当前edge连接的两个节点
const nodesData = useNodesData([props.target, props.source]);

const targetNodeData = toRef(() => nodesData.value[0].data);

const sourceNodeData = toRef(() => nodesData.value[1].data);

// edge动画开始位置等相关信息
const edgePoint = ref(0);

const edgeRef = ref();

const labelPosition = ref({ x: 0, y: 0 });

const currentLength = ref(0);

// edge当前的状态

const isFinished = toRef(() => sourceNodeData.value.isFinished);

const isCancelled = toRef(() => targetNodeData.value.isCancelled);

// 显示动画flag
const isAnimating = ref(false);

// edge颜色
const edgeColor = toRef(() => {
  if (targetNodeData.value.hasError) {
    return "#f87171";
  }

  if (targetNodeData.value.isFinished) {
    return "#42B983";
  }

  if (targetNodeData.value.isCancelled || targetNodeData.value.isSkipped) {
    return "#fbbf24";
  }

  if (targetNodeData.value.isRunning || isAnimating.value) {
    return "#2563eb";
  }

  return "#6b7280";
});

// 得到edge的路径
const path = computed(() => getSmoothStepPath(props));


// 重置动画
watch(isCancelled, isCancelled => {
  if (isCancelled) {
    reset();
  }
});

// 更新edge数据
watch(isAnimating, isAnimating => {
  const edge = findEdge(props.id);

  if (edge) {
    edge.data = {
      ...edge.data,
      isAnimating
    };
  }
});

// 监听edgePoint变化
watch(edgePoint, point => {
  const pathEl = edgeRef.value?.pathEl;

  if (!pathEl || point === 0 || !isAnimating.value) {
    return;
  }

  const nextLength = pathEl.getTotalLength();

  // 当currentLength路径没有被更新时启动动画
  if (currentLength.value !== nextLength) {
    runAnimation();
    return;
  }

  // 更新label的位置
  labelPosition.value = pathEl.getPointAtLength(point);
});

watch(isFinished, isFinished => {
  if (isFinished) {
    runAnimation();
  }
});

// 开启动画
async function runAnimation() {
  
  // 获取edge路径
  const pathEl = edgeRef.value?.pathEl;

  if (!pathEl) {
    return;
  }

  const totalLength = pathEl.getTotalLength();

  const from = edgePoint.value || 0;

  // 更新label的位置
  labelPosition.value = pathEl.getPointAtLength(from);

  // 更新动画flag
  isAnimating.value = true;

  // 更新当前路径总长度
  if (currentLength.value !== totalLength) {
    currentLength.value = totalLength;
  }

  // 使用vueUse的executeTransition处理缓动动画
  await executeTransition(edgePoint, from, totalLength, {
      transition: TransitionPresets.easeInOutCubic,
        duration: Math.max(1500, totalLength / 2),
          abort: () => !isAnimating.value
    });

  reset();
}

//重置动画
function reset() {
  nextTick(() => {
    edgePoint.value = 0;
    currentLength.value = 0;
    labelPosition.value = { x: 0, y: 0 };
    isAnimating.value = false;
  });
}
</script>

<template>
  <BaseEdge
    :id="id"
    ref="edgeRef"
    :path="path[0]"
    :style="{ stroke: edgeColor }"
  />

  <EdgeLabelRenderer v-if="isAnimating">
    <div
      :style="{
        transform: `translate(-50%, -50%) translate(${labelPosition.x}px,${labelPosition.y}px)`
      }"
      class="nodrag nopan animated-edge-label"
    >
      <span class="truck">
        <span class="box">📦</span>
        🚚
      </span>
    </div>
  </EdgeLabelRenderer>
</template>

<style scoped>
.animated-edge-label {
  position: absolute;
  z-index: 100;
}

.truck {
  position: relative;
  display: inline-block;
  transform: scaleX(-1);
}

.box {
  position: absolute;
  top: -10px;
}
</style>


总结

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

1. 安装

2. 基础组件

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

3. 主题定制

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

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

4. 节点(Nodes)

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

5. 连线(Edges)

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

6. Handles

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

转载自:https://juejin.cn/post/7407683752712388608
评论
请登录