vue3+vue-flow制作简单可拖拽可增删改流程图

发布于:2025-08-05 ⋅ 阅读:(18) ⋅ 点赞:(0)

实现效果

在这里插入图片描述

实现代码

准备工作

安装依赖
npm install @vue-flow/core
npm install @vue-flow/minimap //小地图
npm install @vue-flow/controls //自带的缩放、居中、加锁功能

我这里只用到上述三个,还有其余的可根据实际情况配合官方文档使用。

npm install @vue-flow/background //背景
npm install @vue-flow/node-toolbar //工具栏
npm install @vue-flow/node-resizer //缩放
创建<初始元素>js文件 initial-elements.js
import { MarkerType } from '@vue-flow/core'

export const initialNodes = [
  {
    id: '1',
    position: { x: 100, y: 250 },
    type: 'custom',
    data: {
      value: '需求响应',
      icon1: false,
      icon2: false,
      icon3: false,
      icon4: false,
      icon5: false,
    },

  },
  {
    id: '2',
    position: { x: 350, y: 250 },
    type: 'custom',
    data: {
      value: '方案制定',
      icon1: false,
      icon2: false,
      icon3: false,
      icon4: false,
      icon5: false,
    },

  },
  {
    id: '3',
    position: { x: 600, y: 250 },
    type: 'custom',
    data: {
      value: '实施',
      icon1: false,
      icon2: false,
      icon3: false,
      icon4: false,
      icon5: false,
    },

  },
  {
    id: '4',
    position: { x: 850, y: 250 },
    type: 'custom',
    data: {
      value: '效果验证',
      icon1: false,
      icon2: false,
      icon3: false,
      icon4: false,
      icon5: false,
    },

  },
]

export const initialEdges = [
  { id: 'e1-2', source: '1', target: '2', markerEnd: MarkerType.ArrowClosed, updatable: true, EdgeMarkerType: { strokeWidth: 10 }, style: { stroke: '#999', strokeWidth: 2, strokeLinecap: 'round' } },
  { id: 'e2-3', source: '2', target: '3', markerEnd: MarkerType.ArrowClosed, updatable: true, EdgeMarkerType: { strokeWidth: 10 }, style: { stroke: '#999', strokeWidth: 2, strokeLinecap: 'round' } },
  { id: 'e3-4', source: '3', target: '4', markerEnd: MarkerType.ArrowClosed, updatable: true, EdgeMarkerType: { strokeWidth: 10 }, style: { stroke: '#999', strokeWidth: 2, strokeLinecap: 'round' } },
]

创建<使用拖拽>js文件 useDnD.js
import { useVueFlow } from '@vue-flow/core'
import { ref, watch } from 'vue'


/**
 * In a real world scenario you'd want to avoid creating refs in a global scope like this as they might not be cleaned up properly.
 * @type {{draggedType: Ref<string|null>, isDragOver: Ref<boolean>, isDragging: Ref<boolean>}}
 */
const state = {
  /**
   * The type of the node being dragged.
   */
  draggedType: ref(null),
  isDragOver: ref(false),
  isDragging: ref(false),
}


export default function useDragAndDrop() {
  const { draggedType, isDragOver, isDragging } = state

  const { addNodes, screenToFlowCoordinate, onNodesInitialized, updateNode } = useVueFlow()

  watch(isDragging, (dragging) => {
    document.body.style.userSelect = dragging ? 'none' : ''
  })

  function onDragStart(event, type) {
    console.log("onDragStart", type);



    if (event.dataTransfer) {
      event.dataTransfer.setData('application/vueflow', type)
      event.dataTransfer.effectAllowed = 'move'
    }

    draggedType.value = type
    isDragging.value = true

    document.addEventListener('drop', onDragEnd)
  }

  /**
   * Handles the drag over event.
   *
   * @param {DragEvent} event
   */
  function onDragOver(event) {
    event.preventDefault()

    if (draggedType.value) {
      isDragOver.value = true

      if (event.dataTransfer) {
        event.dataTransfer.dropEffect = 'move'
      }
    }
  }

  function onDragLeave() {
    isDragOver.value = false
  }

  function onDragEnd() {
    console.log("onDragEnd");

    isDragging.value = false
    isDragOver.value = false
    draggedType.value = null
    document.removeEventListener('drop', onDragEnd)
  }

  /**
   * Handles the drop event.
   *
   * @param {DragEvent} event
   */
  function onDrop(event, node) {

    const position = screenToFlowCoordinate({
      x: event.clientX,
      y: event.clientY,
    })

    node.position = position
    // /**
    //  * Align node position after drop, so it's centered to the mouse
    //  *
    //  * We can hook into events even in a callback, and we can remove the event listener after it's been called.
    //  */
    const { off } = onNodesInitialized(() => {
      updateNode(node.id, (node) => ({
        position: { x: node.position.x - node.dimensions.width / 2, y: node.position.y - node.dimensions.height / 2 },
      }))

      off()
    })

    addNodes(node)
  }

  return {
    draggedType,
    isDragOver,
    isDragging,
    onDragStart,
    onDragLeave,
    onDragOver,
    onDrop,
  }
}

