vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue3 vueFLow流程组件

Vue3实现vueFLow流程组件的详细指南

作者:嘿!!

VueFlow是一个专门为Vue.js框架设计的交互式可视化库,它允许开发者轻松创建和管理复杂的图形模型,如流程图、状态机、组织结构图等,本文给大家介绍了Vue3实现vueFLow流程组件的详细指南,需要的朋友可以参考下

一、前言

使用vueFlow封装了一个层级关系组件。

二、官网

Vue Flow

三、安装

方式一:papackage.json添加依赖后直接npm install

  1. @vue-flow/background@^1.3.0
    • 组件名称:背景栅格组件
    • 功能:为Vue Flow提供背景支持,通常用于显示栅格线或背景图案,以帮助用户更好地对齐和布局流程图中的元素。
    • 版本:^1.3.0 表示该组件的版本号至少为1.3.0,但会兼容该版本之后的任何更新(遵循语义化版本控制规则)。
  2. @vue-flow/controls@^1.1.2
    • 组件名称:控件组件
    • 功能:提供用于缩放、平移和旋转流程图的控制元素。这些控件允许用户以交互方式调整流程图的视角和布局。
    • 版本:^1.1.2 表示该组件的版本号至少为1.1.2,同样遵循语义化版本控制规则。
  3. @vue-flow/core@^1.41.2
    • 组件名称:核心组件
    • 功能:Vue Flow的核心功能组件,提供了创建和管理流程图所需的基础设施。这包括节点、边(连接线)、事件处理、状态管理等核心功能。
    • 版本:^1.41.2 表示该组件的版本号至少为1.41.2,并兼容后续更新。
  4. @vue-flow/minimap@^1.5.0
    • 组件名称:缩略图组件
    • 功能:提供一个缩略图视图,用于显示整个流程图的概览。用户可以通过缩略图快速定位到流程图中的特定区域。
    • 版本:^1.5.0 表示该组件的版本号至少为1.5.0,遵循语义化版本控制规则。
  5. @vue-flow/node-resizer@^1.4.0
    • 组件名称:节点调整大小组件
    • 功能:允许用户通过拖动边缘来调整节点的大小。这增加了流程图创建的灵活性和用户友好性。
    • 版本:^1.4.0 表示该组件的版本号至少为1.4.0,同样遵循语义化版本控制规则。
  6. @vue-flow/node-toolbar@^1.1.0
    • 组件名称:节点工具栏组件
    • 功能:为节点提供附加的工具栏,通常包含用于编辑、删除或配置节点选项的按钮。这增强了流程图编辑的交互性和便捷性。
    • 版本:^1.1.0 表示该组件的版本号至少为1.1.0,遵循语义化版本控制规则。

方式二:npm install @vue-flow

四、引用

1.App.vue文件

<script setup>
import { ref, toRef } from 'vue'
import { MiniMap } from '@vue-flow/minimap'
import { Position, VueFlow } from '@vue-flow/core'
import ColorSelectorNode from './ColorSelectorNode.vue'
import OutputNode from './OutputNode.vue'
import { presets } from './presets.js'
 
const nodes = ref([
  {
    id: '1',
    type: 'color-selector',
    data: { color: presets.ayame },
    position: { x: 0, y: 50 },
  },
  {
    id: '2',
    type: 'output',
    position: { x: 350, y: 114 },
    targetPosition: Position.Left,
  },
])
 
const edges = ref([
  {
    id: 'e1a-2',
    source: '1',
    sourceHandle: 'a',
    target: '2',
    animated: true,
    style: {
      stroke: presets.ayame,
    },
  },
])
 
const colorSelectorData = toRef(() => nodes.value[0].data)
 
// minimap stroke color functions
function nodeStroke(n) {
  switch (n.type) {
    case 'input':
      return '#0041d0'
    case 'color-selector':
      return n.data.color
    case 'output':
      return '#ff0072'
    default:
      return '#eee'
  }
}
 
