Browse Source

feature/流程图实现

dengrui 9 months ago
parent
commit
807dc321ee

+ 3 - 0
package.json

@@ -43,6 +43,8 @@
     "@element-plus/icons-vue": "^2.3.1",
     "@smallwei/avue": "^3.3.3",
     "@types/smallwei__avue": "^3.0.5",
+    "@vue-flow/background": "^1.3.0",
+    "@vue-flow/core": "^1.37.1",
     "@vueup/vue-quill": "1.0.0-alpha.40",
     "@vueuse/core": "^10.9.0",
     "@wangeditor/editor": "^5.1.23",
@@ -53,6 +55,7 @@
     "element-plus": "^2.6.0",
     "exceljs": "^4.4.0",
     "file-saver": "^2.0.5",
+    "html-to-image": "^1.11.11",
     "html2canvas": "^1.4.1",
     "lodash-es": "^4.17.21",
     "luckyexcel": "^1.0.1",

+ 128 - 0
src/hooks/useDnD.js

@@ -0,0 +1,128 @@
+import { useVueFlow } from "@vue-flow/core";
+import { ref, watch } from "vue";
+import { v4 as uuidv4 } from "uuid";
+
+/**
+ * @returns {string} - A unique id.
+ */
+function getId() {
+  return uuidv4();
+}
+/**
+ * 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),
+  draggedLable: ref(null),
+  draggedItem: ref(null),
+  isDragOver: ref(false),
+  isDragging: ref(false),
+};
+
+export default function useDragAndDrop() {
+  const { draggedType, draggedLable, draggedItem, isDragOver, isDragging } =
+    state;
+
+  const { addNodes, screenToFlowCoordinate, onNodesInitialized, updateNode } =
+    useVueFlow();
+
+  watch(isDragging, (dragging) => {
+    document.body.style.userSelect = dragging ? "none" : "";
+  });
+
+  function onDragStart(event, type, lable, item) {
+    if (event.dataTransfer) {
+      event.dataTransfer.setData("application/vueflow", type);
+      event.dataTransfer.effectAllowed = "move";
+    }
+
+    draggedType.value = type;
+    draggedLable.value = lable;
+    draggedItem.value = item;
+    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() {
+    isDragging.value = false;
+    isDragOver.value = false;
+    draggedType.value = null;
+    draggedLable.value = null;
+    document.removeEventListener("drop", onDragEnd);
+  }
+
+  /**
+   * Handles the drop event.
+   *
+   * @param {DragEvent} event
+   */
+  function onDrop(event) {
+    const position = screenToFlowCoordinate({
+      x: event.clientX,
+      y: event.clientY,
+    });
+
+    const nodeId = getId();
+
+    const newNode = {
+      ...draggedItem.value,
+      id: nodeId,
+      type: draggedType.value,
+      position,
+      data: { label: draggedLable.value },
+    };
+    console.log(newNode, "22");
+    /**
+     * 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(nodeId, (node) => ({
+        position: {
+          x: node.position.x - node.dimensions.width / 2,
+          y: node.position.y - node.dimensions.height / 2,
+        },
+      }));
+
+      off();
+    });
+    addNodes(newNode);
+  }
+
+  return {
+    draggedType,
+    isDragOver,
+    isDragging,
+    onDragStart,
+    onDragLeave,
+    onDragOver,
+    onDrop,
+  };
+}

+ 2 - 1
src/main.ts

@@ -4,6 +4,7 @@ import router from "@/router";
 import { setupStore } from "@/store";
 import { setupDirective } from "@/directive";
 import { setupElIcons, setupI18n, setupPermission } from "@/plugins";
+
 // 本地SVG图标
 import "virtual:svg-icons-register";
 
@@ -31,6 +32,6 @@ setupPermission();
 
 setupEleAvue(app);
 
-app.component('vue-qrcode', VueQrcode)
+app.component("VueQrcode", VueQrcode);
 
 app.use(router).mount("#app");

+ 19 - 0
src/styles/index.scss

@@ -1,5 +1,24 @@
 @use "./reset";
+@import "@vue-flow/core/dist/style.css";
+@import "@vue-flow/core/dist/theme-default.css";
+@import "https://cdn.jsdelivr.net/npm/@vue-flow/core@1.37.1/dist/style.css";
+@import "https://cdn.jsdelivr.net/npm/@vue-flow/core@1.37.1/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";
 
+.vue-flow__minimap {
+  transform: scale(75%);
+  transform-origin: bottom right;
+}
+.vue-flow__node-custom {
+  background: purple;
+  color: white;
+  border: 1px solid purple;
+  border-radius: 4px;
+  box-shadow: 0 0 0 1px purple;
+  padding: 8px;
+}
 .app-container {
   padding: 10px;
 }

+ 208 - 218
src/views/base/craftManagement/route/bindProcess.vue

@@ -5,17 +5,7 @@
   >
     <div class="header">
       <div class="title">绑定工序</div>
-      <el-space>
-        <el-button :icon="Back" size="small" @click="back">返回</el-button>
-        <!--        <el-button type="primary" :icon="Download" size="small">下载</el-button>-->
-        <el-button
-          :type="isChanged ? 'danger' : 'primary'"
-          :icon="Document"
-          size="small"
-          @click="saveFlow"
-          >保存</el-button
-        >
-      </el-space>
+      <el-button :icon="Back" size="small" @click="back">返回</el-button>
     </div>
     <div class="binContainer">
       <div class="processTree">
@@ -32,76 +22,120 @@
                 </div>
               </template>
               <div class="treeChild">
-                <VueDraggable
-                  v-model="pProcess.baseOperationList"
-                  :animation="150"
-                  :group="{ name: 'people', pull: 'clone', put: false }"
-                  :sort="false"
-                  :clone="clone"
+                <div
+                  class="childItem"
+                  v-for="(item, index) in pProcess?.baseOperationList"
+                  :key="index"
+                  :draggable="!editStatus"
+                  @dragstart="
+                    onDragStart($event, nodeType, item.operationName, item)
+                  "
                 >
-                  <div
-                    class="childItem"
-                    v-for="(item, index) in pProcess?.baseOperationList"
-                    :key="item.id"
-                  >
-                    {{ item.operationName }}
-                  </div>
-                </VueDraggable>
+                  {{ item.operationName }}
+                </div>
               </div>
             </el-collapse-item>
           </el-collapse>
         </el-scrollbar>
       </div>
-      <div class="flowBox">
-        <el-scrollbar style="padding-bottom: 20px">
-          <VueDraggable
-            v-model="list2"
-            :animation="150"
-            group="people"
-            @update="onUpdate"
-            style="min-width: 400px"
-            @add="onClone"
+      <div
+        class="flowBox"
+        id="flowBox"
+        style="height: 100%; width: 100%; overflow: hidden"
+      >
+        <div class="dnd-flow" @drop="onDrop">
+          <VueFlow
+            v-model:nodes="flowData.nodes"
+            v-model:edges="flowData.edges"
+            :apply-default="!editStatus"
+            @dragover="onDragOver"
+            @dragleave="onDragLeave"
+            @node-click="nodeClick($event)"
           >
-            <div
-              v-for="(item, index) in list2"
-              :key="item.uuid"
-              class="flowItem"
-              @click="clickFlowItem(item, index)"
+            <template #edge-custom="props">
+              <CustomConnectionLine v-bind="props" />
+            </template>
+            <template #connection-line="props">
+              <CustomConnectionLine v-bind="props" />
+            </template>
+            <template #node-custom="props">
+              <CustomNode v-bind="props" />
+            </template>
+            <DropzoneBackground
+              :style="{
+                backgroundColor: isDragOver ? '#e7f3ff' : 'transparent',
+                transition: 'background-color 0.2s ease',
+              }"
             >
-              <div class="indexLabel">工序 {{ index + 1 }}</div>
-              <div :class="getFlowNameClass(index)">
-                {{ item?.operationVO?.operationName }}
+              <p v-if="isDragOver">拖拽中</p>
+            </DropzoneBackground>
+          </VueFlow>
+
+          <div
+            :style="{
+              width: flowBoxW - 20 + 'px',
+              height: flowBoxH - 20 + 'px',
+            }"
+          ></div>
+        </div>
+      </div>
+      <div class="detailInfo">
+        <el-scrollbar>
+          <div class="opBox">
+            <el-button type="primary" @click="changeEditStatus">{{
+              !editStatus ? "切换为工序信息编辑模式" : "切换为工序路线编辑模式"
+            }}</el-button>
+
+            <el-button @click="getPng">导出流程图 </el-button>
+          </div>
+
+          <!-- 工艺路线编辑模式 -->
+          <div v-if="!editStatus">
+            <div class="btns">
+              <el-button type="primary" @click="saveFlow">保存</el-button>
+              <el-button type="danger" @click="cancelFlow">取消</el-button>
+            </div>
+          </div>
+          <!-- 工序信息编辑模式 -->
+          <div v-else>
+            <div v-if="currentProcess.id">
+              <avue-form
+                ref="formRef"
+                :option="formOption"
+                v-model="currentProcess"
+                style="padding: 10px; background-color: transparent"
+              >
+                <template #tbomUrl>
+                  <FilesUpload
+                    v-model:src="currentProcess.tbomUrl"
+                    :show-tip="false"
+                  />
+                </template>
+              </avue-form>
+              <div class="btns">
+                <el-tooltip
+                  class="box-item"
+                  effect="dark"
+                  content="会同时保存现线路情况,请确认好变更"
+                  placement="bottom"
+                >
+                  <el-button type="primary" @click="saveInfo"
+                    >保存工序详情</el-button
+                  >
+                </el-tooltip>
+                <el-button type="danger" @click="cancelInfo"
+                  >重置该工序详情</el-button
+                >
               </div>
-              <div>
-                <Delete class="flowDelete" @click.stop="clickDelete(index)" />
+              <div class="editProcces">
+                <el-button type="primary" @click="editProComponent"
+                  >编辑工序组件</el-button
+                >
               </div>
-              <Bottom class="arrow" v-show="index < list2.length - 1" />
             </div>
-          </VueDraggable>
-        </el-scrollbar>
-      </div>
-      <div class="detailInfo">
-        <div v-if="selectIndex > -1 && !isChanged">
-          <avue-form
-            ref="formRef"
-            :option="formOption"
-            v-model="currentProcess"
-            style="padding: 10px; background-color: white"
-          >
-            <!--<template #tbomUrl>
-              <FilesUpload
-                v-model:src="currentProcess.tbomUrl"
-                :show-tip="false"
-              />
-            </template>-->
-          </avue-form>
-          <div class="editProcces">
-            <el-button type="primary" :icon="Grid" @click="editProComponent"
-              >编辑工序组件</el-button
-            >
+            <div v-else class="tipContent">请选择工序</div>
           </div>
-        </div>
-        <div v-else class="tipContent">{{ tipContent }}</div>
+        </el-scrollbar>
       </div>
     </div>
   </div>
@@ -115,7 +149,12 @@ import {
   Bottom,
   Grid,
 } from "@element-plus/icons-vue";
-import { VueDraggable } from "vue-draggable-plus";
+import _ from "lodash-es";
+import { VueFlow, useVueFlow } from "@vue-flow/core";
+import DropzoneBackground from "./components/DropzoneBackground/index.vue";
+import CustomConnectionLine from "./components/CustomConnectionLine/index.vue";
+import CustomNode from "./components/CustomNode/index.vue";
+import useDragAndDrop from "@/hooks/useDnD.js";
 import { processTreeList } from "@/api/craft/process/index";
 import { useCommonStoreHook, useDictionaryStore } from "@/store";
 import {
@@ -126,13 +165,47 @@ import {
 import { v4 as uuidv4 } from "uuid";
 import { formOption } from "./bindConfig";
 import { ElMessage } from "element-plus";
-
+import { useScreenshot } from "./screenshot.ts";
+const { capture } = useScreenshot();
+const instance = useVueFlow();
+const { onConnect, addEdges, vueFlowRef, onEdgeUpdateEnd, applyEdgeChanges } =
+  instance;
+const { onDragOver, onDrop, onDragLeave, isDragOver, onDragStart } =
+  useDragAndDrop();
+const flowData = ref({ edges: [], nodes: [] });
+const flowDataCopy = ref({ edges: [], nodes: [] });
+provide("edges", flowData.value.edges);
+const currentProcess = ref({});
+provide("currentProcess", currentProcess);
+const nodeClick = (event) => {
+  if (!editStatus.value) return;
+  currentProcess.value = event.node;
+};
+onConnect(addEdges);
+const getPng = () => {
+  if (!vueFlowRef.value) {
+    console.warn("VueFlow element not found");
+    return;
+  }
+  capture(vueFlowRef.value, { shouldDownload: true });
+};
+const nodeType = ref("custom");
+const editStatus = ref(false);
+const changeEditStatus = () => {
+  editStatus.value = !editStatus.value;
+};
 const router = useRouter();
 const route = useRoute();
-
 // 数据字典相关
 const { dicts } = useDictionaryStore();
-
+//获取画布盒子具体长宽
+const flowBoxH = ref(null);
+const flowBoxW = ref(null);
+const flowBoxScreen = () => {
+  const flowBox = document.getElementById("flowBox");
+  flowBoxH.value = flowBox.clientHeight;
+  flowBoxW.value = flowBox.clientWidth;
+};
 // 顶部====================
 const back = () => {
   router.back();
@@ -142,114 +215,63 @@ const download = () => {};
 
 // 左侧工序树====================
 const activeNames = ref([0]);
-const handleChange = (val) => {};
-
 const list1 = ref([]);
 
-// 由于工序和(工序-工艺)模型字段不同,需要自定义克隆
-const clone = (origin) => {
-  const data = JSON.parse(JSON.stringify(origin));
-  return {
-    created: "",
-    creator: "",
-    deleted: 0,
-    deptId: "",
-    id: null,
-    uuid: uuidv4(),
-    nextOperationId: "", //下一工序id
-    operationId: "", //	当前工序id
-    operationSort: 0, //当前工序排序号
-    operationVO: data,
-    orgId: "",
-    processRouteId: route.params.id, //工艺路线id
-    updated: "",
-    updator: "",
-    virtualCode: "",
-  };
-};
-
 // 保存中间的工序列表
 const saveFlow = async () => {
-  for (let i = 0; i < list2.value.length; i++) {
-    list2.value[i].operationSort = i;
-    list2.value[i].operationId = list2.value[i]?.operationVO?.id;
-    list2.value[i].operation = list2.value[i]?.operationVO; //后端入参和返回不一样,在这里处理一下。
-    if (i === list2.value.length - 1) {
-      list2.value[i].nextOperationId = null;
-    } else {
-      list2.value[i].nextOperationId = list2.value[i + 1]?.operationVO?.id;
-    }
-  }
-  let p = {
+  const { code } = await saveProcessInRoute({
     processRouteId: route.params.id,
-    routeOpList: list2.value,
-  };
-  let res = await saveProcessInRoute(p);
-  // Elmessage.success("保存成功");
-  loadProcessesFlow();
-};
-
-const loadTreeData = () => {
-  processTreeList().then((res) => {
-    list1.value = res.data ?? [];
+    routeData: JSON.stringify({ ...flowData.value }),
   });
-};
-
-// 中间列表====================
-const list2 = ref([]);
-const selectIndex = ref(-1);
-const isChanged = ref(true); //如果中间列表发生了改变则不显示右侧信息
-// 点击中间列表事件,需要设置index和值
-const clickFlowItem = (item, index) => {
-  if (isChanged.value) {
-    ElMessage.warning("请先保存工序列表");
-    return;
+  if (code == "200") {
+    ElMessage.success("保存成功");
   }
-  selectIndex.value = index;
-  currentProcess.value = item.operationVO;
+  loadProcessesFlow();
 };
-
-const clickDelete = (index) => {
-  list2.value.splice(index, 1);
-  onUpdate();
+const cancelFlow = () => {
+  flowData.value = JSON.parse(flowDataCopy.value);
 };
-
-const onClone = () => {
-  isChanged.value = true;
-  tipContent.value = "请先保存工序列表,再选择工序进行编辑";
+const saveInfo = async () => {
+  flowData.value.nodes.forEach((item, index) => {
+    if (item.id == currentProcess.value.id) {
+      flowData.value.nodes[index] = currentProcess.value;
+    }
+  });
+  saveFlow();
+  reset();
 };
-
-const onUpdate = () => {
-  //   中间发生了改变调用
-  selectIndex.value = -1;
-  isChanged.value = true;
-  tipContent.value = "请先保存工序列表,再选择工序进行编辑";
+const cancelInfo = () => {
+  flowData.value.nodes.forEach((item, index) => {
+    if (item.id == currentProcess.value.id) {
+      currentProcess.value = flowData.value.nodes[index];
+    }
+  });
 };
-
-const getFlowNameClass = (index) => {
-  return index === selectIndex.value ? "nameLabelSelect" : "nameLabelCommon";
+const loadTreeData = () => {
+  processTreeList().then((res) => {
+    list1.value = res.data ?? [];
+  });
 };
 
+//通过id获取现存储信息
 const loadProcessesFlow = async () => {
   const res = await processesByRouteId(route.params.id);
-
-  list2.value = res.data ?? [];
-  isChanged.value = false;
-  tipContent.value = "请选择需要编辑的工序";
+  if (res.data) {
+    flowData.value = JSON.parse(res.data.routeData);
+    flowDataCopy.value = res.data.routeData;
+  } else {
+    flowDataCopy.value = JSON.stringify(flowData.value);
+  }
 };
 
-// 右侧具体信息====================
 const formRef = ref(null);
-const currentProcess = ref({});
-const tipContent = ref("");
-const handleSubmit = () => {};
-
+const reset = () => {
+  currentProcess.value = {};
+  editStatus.value = false;
+};
 const editProComponent = async () => {
-  // saveFlow();
-  let res = await updateProcess(currentProcess.value);
-  ElMessage.success("保存成功");
   router.push({
-    path: `/base/craftManagement/processCom/${currentProcess.value.id}`,
+    path: `/base/craftManagement/processCom/${currentProcess.value.operationId}`,
     query: { prodtCode: route.query.prodtCode, routeId: route.query.routeId },
   });
 };
@@ -259,6 +281,7 @@ const editProComponent = async () => {
 onMounted(() => {
   loadTreeData();
   loadProcessesFlow();
+  flowBoxScreen();
 });
 
 const getNameByDictType = (dictValue) => {
@@ -270,18 +293,29 @@ const getNameByDictType = (dictValue) => {
   });
   return str;
 };
+watch(
+  () => flowData.value.edges,
+  () => {
+    flowData.value.edges.forEach((item) => {
+      item.type = "custom";
+    });
+  }
+);
 </script>
 
 <style lang="scss" scoped>
 .binContainer {
   height: calc(100vh - 165px);
   background-color: #f5f7f9;
-  overflow-y: scroll;
   // background-color: white;
   padding: 0;
   display: flex;
 }
-
+.btns {
+  display: flex;
+  justify-content: center;
+  margin: 10px 0;
+}
 .header {
   height: 50px;
   display: flex;
@@ -315,60 +349,6 @@ const getNameByDictType = (dictValue) => {
   display: flex;
   justify-content: center;
   overflow-y: auto;
-  padding-top: 47px;
-  padding-bottom: 47px;
-  .flowContain {
-    width: 400px;
-
-    min-height: 200px;
-  }
-  .flowItem {
-    height: 50px;
-    width: 400px;
-    margin-bottom: 20px;
-    display: flex;
-    justify-content: space-between;
-    position: relative;
-    .indexLabel {
-      line-height: 50px;
-
-      font-size: 14px;
-      color: #6e7993;
-
-      text-align: left;
-    }
-    .nameLabelCommon {
-      width: 299px;
-      line-height: 50px;
-      background: #ffffff;
-      box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.5);
-      border-radius: 4px 4px 4px 4px;
-      border: 1px solid #232032;
-      text-align: center;
-    }
-    .nameLabelSelect {
-      width: 299px;
-      line-height: 50px;
-      background: rgba(10, 89, 247, 0.2);
-      box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.5);
-      border-radius: 4px 4px 4px 4px;
-      border: 2px solid #0a59f7;
-      text-align: center;
-    }
-    .flowDelete {
-      width: 1em;
-      height: 1em;
-      color: red;
-      visibility: hidden;
-    }
-    .arrow {
-      position: absolute;
-      width: 10px;
-      height: 18px;
-      top: 50px;
-      left: 50%;
-    }
-  }
   .flowItem:hover {
     .flowDelete {
       visibility: visible;
@@ -379,11 +359,21 @@ const getNameByDictType = (dictValue) => {
 .detailInfo {
   width: 320px;
   height: 100%;
+  background-color: white;
   border-left: 1px solid rgba(0, 0, 0, 0.1);
+  .opBox {
+    border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+    padding: 0 20px;
+    height: 150px;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-evenly;
+    align-items: center;
+  }
   .editProcces {
     display: flex;
     justify-content: center;
-    margin-top: 15px;
+    margin-bottom: 15px;
   }
 }
 

+ 77 - 0
src/views/base/craftManagement/route/components/CustomConnectionLine/index.vue

@@ -0,0 +1,77 @@
+<template>
+  <!-- 此页面更改线条样式 -->
+  <g>
+    <!-- 圆形markStart -->
+    <circle
+      :cx="props.sourceX"
+      :cy="props.sourceY - 4"
+      r="4"
+      :fill="status ? 'gray' : 'black'"
+    />
+    <!-- path -->
+    <path
+      :d="path"
+      fill="none"
+      :stroke="status ? 'gray' : 'black'"
+      stroke-width="2"
+    />
+    <!-- 箭头markEnd -->
+    <path
+      :transform="transform"
+      :d="`M ${targetX} ${targetY + 2} L ${targetX - 5} ${targetY - 10} L ${targetX + 5} ${targetY - 10} Z`"
+      :fill="status ? 'gray' : 'black'"
+      stroke="none"
+    />
+  </g>
+</template>
+
+<script lang="ts" setup>
+import { getSmoothStepPath, SmoothStepEdgeProps } from "@vue-flow/core";
+import { computed, inject } from "vue";
+//由于props不包含及时的selected参数只有靠依赖注入解决
+const edges = inject("edges");
+const status = ref(true);
+const props = defineProps<SmoothStepEdgeProps>();
+const path = computed(() => getSmoothStepPath(props)[0]);
+const setSeletedStatus = () => {
+  edges?.value.forEach((item: any) => {
+    if (item.id == props.id) {
+      if (item.selected == true) {
+        status.value = false;
+      } else {
+        status.value = true;
+      }
+    }
+  });
+};
+watch(
+  () => edges.value,
+  () => {
+    setSeletedStatus();
+  },
+  { deep: true }
+);
+const transform = computed(() => {
+  return getArrowTransform(props);
+});
+function getArrowTransform(props: SmoothStepEdgeProps) {
+  const { targetPosition } = props;
+  if (targetPosition === "top") {
+    return `rotate(0 ${props.targetX} ${props.targetY})`;
+  }
+  if (targetPosition === "bottom") {
+    return `rotate(180 ${props.targetX} ${props.targetY})`;
+  }
+  if (targetPosition === "left") {
+    return `rotate(-90 ${props.targetX} ${props.targetY})`;
+  }
+  if (targetPosition === "right") {
+    return `rotate(90 ${props.targetX} ${props.targetY})`;
+  }
+}
+</script>
+<script lang="ts">
+export default {
+  name: "Custom",
+};
+</script>

+ 76 - 0
src/views/base/craftManagement/route/components/CustomNode/index.vue

@@ -0,0 +1,76 @@
+<script setup>
+import { Position, Handle } from "@vue-flow/core";
+// props were passed from the slot using `v-bind="customNodeProps"`
+const props = defineProps(["data", "id"]);
+const currentProcess = inject("currentProcess");
+const selectStatus = ref(false);
+const selectNode = () => {
+  if (currentProcess != null) {
+    if (props.id == currentProcess.value.id) {
+      selectStatus.value = true;
+    } else {
+      selectStatus.value = false;
+    }
+  } else {
+    selectStatus.value = false;
+  }
+  console.log(selectStatus.value, "222");
+};
+watch(
+  () => currentProcess.value.id,
+  () => {
+    selectNode();
+  },
+  { deep: true }
+);
+</script>
+
+<template>
+  <div class="nodes" :style="{ borderColor: selectStatus ? 'blue' : 'balck' }">
+    <Handle
+      :style="{
+        height: '13px',
+        width: '13px',
+      }"
+      type="target"
+      :position="Position.Top"
+    />
+    <div
+      style="
+        text-align: center;
+        width: 100%;
+        color: black;
+        word-wrap: break-word;
+        margin: 10px;
+      "
+    >
+      {{ data.label }}
+    </div>
+
+    <Handle
+      :style="{
+        height: '13px',
+        width: '13px',
+      }"
+      type="source"
+      :position="Position.Bottom"
+    />
+  </div>
+</template>
+<style lang="scss">
+.vue-flow__node-custom {
+  padding: 0px !important;
+  background-color: white !important;
+  box-shadow: 0 0 0 0 !important;
+  border: 0px solid black !important;
+}
+.nodes {
+  width: 150px;
+  min-height: 60px;
+  border-radius: 4px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: 1px solid black;
+}
+</style>

+ 14 - 0
src/views/base/craftManagement/route/components/DropzoneBackground/index.vue

@@ -0,0 +1,14 @@
+<script setup>
+import { Background } from "@vue-flow/background";
+
+</script>
+
+<template>
+  <div class="dropzone-background">
+    <Background :size="2" :gap="20" pattern-color="#BDBDBD" />
+
+    <div class="overlay">
+      <slot></slot>
+    </div>
+  </div>
+</template>

+ 85 - 0
src/views/base/craftManagement/route/screenshot.ts

@@ -0,0 +1,85 @@
+import { toJpeg as ElToJpg, toPng as ElToPng } from "html-to-image";
+import { ref } from "vue";
+import type { Options as HTMLToImageOptions } from "html-to-image/es/types";
+import type { ImageType, UseScreenshot, UseScreenshotOptions } from "./types";
+
+export function useScreenshot(): UseScreenshot {
+  const dataUrl = ref<string>("");
+  const imgType = ref<ImageType>("png");
+  const error = ref();
+
+  async function capture(el: HTMLElement, options: UseScreenshotOptions = {}) {
+    let data;
+
+    const fileName = options.fileName ?? `vue-flow-screenshot-${Date.now()}`;
+
+    switch (options.type) {
+      case "jpeg":
+        data = await toJpeg(el, options);
+        break;
+      case "png":
+        data = await toPng(el, options);
+        break;
+      default:
+        data = await toPng(el, options);
+        break;
+    }
+
+    // immediately download the image if shouldDownload is true
+    if (options.shouldDownload && fileName !== "") {
+      download(fileName);
+    }
+
+    return data;
+  }
+
+  function toJpeg(
+    el: HTMLElement,
+    options: HTMLToImageOptions = { quality: 0.95 }
+  ) {
+    error.value = null;
+
+    return ElToJpg(el, options)
+      .then((data) => {
+        dataUrl.value = data;
+        imgType.value = "jpeg";
+        return data;
+      })
+      .catch((error) => {
+        error.value = error;
+        throw new Error(error);
+      });
+  }
+
+  function toPng(
+    el: HTMLElement,
+    options: HTMLToImageOptions = { quality: 0.95 }
+  ) {
+    error.value = null;
+
+    return ElToPng(el, options)
+      .then((data) => {
+        dataUrl.value = data;
+        imgType.value = "png";
+        return data;
+      })
+      .catch((error) => {
+        error.value = error;
+        throw new Error(error);
+      });
+  }
+
+  function download(fileName: string) {
+    const link = document.createElement("a");
+    link.download = `${fileName}.${imgType.value}`;
+    link.href = dataUrl.value;
+    link.click();
+  }
+
+  return {
+    capture,
+    download,
+    dataUrl,
+    error,
+  };
+}

+ 26 - 0
src/views/base/craftManagement/route/type.ts

@@ -0,0 +1,26 @@
+import type { Options as HTMLToImageOptions } from 'html-to-image/es/types';
+import type { Ref } from 'vue';
+
+export type ImageType = 'jpeg' | 'png';
+
+export interface UseScreenshotOptions extends HTMLToImageOptions {
+  type?: ImageType;
+  fileName?: string;
+  shouldDownload?: boolean;
+  fetchRequestInit?: RequestInit;
+}
+
+export type CaptureScreenshot = (
+  el: HTMLElement,
+  options?: UseScreenshotOptions
+) => Promise<string>;
+
+export type Download = (fileName: string) => void;
+
+export interface UseScreenshot {
+  // returns the data url of the screenshot
+  capture: CaptureScreenshot;
+  download: Download;
+  dataUrl: Ref<string>;
+  error: Ref;
+}