创建<单个流程图节点>vue文件 ValueNode.vue
<template>
  <div class="nodeItem relative">
    <!-- @contextmenu="handleRightClick($event, props.id)" -->
    <!-- 开始节点的位置 -->
    <Handle type="source" position="right" />
    <el-input
      :id="`${id}-input`"
      v-model="value"
      placeholder="点击添加文字"
      style="width: 170px; font-size: 14px"
      type="textarea"
      autosize
      maxlength="20"
      resize="none"
    />
    <!-- 结束节点的位置 -->
    <Handle type="target" position="left" />
    <el-icon
      :size="20"
      class="absolute red pointer"
      style="right: -10px; top: -10px"
      @click="handleDel($event, id)"
      ><CircleCloseFilled
    /></el-icon>
  </div>
</template>

<script setup>
import { computed } from "vue";
import { Handle, Position, useVueFlow } from "@vue-flow/core";
const { proxy } = getCurrentInstance();

// 定义传递给父组件的事件
const emit = defineEmits(["updateNodes"]);
const props = defineProps([
  "id",
  "data",
  "length",
]);

const { updateNodeData, removeNodes } = useVueFlow();

const value = computed({
  get: () => props.data.value,
  set: (value) => {
    updateNodeData(props.id, { value });
    emit("updateNodes");
  },
});

function handleDel(event, id) {
  event.preventDefault();
  if (props.length == 1) {
    proxy.$modal.msgWarning("至少保留一个节点");
  } else {
    removeNodes([id]);
    emit("updateNodes");
  }
}

function handleRightClick(event, id) {
  console.log("右键被点击了");
  event.preventDefault(); // 阻止默认的右键菜单显示
  // 在这里可以添加更多逻辑,比如显示自定义的右键菜单等
  console.log("右键被点击");
  removeNodes([id]);
}
</script>
<style scoped lang="scss">
.nodeItem {
  padding: 6px 20px;
  background: rgba(219, 227, 247, 1);
  border-radius: 8px;
}
</style>

具体实现


<template>
  <div class="w100 h100 flex1 size-15" @drop="onDrop($event, getNewNode())">
    <!-- <el-button @click="addNode">add</el-button> -->
    <div class="bg-white h100 pd-16" style="width: 340px">
      <div class="mb-12 bold">流程图组件</div>
      <el-button
        style="width: 100%; cursor: grab"
        plain
        :draggable="true"
        @dragstart="onDragStart($event, 'custom')"
        >拖转至画布</el-button
      >
    </div>
    <div class="flex-1 h100">
      <VueFlow
        :key="key"
        ref="vueFlowRef"
        :nodes="nodes"
        :edges="edges"
        auto-connect
        :default-viewport="{ zoom: 1.0 }"
        :min-zoom="0.2"
        :max-zoom="4"
        @edge-update="onEdgeUpdate"
        @connect="onConnect"
        @edge-update-start="onEdgeUpdateStart"
        @edge-update-end="onEdgeUpdateEnd"
        @dragover="onDragOver"
        @dragleave="onDragLeave"
      >
        <template #node-custom="props">
          <ValueNode
            :id="props.id"
            :data="props.data"
            @updateNodes="updateNodes"
            :length="nodes.length"
          />
        </template>
        <MiniMap />
      </VueFlow>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, nextTick, watch } from "vue";
import { VueFlow, useVueFlow, MarkerType } from "@vue-flow/core";
import { initialEdges, initialNodes } from "./initial-elements.js";
import { MiniMap } from "@vue-flow/minimap";
import ValueNode from "./ValueNode.vue";
import "@vue-flow/core/dist/style.css";
import "@vue-flow/core/dist/theme-default.css";

const { proxy } = getCurrentInstance();

import useDragAndDrop from "./useDnD.js";
import { Sunny } from "@element-plus/icons-vue";