function nodeColor(n) {
  if (n.type === 'color-selector') {
    return n.data.color
  }
 
  return '#fff'
}
</script>
 
<template>
  <VueFlow
    v-model:nodes="nodes"
    :edges="edges"
    class="custom-node-flow"
    :class="[colorSelectorData?.isGradient ? 'animated-bg-gradient' : '']"
    :style="{ backgroundColor: colorSelectorData?.color }"
    fit-view-on-init
  >
    <template #node-color-selector="props">
      <ColorSelectorNode :id="props.id" :data="props.data" />
    </template>
 
    <template #node-output>
      <OutputNode />
    </template>
 
    <MiniMap :node-stroke-color="nodeStroke" :node-color="nodeColor" />
  </VueFlow>
</template>

2.ColorSelectorNode.vue

<script setup>
import { Handle, Position, useVueFlow } from '@vue-flow/core'
import { colors } from './presets.js'
 
const props = defineProps({
  id: {
    type: String,
    required: true,
  },
  data: {
    type: Object,
    required: true,
  },
})
 
const { updateNodeData, getConnectedEdges } = useVueFlow()
 
function onSelect(color) {
  updateNodeData(props.id, { color, isGradient: false })
 
  const connectedEdges = getConnectedEdges(props.id)
  for (const edge of connectedEdges) {
    edge.style = {
      stroke: color,
    }
  }
}
 
function onGradient() {
  updateNodeData(props.id, { isGradient: true })
}
</script>
 
<template>
  <div>Select a color</div>
 
  <div class="color-selector nodrag nopan">
    <button
      v-for="{ name: colorName, value: color } of colors"
      :key="colorName"
      :title="colorName"
      :class="{ selected: color === data.color }"
      :style="{ backgroundColor: color }"
      type="button"
      @click="onSelect(color)"
    />
 
    <button class="animated-bg-gradient" title="gradient" type="button" @click="onGradient" />
  </div>
 
  <Handle id="a" type="source" :position="Position.Right" />
</template>

3.OutputNode.vue

<script setup>
import { Handle, Position, useHandleConnections, useNodesData } from '@vue-flow/core'
 
const connections = useHandleConnections({
  type: 'target',
})
 
const nodesData = useNodesData(() => connections.value[0]?.source)
</script>
 
<template>
  <Handle
    type="target"
    :position="Position.Left"
    :style="{ height: '16px', width: '6px', backgroundColor: nodesData.data?.color, filter: 'invert(100%)' }"
  />
  {{ nodesData.data?.isGradient ? 'GRADIENT' : nodesData.data?.color }}
</template>

4.presets.js

export const presets = {
  sumi: '#1C1C1C',
  gofun: '#FFFFFB',
  byakuroku: '#A8D8B9',
  mizu: '#81C7D4',
  asagi: '#33A6B8',
  ukon: '#EFBB24',
  mushikuri: '#D9CD90',
  hiwa: '#BEC23F',
  ichigo: '#B5495B',
  kurenai: '#CB1B45',
  syojyohi: '#E83015',
  konjyo: '#113285',
  fuji: '#8B81C3',
  ayame: '#6F3381',
  torinoko: '#DAC9A6',
  kurotsurubami: '#0B1013',
  ohni: '#F05E1C',
  kokikuchinashi: '#FB9966',
  beniukon: '#E98B2A',
  sakura: '#FEDFE1',
  toki: '#EEA9A9',
}
 
export const colors = Object.keys(presets).map((color) => {
  return {
    name: color,
    value: presets[color],
  }
})

5.main.css

@import 'https://cdn.jsdelivr.net/npm/@vue-flow/core@1.41.2/dist/style.css';
@import 'https://cdn.jsdelivr.net/npm/@vue-flow/core@1.41.2/dist/theme-default.css';
@import 'https://cdn.jsdelivr.net/npm/@vue-flow/controls@latest/dist/style.css';
@import 'https://cdn.jsdelivr.net/npm/@vue-flow/minimap@latest/dist/style.css';
@import 'https://cdn.jsdelivr.net/npm/@vue-flow/node-resizer@latest/dist/style.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;
}
 