const { onDragStart, onDrop, onDragOver, onDragLeave } = useDragAndDrop();
/**
 * `useVueFlow` provides:
 * 1. a set of methods to interact with the VueFlow instance (like `fitView`, `setViewport`, `addEdges`, etc)
 * 2. a set of event-hooks to listen to VueFlow events (like `onInit`, `onNodeDragStop`, `onConnect`, etc)
 * 3. the internal state of the VueFlow instance (like `nodes`, `edges`, `viewport`, etc)
 */
const {
  onInit,
  onNodeDragStop,
  onConnect,
  addEdges,
  updateEdge,
  getNodes,
  getEdges,
} = useVueFlow();

const props = defineProps({
  nodes: {
    type: Object,
    default: initialNodes,
  },
  edges: {
    type: Object,
    default: initialEdges,
  },
  iconShow: {
    type: Object,
    default: () => {},
  },
});
const nodes = ref(null);
const edges = ref(null);

const abc = "需求响应";
nodes.value = props.nodes || initialNodes;
edges.value = props.edges || initialEdges;
proxy.$emit("updateList", nodes.value, edges.value);

const vueFlowRef = ref(null);
const nodeOptions = ref([]);
nodeOptions.value = handleNodesOption();

// 创建一个新的节点对象
function getNewNode() {
  return {
    id: new Date().getTime().toString(),
    type: "custom",
    data: {
      value: "",
    },
    position: { x: 50, y: 50 },
  };
}

// 更新节点列表
function updateNodes() {
  nodes.value = getNodes.value;
  nodeOptions.value = handleNodesOption();
  proxy.$emit("updateList", getNodes.value, getEdges.value);
}

// 处理节点下拉数据
function handleNodesOption() {
  return nodes.value
    .filter((item) => (item.data.value ?? "") !== "")
    .map((r) => ({
      label: r.data.value,
      value: r.data.value,
    }));
}

/**
 * onNodeDragStop is called when a node is done being dragged
 *
 * Node drag events provide you with:
 * 1. the event object
 * 2. the nodes array (if multiple nodes are dragged)
 * 3. the node that initiated the drag
 * 4. any intersections with other nodes
 */
onNodeDragStop(({ event, nodes, node }) => {
  console.log("Node Drag Stop", { event, nodes, node });
});

function onEdgeUpdateStart(edge) {
  console.log("start update", edge);
}

function onEdgeUpdateEnd(edge) {
  console.log("end update", edge);
}

function onEdgeUpdate({ edge, connection }) {
  console.log("onEdgeUpdate", edge, connection);
  updateEdge(edge, connection);
  console.log("onEdgeUpdate", getEdges.value);
}

/**
 * onConnect is called when a new connection is created.
 *
 * You can add additional properties to your new edge (like a type or label) or block the creation altogether by not calling `addEdges`
 */
onConnect((connection) => {
  console.log("onConnect", connection, [connection]);
  const newEdges = {
    ...connection,
    markerEnd: MarkerType.ArrowClosed,
    updatable: true,
    style: { stroke: "#999", strokeWidth: 2, strokeLinecap: "round" },
  };
  addEdges([newEdges]);
  console.log("onConnect", getEdges.value);
});

watchEffect(() => {
  nodes.value = props.nodes || initialNodes;
  edges.value = props.edges || initialEdges;
  proxy.$emit("updateList", nodes.value, edges.value);
});

function multipleChange(keyArr, type) {
  console.log(keyArr, type);

  nodes.value.forEach((node) => {
    let item = keyArr.find((r) => r === node.data.value);
    node.data[type] = item ? true : false;
  });
  console.log(nodes.value);
}
const key = ref(0);
// 重新生成
const init = (nodeArr, edgeArr) => {
  edges.value = edgeArr || initialEdges;
  nodes.value = nodeArr || initialNodes;
  key.value++;
};

// 下一步前的校验
const checkNodesEdges = () => {
  console.log("checkNodesEdges", getEdges.value);

  let hasNoTarget = getEdges.value.length < getNodes.value.length - 1;
  if (hasNoTarget) {
    proxy.$modal.msgWarning("画布中存在节点未连线");
    return false;
  } else {
    proxy.$emit("updateList", getNodes.value, getEdges.value);
    return true;
  }
};

// 使用defineExpose暴露方法给父组件
defineExpose({
  checkNodesEdges,
  init,
});
</script>

<style scoped>
:deep(.vue-flow__handle) {
  width: 12px !important;
  height: 12px !important;
  border: 1px solid #666 !important;
  background: #fff !important;
}
</style>


网站公告

今日签到

点亮在社区的每一天
去签到