.vue-flow__minimap {
  transform: scale(75%);
  transform-origin: bottom right;
}
 
.vue-flow__edges {
    filter:invert(100%)
}
 
.vue-flow__handle {
    height:24px;
    width:8px;
    border-radius:4px
}
 
.vue-flow__node-color-selector {
    border:1px solid #777;
    padding:10px;
    border-radius:10px;
    background:#f5f5f5;
    display:flex;
    flex-direction:column;
    justify-content:space-between;
    align-items:center;
    gap:10px;
    max-width:250px
}
 
.vue-flow__node-color-selector .color-selector {
    display:flex;
    flex-direction:row;
    flex-wrap:wrap;
    justify-content:center;
    max-width:90%;
    margin:auto;
    gap:4px
}
 
.vue-flow__node-color-selector .color-selector button {
    border:none;
    cursor:pointer;
    padding:5px;
    width:25px;
    height:25px;
    border-radius:8px;
    box-shadow:0 0 10px #0000004d
}
 
.vue-flow__node-color-selector .color-selector button:hover {
    box-shadow:0 0 0 2px #2563eb;
    transition:box-shadow .2s
}
 
.vue-flow__node-color-selector .color-selector button.selected {
    box-shadow:0 0 0 2px #2563eb
}
 
.vue-flow__node-color-selector .vue-flow__handle {
    background-color:#ec4899;
    height:24px;
    width:8px;
    border-radius:4px
}
 
.animated-bg-gradient {
    background:linear-gradient(122deg,#6f3381,#81c7d4,#fedfe1,#fffffb);
    background-size:800% 800%;
    -webkit-animation:gradient 4s ease infinite;
    -moz-animation:gradient 4s ease infinite;
    animation:gradient 4s ease infinite
}
 
@-webkit-keyframes gradient {
    0% {
    background-position:0% 22%
}
 
50% {
    background-position:100% 79%
}
 
to {
    background-position:0% 22%
}
 
 
}
 
@-moz-keyframes gradient {
    0% {
    background-position:0% 22%
}
 
50% {
    background-position:100% 79%
}
 
to {
    background-position:0% 22%
}
 
 
}
 
@keyframes gradient {
    0% {
    background-position:0% 22%
}
 
50% {
    background-position:100% 79%
}
 
to {
    background-position:0% 22%
}
 
 
}

五、预览效果

六、个人实现

七、问题记录

vueflow每个层级节点的位置position无法自动生成,所以需要自己进行封装。我是根据层级来进行计算从顶部依次向下布局。

<script setup lang="ts">
import {ref} from "vue";
import {tableDetail} from "./datatable.api";
import Blood from "./compoent/blood.vue"
import {MarkerType} from "@vue-flow/core";
import { gettestListIndexByTableName } from "@/views/test/common/test.api";
import {useUserStore} from "@/store/modules/user";
 
 
const userStore = useUserStore();
 
// 详情抽屉
const drawerDetail = ref({})
const drawerOpenFlag = ref(false)
const drawerDetailFields = ref([])
 
const nodes = ref([]);
const edges = ref([]);
 
/**
 * 查看详情
 * @param record
 */
const onHandleOpenDrawer = async (record) => {
  const detail = await tableDetail(record)
  let fields = []
  for (let fieldName of Object.keys(detail.fields) || []) {
    fields.push(detail.fields[fieldName])
  }
  drawerDetail.value = detail
  drawerDetailFields.value = fields
  drawerOpenFlag.value = true
 
  nodes.value = []
  edges.value = []
  // 添加节点
  addNode({
    id: 'testyuan',
    type: 'data-source',
    data: { database: record.database, testyuanTable: record.tableName, fieldArr: fields },
    position: { x: 0, y: 90 },
  });
  addNode({
    id: '0',
    type: 'data-set',
    data: { database: record.database, testyuanTable: record.tableName, fieldArr: fields },
    position: { x: 350, y: 70 },
  });
 
  // 添加从到集的边
  addEdge({ id: 'first', source: 'testyuan', target: '0', markerEnd: MarkerType.ArrowClosed });
 
  // 获取指标列表并添加节点和边
  const tests = await gettestListIndexByTableName({ tableName: record.tableName });
  if (tests.length > 0) {
    for (let i = 0; i < tests.length; i++) {
      const test = tests[i];
      addNode({
        id: test.id.toString(),
        type: 'index-info',
        position: { x: 0, y:  0 },
        data: {
          indexNameEn: test.indexNameEn,
          indexNameCn: test.indexNameCn,
          group: getIndexClass(test.secondlevel),
          type: gettestType(test.indexType)
        },
      });
      addEdge({
        id: test.id.toString(),
        source: test.parentId,
        target: test.id.toString(),
        markerEnd: MarkerType.ArrowClosed,
      });
    }
    // 记录每个 level 出现的次数
    let levelCounts = {};
    for (let j=2; j<nodes.value.length; j++) {
      const node = nodes.value[j];
      const level = Number(getNodeLevel(edges.value,node.id.toString()));
      node.position.x =  350 * level;
      // 更新 level 出现的次数
      if (levelCounts[level]) {
        levelCounts[level]++;
      } else {
        levelCounts[level] = 1;
      }
      node.position.y =  100 * levelCounts[level];
    }
 
  } else {
    console.warn('No tests found for table:', record.tableName);
  }
}
 
// 封装获取当前节点层级的函数
const getNodeLevel = (edges, nodeId) => {
  const levelMap = {};
  const visited = new Set();
  const dfs = (node, level) => {
    visited.add(node);
    levelMap[node] = level;
    // 遍历边列表,找到所有从当前节点出发的边
    edges.forEach((edge) => {
      if (edge.source === node && !visited.has(edge.target)) {
        // 递归地更新目标节点的下一层级
        dfs(edge.target, level + 1);
      }
    });
  };
  // 起始节点ID为"0"
  dfs('0', 1);
  return levelMap[nodeId] || 'Node not found in the graph';
};
 
// 封装添加节点的函数
const addNode = (node) => {
  nodes.value.push(node);
};
 
// 封装添加边的函数
const addEdge = (edge) => {
  edges.value.push(edge);
};
 
const firstArr = ref([]);
const tree = userStore.gettestDictAllTree.find(t => t.dictCode === "first");
firstArr.value = tree ? tree.dictItemList : [];
// 获取指标分级并转成对象
const firstMap = firstArr.value.reduce((acc, item) => {
  acc[item.itemValue] = item.itemText;
  return acc;
}, {});
 
const getIndexClass = (indexClass) => {
  return firstMap[indexClass] || '';
};
 
// 获取指标分类并转成对象
const testTypeMap = userStore.testDictAllTreeObj['testType'].reduce((acc, item) => {
  acc[item.itemValue] = item.itemText;
  return acc;
}, {});
 
// 根据indexType获取指标类型
const gettestType = (indexType) => {
  return testTypeMap[indexType] || '';
};
 
</script>
 
<template>
  <a-drawer
    v-model:open="drawerOpenFlag"
    title="详情"
    width="700"
    placement="right"
  >
    <div style="font-size: 16px; font-weight: 500; color: rgba(0, 0, 0, 0.88); margin-left: 23px">
      基础信息
    </div>
    <Blood :nodes="nodes" :edges="edges"></Blood>
 
  </a-drawer>
</template>
 
<style scoped lang="less"></style>

以上就是Vue3实现vueFLow流程组件的详细指南的详细内容,更多关于Vue3 vueFLow流程组件的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文