Browse Source

复制剪切粘贴功能开发

lichunyang 1 month ago
parent
commit
585d5af3bb

+ 4 - 4
package-lock.json

@@ -14,7 +14,7 @@
         "@monaco-editor/loader": "^1.5.0",
         "@monaco-editor/loader": "^1.5.0",
         "@vue-flow/background": "^1.3.0",
         "@vue-flow/background": "^1.3.0",
         "@vue-flow/controls": "^1.1.2",
         "@vue-flow/controls": "^1.1.2",
-        "@vue-flow/core": "^1.37.1",
+        "@vue-flow/core": "^1.46.4",
         "@vue-flow/minimap": "^1.5.0",
         "@vue-flow/minimap": "^1.5.0",
         "@vue-flow/node-resizer": "^1.4.0",
         "@vue-flow/node-resizer": "^1.4.0",
         "@vue-flow/node-toolbar": "^1.1.0",
         "@vue-flow/node-toolbar": "^1.1.0",
@@ -3134,9 +3134,9 @@
       }
       }
     },
     },
     "node_modules/@vue-flow/core": {
     "node_modules/@vue-flow/core": {
-      "version": "1.44.0",
-      "resolved": "https://registry.npmmirror.com/@vue-flow/core/-/core-1.44.0.tgz",
-      "integrity": "sha512-3efgrj3KCTSSUpPRefL+muRuPv+7Oy8nPDPOhLuJACyiGrBHzAZDyCC9irqsWD1E9ko8g1XYgupReRT0q78fOA==",
+      "version": "1.46.4",
+      "resolved": "https://registry.npmmirror.com/@vue-flow/core/-/core-1.46.4.tgz",
+      "integrity": "sha512-qswfL4acg74wAG8/+x6cryctSzmTfSAi/KOZLWlb3yyIPCK6fG+flGwB2VqDQrlYvZEY8HClJOWQty+T2UpWXg==",
       "dependencies": {
       "dependencies": {
         "@vueuse/core": "^10.5.0",
         "@vueuse/core": "^10.5.0",
         "d3-drag": "^3.0.0",
         "d3-drag": "^3.0.0",

+ 1 - 1
package.json

@@ -16,7 +16,7 @@
     "@monaco-editor/loader": "^1.5.0",
     "@monaco-editor/loader": "^1.5.0",
     "@vue-flow/background": "^1.3.0",
     "@vue-flow/background": "^1.3.0",
     "@vue-flow/controls": "^1.1.2",
     "@vue-flow/controls": "^1.1.2",
-    "@vue-flow/core": "^1.37.1",
+    "@vue-flow/core": "^1.46.4",
     "@vue-flow/minimap": "^1.5.0",
     "@vue-flow/minimap": "^1.5.0",
     "@vue-flow/node-resizer": "^1.4.0",
     "@vue-flow/node-resizer": "^1.4.0",
     "@vue-flow/node-toolbar": "^1.1.0",
     "@vue-flow/node-toolbar": "^1.1.0",

+ 109 - 0
src/components/dialog/FileUploadDialog.vue

@@ -0,0 +1,109 @@
+<template>
+  <el-dialog
+    v-model="dialogVisible"
+    :title="$t('dialog.importProject')"
+    width="500px"
+    align-center
+    :append-to-body="true"
+    draggable
+    @close="handleClose"
+  >
+    <el-form>
+      <el-form-item :label="$t('project.selectFile')">
+        <el-upload
+          ref="uploadRef"
+          :auto-upload="false"
+          :limit="1"
+          :on-change="handleFileChange"
+          :accept="acceptTypes"
+        >
+          <el-button type="primary">
+            {{ $t('project.chooseFile') }}
+          </el-button>
+        </el-upload>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="handleCancel">{{ $t('dialog.cancel') }}</el-button>
+        <el-button type="primary" :disabled="!selectedFile" @click="handleConfirm">
+          {{ $t('dialog.ok') }}
+        </el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { ref, defineEmits, defineProps } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { ElMessage } from 'element-plus';
+
+const { t } = useI18n();
+
+// 定义 props
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false,
+  },
+  acceptTypes: {
+    type: String,
+    default: '.json', // 默认接受 JSON 文件,可根据需求修改
+  },
+});
+
+// 定义 emits
+const emit = defineEmits(['update:visible', 'file-selected']);
+
+// 控制对话框显示
+const dialogVisible = ref(props.visible);
+
+// 监听 visible prop 变化
+watch(
+  () => props.visible,
+  (newVal) => {
+    dialogVisible.value = newVal;
+  }
+);
+
+// 选择的 文件
+const selectedFile = ref(null);
+const uploadRef = ref(null);
+
+// 文件选择变化
+const handleFileChange = (file) => {
+  selectedFile.value = file.raw; // 获取原始文件对象
+};
+
+// 确认按钮
+const handleConfirm = () => {
+  if (selectedFile.value) {
+    emit('file-selected', selectedFile.value);
+    handleClose();
+  } else {
+    ElMessage.warning(t('project.pleaseSelectFile'));
+  }
+};
+
+// 取消按钮
+const handleCancel = () => {
+  handleClose();
+};
+
+// 关闭对话框
+const handleClose = () => {
+  dialogVisible.value = false;
+  emit('update:visible', false);
+  selectedFile.value = null;
+  uploadRef.value.clearFiles(); // 清空上传文件
+};
+</script>
+
+<style scoped>
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+</style>

+ 120 - 117
src/components/layout/HeaderButtonBar.vue

@@ -22,10 +22,10 @@
       <!-- 普通标签页 -->
       <!-- 普通标签页 -->
       <el-tab-pane v-for="tab in tabs" :key="tab.name" :name="tab.name">
       <el-tab-pane v-for="tab in tabs" :key="tab.name" :name="tab.name">
         <template #label>
         <template #label>
-            <span class="tab-label">
-              <img :src="tab.icon" alt="icon" class="tab-icon" />
-              {{ tab.label }}
-            </span>
+          <span class="tab-label">
+            <img :src="tab.icon" alt="icon" class="tab-icon" />
+            {{ tab.label }}
+          </span>
         </template>
         </template>
         <div class="button-group">
         <div class="button-group">
           <el-tooltip
           <el-tooltip
@@ -66,9 +66,9 @@
       </el-tab-pane>
       </el-tab-pane>
       <el-tab-pane name="user" key="userButton">
       <el-tab-pane name="user" key="userButton">
         <template #label>
         <template #label>
-          <span @click.stop style="margin-top:5px;">
+          <span @click.stop style="margin-top: 5px">
             <el-avatar :src="user" :size="22" />
             <el-avatar :src="user" :size="22" />
-            <el-dropdown style="margin-top:4px;">
+            <el-dropdown style="margin-top: 4px">
               <div class="user-dropdown-trigger">
               <div class="user-dropdown-trigger">
                 <span class="nickname">{{ nickName }}</span>
                 <span class="nickname">{{ nickName }}</span>
                 <el-icon class="el-icon--right"><arrow-down /></el-icon>
                 <el-icon class="el-icon--right"><arrow-down /></el-icon>
@@ -218,95 +218,98 @@
     </template>
     </template>
   </el-dialog>
   </el-dialog>
 
 
-<el-dialog
-  v-model="dialog.projectListDialog"
-  align-center
-  :append-to-body="true"
-  width="800"
-  class="dialog_class"
-  draggable
->
-  <template #header="{ titleId, titleClass }">
-    <div class="my-header">
-      <h4 :id="titleId" :class="titleClass">
-        {{ $t("dialog.projectlist") }}
-      </h4>
-    </div>
-  </template>
-
-  <template #default>
-    <div class="project-main-content custom-table">
-      <el-table
-        :data="projectlists"
-        style="width: 100%; height: 540px; overflow: auto"
-        @row-click="handleRowClick"
-        :row-class-name="tableRowClassName"
-      >
-        <el-table-column
-          type="index"
-          :label="$t('project.number')"
-          width="100"
-        ></el-table-column>
-        <el-table-column
-          prop="name"
-          :label="$t('project.name')"
-        ></el-table-column>
-        <el-table-column
-          prop="dirsize"
-          :label="$t('project.dirsize')"
-        ></el-table-column>
-        <el-table-column
-          prop="updateTime"
-          :label="$t('project.updateTime')"
-          min-width="100"
-        ></el-table-column>
-        <el-table-column
-          prop="uname"
-          :label="$t('project.uname')"
-        ></el-table-column>
-        <el-table-column
-          prop="keywords"
-          :label="$t('project.keywords')"
-        ></el-table-column>
-        <el-table-column
-          prop="remark"
-          :label="$t('project.description')"
-        ></el-table-column>
-      </el-table>
-    </div>
-    <div class="project-main-pagination">
-      <div class="custom-pagination">
-        <el-pagination
-          v-model:current-page="gd.currentPage4"
-          v-model:page-size="gd.pageSize4"
-          :page-sizes="[5, 10, 20, 50]"
-          background
-          size="small"
-          layout="prev, slot, sizes, pager, next"
-          :total="parseInt(gd.total)"
-          class="mt-4"
-          @size-change="handleSizeChange"
-          @current-change="handleCurrentChange2"
+  <el-dialog
+    v-model="dialog.projectListDialog"
+    align-center
+    :append-to-body="true"
+    width="800"
+    class="dialog_class"
+    draggable
+  >
+    <template #header="{ titleId, titleClass }">
+      <div class="my-header">
+        <h4 :id="titleId" :class="titleClass">
+          {{ $t("dialog.projectlist") }}
+        </h4>
+      </div>
+    </template>
+
+    <template #default>
+      <div class="project-main-content custom-table">
+        <el-table
+          :data="projectlists"
+          style="width: 100%; height: 540px; overflow: auto"
+          @row-click="handleRowClick"
+          :row-class-name="tableRowClassName"
         >
         >
-          <template #default>
-            <span>{{ $t("project.total") }} {{ gd.total }}</span>
-          </template>
-        </el-pagination>
+          <el-table-column
+            type="index"
+            :label="$t('project.number')"
+            width="100"
+          ></el-table-column>
+          <el-table-column
+            prop="name"
+            :label="$t('project.name')"
+          ></el-table-column>
+          <el-table-column
+            prop="dirsize"
+            :label="$t('project.dirsize')"
+          ></el-table-column>
+          <el-table-column
+            prop="updateTime"
+            :label="$t('project.updateTime')"
+            min-width="100"
+          ></el-table-column>
+          <el-table-column
+            prop="uname"
+            :label="$t('project.uname')"
+          ></el-table-column>
+          <el-table-column
+            prop="keywords"
+            :label="$t('project.keywords')"
+          ></el-table-column>
+          <el-table-column
+            prop="remark"
+            :label="$t('project.description')"
+          ></el-table-column>
+        </el-table>
+      </div>
+      <div class="project-main-pagination">
+        <div class="custom-pagination">
+          <el-pagination
+            v-model:current-page="gd.currentPage4"
+            v-model:page-size="gd.pageSize4"
+            :page-sizes="[5, 10, 20, 50]"
+            background
+            size="small"
+            layout="prev, slot, sizes, pager, next"
+            :total="parseInt(gd.total)"
+            class="mt-4"
+            @size-change="handleSizeChange"
+            @current-change="handleCurrentChange2"
+          >
+            <template #default>
+              <span>{{ $t("project.total") }} {{ gd.total }}</span>
+            </template>
+          </el-pagination>
+        </div>
       </div>
       </div>
-    </div>
-  </template>
-
-  <template #footer>
-    <span class="lastbtn">
-      <el-button @click="dialog.projectListDialog = false">{{
-        $t("dialog.cancel")
-      }}</el-button>
-      <el-button @click="confirmSelected" :disabled="selectedRows.length === 0">
-        {{ $t("dialog.ok") }}
-      </el-button>
-    </span>
-  </template>
-</el-dialog>
+    </template>
+
+    <template #footer>
+      <span class="lastbtn">
+        <el-button @click="dialog.projectListDialog = false">{{
+          $t("dialog.cancel")
+        }}</el-button>
+        <el-button
+          @click="confirmSelected"
+          :disabled="selectedRows.length === 0"
+        >
+          {{ $t("dialog.ok") }}
+        </el-button>
+      </span>
+    </template>
+  </el-dialog>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
@@ -317,14 +320,14 @@ import { useI18n } from "vue-i18n"
 import { useUserStore } from "@/store/user"
 import { useUserStore } from "@/store/user"
 import { removeToken, removeUserId } from "@/utils/token"
 import { removeToken, removeUserId } from "@/utils/token"
 import { ElMessageBox, ElMessage } from "element-plus"
 import { ElMessageBox, ElMessage } from "element-plus"
-import { ArrowDown, User, Key, SwitchButton } from '@element-plus/icons-vue'
+import { ArrowDown, User, Key, SwitchButton } from "@element-plus/icons-vue"
 import { useProjectStore } from "@/store/project"
 import { useProjectStore } from "@/store/project"
 import exitIcon from "@/assets/icons/exit.png"
 import exitIcon from "@/assets/icons/exit.png"
 const userStore = useUserStore()
 const userStore = useUserStore()
 const router = useRouter()
 const router = useRouter()
 const { t } = useI18n()
 const { t } = useI18n()
 const nickName = computed(() => userStore.userInfo.nickName || "User")
 const nickName = computed(() => userStore.userInfo.nickName || "User")
- // 退出
+// 退出
 import openIcon from "@/assets/icons/open.png" // 打开文件
 import openIcon from "@/assets/icons/open.png" // 打开文件
 import saveFileIcon from "@/assets/icons/save.png" // 保存文件
 import saveFileIcon from "@/assets/icons/save.png" // 保存文件
 import saveProjectIcon from "@/assets/icons/saveProject.png" // 保存项目
 import saveProjectIcon from "@/assets/icons/saveProject.png" // 保存项目
@@ -366,7 +369,7 @@ const fakeTabs = [
     type: "",
     type: "",
     icon: exitIcon,
     icon: exitIcon,
     onClick: () => {
     onClick: () => {
-      handleExit();
+      handleExit()
     }
     }
   },
   },
   {
   {
@@ -375,7 +378,7 @@ const fakeTabs = [
     type: "",
     type: "",
     icon: openIcon,
     icon: openIcon,
     onClick: () => {
     onClick: () => {
-      handleOpenProject();
+      handleOpenProject()
     }
     }
   },
   },
   {
   {
@@ -384,7 +387,7 @@ const fakeTabs = [
     type: "",
     type: "",
     icon: saveProjectIcon,
     icon: saveProjectIcon,
     onClick: () => {
     onClick: () => {
-      handleSaveProject();
+      handleSaveProject()
     }
     }
   }
   }
 ]
 ]
@@ -530,20 +533,20 @@ const emitButtonClick = (action) => {
 const handleTabClick = (tab) => {
 const handleTabClick = (tab) => {
   console.log("Tab switched to:", tab.name)
   console.log("Tab switched to:", tab.name)
 }
 }
- // 退出项目
+// 退出项目
 const handleExit = () => {
 const handleExit = () => {
-  router.push("/project");
+  router.push("/project")
 }
 }
 
 
 // 打开项目
 // 打开项目
 const handleOpenProject = () => {
 const handleOpenProject = () => {
-  dialog.value.projectListDialog = true;
-  getprojectlist();
+  dialog.value.projectListDialog = true
+  getprojectlist()
 }
 }
 
 
 // 保存项目
 // 保存项目
 const handleSaveProject = () => {
 const handleSaveProject = () => {
-  console.log("Saving project...");
+  emit("button-click", "save");
 }
 }
 
 
 // 获取项目列表
 // 获取项目列表
@@ -601,34 +604,34 @@ const handleCurrentChange2 = (val) => {
 // 确认选中行
 // 确认选中行
 const confirmSelected = () => {
 const confirmSelected = () => {
   if (selectedRows.value.length !== 1) {
   if (selectedRows.value.length !== 1) {
-    ElMessage.warning('请选择一个项目!');
-    return;
+    ElMessage.warning("请选择一个项目!")
+    return
   }
   }
 
 
-  const selected = selectedRows.value[0];
-  
+  const selected = selectedRows.value[0]
+
   const project = {
   const project = {
     projectId: selected.pid,
     projectId: selected.pid,
     projectName: selected.name || `Project ${projectStore.projects.length + 1}`,
     projectName: selected.name || `Project ${projectStore.projects.length + 1}`,
-    keywords: selected.keywords || '',
-    remark: selected.remark || '',
-    flow: selected.flow ? selected.flow : { "nodes": [], "edges": [] },
-  };
+    keywords: selected.keywords || "",
+    remark: selected.remark || "",
+    flow: selected.flow ? selected.flow : { nodes: [], edges: [] }
+  }
 
 
   // 如果项目已存在,更新信息;否则添加新项目
   // 如果项目已存在,更新信息;否则添加新项目
   if (projectStore.projects.some((p) => p.projectId === selected.pid)) {
   if (projectStore.projects.some((p) => p.projectId === selected.pid)) {
-    projectStore.updateProjectInfo(selected.pid, project);
+    projectStore.updateProjectInfo(selected.pid, project)
   } else {
   } else {
-    projectStore.addProject(project);
+    projectStore.addProject(project)
   }
   }
 
 
   // 设置激活项目
   // 设置激活项目
-  projectStore.setActiveProject(selected.pid);
+  projectStore.setActiveProject(selected.pid)
 
 
   // 跳转到首页
   // 跳转到首页
-  router.push({ path: '/home' });
-  dialog.value.projectListDialog = false;
-};
+  router.push({ path: "/home" })
+  dialog.value.projectListDialog = false
+}
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>

+ 87 - 17
src/components/layout/home.vue

@@ -9,7 +9,7 @@
           <HeaderTabs @button-click="handleButtonClick" />
           <HeaderTabs @button-click="handleButtonClick" />
         </div>
         </div>
         <div class="home-page-content">
         <div class="home-page-content">
-          <router-view :key="activeProjectId" />
+          <router-view :key="activeProjectId"  @save="handleSave" @copy="handleCopy" @cut="handleCut" @paste="handlePaste"/>
         </div>
         </div>
         <!-- 底部项目标签栏 -->
         <!-- 底部项目标签栏 -->
         <div class="project-tabs">
         <div class="project-tabs">
@@ -41,7 +41,7 @@
       </div>
       </div>
     </el-main>
     </el-main>
   </div>
   </div>
-    <el-dialog
+  <el-dialog
     v-model="addProjectdialog"
     v-model="addProjectdialog"
     align-center
     align-center
     :append-to-body="true"
     :append-to-body="true"
@@ -105,6 +105,11 @@
       </span>
       </span>
     </template>
     </template>
   </el-dialog>
   </el-dialog>
+  <FileUploadDialog
+      v-model:visible="showFileSelector"
+      accept-types=".json"
+      @file-selected="handleFileSelected"
+  />
 </template>
 </template>
 
 
 <script setup>
 <script setup>
@@ -118,6 +123,8 @@ import { useI18n } from 'vue-i18n';
 import modelIcon from '@/assets/icons/model.png';
 import modelIcon from '@/assets/icons/model.png';
 import { ref, computed, watch, onMounted, nextTick } from 'vue';
 import { ref, computed, watch, onMounted, nextTick } from 'vue';
 import { request } from "@/utils/request"
 import { request } from "@/utils/request"
+import emitter from "@/utils/emitter"
+import FileUploadDialog from '@/components/dialog/FileUploadDialog.vue';
 const { t, locale } = useI18n()
 const { t, locale } = useI18n()
 const router = useRouter();
 const router = useRouter();
 const route = useRoute();
 const route = useRoute();
@@ -129,9 +136,12 @@ const activeTab = ref(route.path.slice(1) || 'model');
 const activeProjectId = computed(() => projectStore.activeProjectId);
 const activeProjectId = computed(() => projectStore.activeProjectId);
 
 
 // 控制添加项目对话框显示
 // 控制添加项目对话框显示
-let addProjectdialog = ref(false)
+let addProjectdialog = ref(false);
 // 表单引用
 // 表单引用
-const projectForm = ref(null)
+const projectForm = ref(null);
+
+// 控制 FileSelector 对话框显示
+const showFileSelector = ref(false);
 
 
 let newproject = ref({
 let newproject = ref({
   name: "",
   name: "",
@@ -215,19 +225,33 @@ const rules = ref({
 })
 })
 
 
 const handleButtonClick = (action) => {
 const handleButtonClick = (action) => {
-  switch (action) {
-    case 'importProject':
-      const newProject = {
-        projectId: `proj-${Date.now()}`,
-        projectName: `Project ${projectStore.projects.length + 1}`,
-      };
-      projectStore.addProject(newProject);
-      ElMessage.success(t('message.importSuccess'));
-      nextTick(() => {
-        updateAddButtonPosition(); // 手动触发位置更新
-      });
-      break;
-  }
+    switch (action) {
+      case 'copy':
+        emitter.emit('copy');
+        break;
+      case 'cut':
+        emitter.emit('cut');
+        break;
+      case 'paste':
+        emitter.emit('paste');
+        break;
+      case 'save':    
+        emitter.emit('save');
+        break;
+      case 'importProject':
+        showFileSelector.value = true;
+        // const newProject = {
+        //   projectId: `proj-${Date.now()}`,
+        //   projectName: `Project ${projectStore.projects.length + 1}`,
+        // };
+        // projectStore.addProject(newProject);
+        // ElMessage.success(t('message.importSuccess'));
+        // nextTick(() => updateAddButtonPosition());
+        break;
+      default:
+        console.log(`Button clicked: ${action}`);
+        break;
+    }
 };
 };
 
 
 const handleProjectTabClick = (tab) => {
 const handleProjectTabClick = (tab) => {
@@ -241,6 +265,36 @@ const handleProjectTabRemove = (projectId) => {
   });
   });
 };
 };
 
 
+// 处理文件选择
+const handleFileSelected = (file) => {
+  const formData = new FormData();
+  formData.append('file', file);
+  formData.append('transCode', 'ES0003'); // 假设导入项目的 transCode 为 ES0003
+
+  request(formData, {
+    headers: {
+      'Content-Type': 'multipart/form-data',
+    },
+  })
+    .then((res) => {
+      // 假设后端返回项目信息
+      const newProject = {
+        projectId: res.pid, // 使用后端返回的 pid
+        projectName: res.name || `Imported Project ${Date.now()}`,
+        keywords: res.keywords || '',
+        remark: res.remark || '',
+        flow: res.flow || '{"nodes":[],"edges":[]}',
+      };
+      projectStore.addProject(newProject);
+      projectStore.setActiveProject(newProject.projectId);
+      ElMessage.success(t('message.importSuccess'));
+      nextTick(() => updateAddButtonPosition());
+    })
+    .catch((err) => {
+      ElMessage.error(err.returnMsg || t('message.importFailed'));
+    });
+};
+
 // 添加项目
 // 添加项目
 const addProject = () => {
 const addProject = () => {
   const params = {
   const params = {
@@ -267,6 +321,22 @@ const addProject = () => {
       ElMessage.error(err.returnMsg)
       ElMessage.error(err.returnMsg)
     })
     })
 }
 }
+
+const handleSave = () => {
+  emitter.emit('save');
+};
+
+const handleCopy = () => {
+  emitter.emit('copy');
+};
+
+const handleCut = () => {
+  emitter.emit('cut');
+};
+
+const handlePaste = () => {
+  emitter.emit('paste');
+};
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>

+ 19 - 0
src/store/clipboard.js

@@ -0,0 +1,19 @@
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+
+export const useClipboardStore = defineStore('clipboard', {
+  state: () => ({
+    clipboard: { nodes: [], edges: [], pid: "" },
+  }),
+  actions: {
+    setClipboard(nodes, edges, pid) {
+      this.clipboard = { nodes, edges, pid };
+    },
+    getClipboard() {
+      return this.clipboard;
+    },
+    clearClipboard() {
+      this.clipboard = { nodes: [], edges: [], pid:"" };
+    },
+  },
+});

+ 61 - 45
src/views/model/index.vue

@@ -151,44 +151,21 @@
                         /></el-icon>
                         /></el-icon>
                       </el-button>
                       </el-button>
                     </el-tooltip>
                     </el-tooltip>
-
-                    <el-tooltip content="删除节点" placement="top">
-                      <el-button @click="removeNode" class="custom-icon-button">
-                        <el-icon class="btn-icon" :color="iconcolor"
-                          ><DocumentDelete
-                        /></el-icon>
-                      </el-button>
-                    </el-tooltip>
-
-                    <el-tooltip content="删除线" placement="top">
-                      <el-button @click="removeEdge" class="custom-icon-button">
-                        <el-icon class="btn-icon" :color="iconcolor"
-                          ><Crop
-                        /></el-icon>
-                      </el-button>
-                    </el-tooltip>
-
-                    <el-tooltip content="清空全部" placement="top">
-                      <el-button
-                        @click="confirmDelete"
-                        class="custom-icon-button"
-                      >
-                        <el-icon class="btn-icon" :color="iconcolor"
-                          ><DeleteFilled
-                        /></el-icon>
-                      </el-button>
-                    </el-tooltip>
                   </el-space>
                   </el-space>
                 </el-tab-pane>
                 </el-tab-pane>
               </el-tabs> -->
               </el-tabs> -->
             <!-- </div> -->
             <!-- </div> -->
             <div class="flow-content">
             <div class="flow-content">
               <vueflow 
               <vueflow 
-                ref="vueflowref" 
+                ref="vueflowRef" 
                 :jobId="jobId"
                 :jobId="jobId"
                 :showGrid="buttonStates.showGrid"
                 :showGrid="buttonStates.showGrid"
                 :fixedGrid="buttonStates.fixedGrid"
                 :fixedGrid="buttonStates.fixedGrid"
                 :isSelectMode="modeSwitch"
                 :isSelectMode="modeSwitch"
+                @save="handleSave"
+                @copy="handleCopy"
+                @cut="handleCut"
+                @paste="handlePaste"
               />
               />
             </div>
             </div>
           </pane>
           </pane>
@@ -234,6 +211,7 @@
 import { Splitpanes, Pane } from "splitpanes"
 import { Splitpanes, Pane } from "splitpanes"
 import "splitpanes/dist/splitpanes.css"
 import "splitpanes/dist/splitpanes.css"
 import { request, getImage } from "@/utils/request"
 import { request, getImage } from "@/utils/request"
+import emitter from '@/utils/emitter';
 import { ElMessage } from "element-plus"
 import { ElMessage } from "element-plus"
 import { useProjectStore } from "@/store/project"
 import { useProjectStore } from "@/store/project"
 import { useI18n } from "vue-i18n"
 import { useI18n } from "vue-i18n"
@@ -266,7 +244,7 @@ import topoIcon from "@/assets/icons/topo.png"
 import expandIcon from "@/assets/img/treeExpand.png"
 import expandIcon from "@/assets/img/treeExpand.png"
 import collapseIcon from "@/assets/img/treeCollapse.png"
 import collapseIcon from "@/assets/img/treeCollapse.png"
 
 
-const { zoomIn, zoomOut, fitView } = useVueFlow()
+const { zoomIn, zoomOut, fitView, setInteractive } = useVueFlow()
 
 
 const { t, locale } = useI18n()
 const { t, locale } = useI18n()
 
 
@@ -336,7 +314,7 @@ let pid = computed(() => projectStore.pid || "")
 let spacesize = ref(10)
 let spacesize = ref(10)
 let colactiveNames = ref(["1"])
 let colactiveNames = ref(["1"])
 let collapseData = ref()
 let collapseData = ref()
-const vueflowref = ref()
+const vueflowRef = ref()
 const dark = ref(false)
 const dark = ref(false)
 let iconcolor = ref("#000")
 let iconcolor = ref("#000")
 
 
@@ -349,7 +327,7 @@ const jobId = ref()
 
 
 const SLdatadialogref = ref(null)
 const SLdatadialogref = ref(null)
 const RunDialogRef = ref(null)
 const RunDialogRef = ref(null)
-const modeSwitch = ref(false) // 连接模式开关
+const modeSwitch = ref(true) // 连接模式开关
 const buttonStates = ref({ // 拓扑按钮状态
 const buttonStates = ref({ // 拓扑按钮状态
   showGrid: false,
   showGrid: false,
   fixedGrid: false
   fixedGrid: false
@@ -427,36 +405,36 @@ const getComponent = () => {
 }
 }
 
 
 const resetTransform = () => {
 const resetTransform = () => {
-  if (vueflowref.value) {
-    vueflowref.value.resetTransform()
+  if (vueflowRef.value) {
+    vueflowRef.value.resetTransform()
   }
   }
 }
 }
 
 
 const toggleDarkMode = () => {
 const toggleDarkMode = () => {
-  if (vueflowref.value) {
-    vueflowref.value.toggleDarkMode()
+  if (vueflowRef.value) {
+    vueflowRef.value.toggleDarkMode()
   }
   }
 }
 }
 
 
 const saveproject = () => {
 const saveproject = () => {
-  if (vueflowref.value) {
-    vueflowref.value.saveproject()
+  if (vueflowRef.value) {
+    vueflowRef.value.saveproject()
   }
   }
 }
 }
 const removeNode = () => {
 const removeNode = () => {
-  if (vueflowref.value) {
-    vueflowref.value.removeNode()
+  if (vueflowRef.value) {
+    vueflowRef.value.removeNode()
   }
   }
 }
 }
 const removeEdge = () => {
 const removeEdge = () => {
-  if (vueflowref.value) {
-    vueflowref.value.removeEdge()
+  if (vueflowRef.value) {
+    vueflowRef.value.removeEdge()
   }
   }
 }
 }
 
 
 const confirmDelete = () => {
 const confirmDelete = () => {
-  if (vueflowref.value) {
-    vueflowref.value.confirmDelete()
+  if (vueflowRef.value) {
+    vueflowRef.value.confirmDelete()
   }
   }
 }
 }
 
 
@@ -546,7 +524,7 @@ const handleTimelineItemClick = (index) => {
   selectedIndex.value = index
   selectedIndex.value = index
   console.log("Timeline item clicked:", activities.value[index])
   console.log("Timeline item clicked:", activities.value[index])
   jobId.value = activities.value[index].jobId
   jobId.value = activities.value[index].jobId
-  vueflowref.value?.asideDataref?.getresultData(jobId.value)
+  vueflowRef.value?.asideDataref?.getresultData(jobId.value)
 }
 }
 
 
 // 处理 topoButtonBar 触发的 button-click 事件
 // 处理 topoButtonBar 触发的 button-click 事件
@@ -558,9 +536,35 @@ const handleTopoButtonClick = ({ action, isActive }) => {
 // 处理 topoButtonBar 触发的 mode-switch 事件
 // 处理 topoButtonBar 触发的 mode-switch 事件
 const handleTopoModeSwitch = (value) => {
 const handleTopoModeSwitch = (value) => {
   modeSwitch.value = value
   modeSwitch.value = value
+  setInteractive(!value)
   console.log(value ? "切换到选择模式" : "切换到连接模式")
   console.log(value ? "切换到选择模式" : "切换到连接模式")
 }
 }
 
 
+// 新增事件处理函数
+const handleSave = () => {
+  if (vueflowRef.value) {
+    vueflowRef.value.saveproject();
+  }
+};
+
+const handleCopy = () => {
+  if (vueflowRef.value) {
+    vueflowRef.value.copyNodes();
+  }
+};
+
+const handleCut = () => {
+  if (vueflowRef.value) {
+    vueflowRef.value.cutNodes();
+  }
+};
+
+const handlePaste = () => {
+  if (vueflowRef.value) {
+    vueflowRef.value.pasteNodes();
+  }
+};
+
 
 
 //websockct的连接
 //websockct的连接
 function initWebSocket() {
 function initWebSocket() {
@@ -699,7 +703,19 @@ onMounted(() => {
     initWebSocket()
     initWebSocket()
     getlogs()
     getlogs()
   }, 1500)
   }, 1500)
-})
+
+  emitter.on('save', handleSave);
+  emitter.on('copy', handleCopy);
+  emitter.on('cut', handleCut);
+  emitter.on('paste', handlePaste);
+});
+
+onUnmounted(() => {
+  emitter.off('save', handleSave);
+  emitter.off('copy', handleCopy);
+  emitter.off('cut', handleCut);
+  emitter.off('paste', handlePaste);
+});
 
 
 // onBeforeUnmount(() => {
 // onBeforeUnmount(() => {
 //   websock?.close()
 //   websock?.close()

+ 360 - 111
src/views/model/vueflow/index.vue

@@ -1,6 +1,8 @@
 <template>
 <template>
   <splitpanes class="default-theme">
   <splitpanes class="default-theme">
     <pane min-size="50" size="100" max-size="100">
     <pane min-size="50" size="100" max-size="100">
+              <!-- :nodes-draggable="isSelectMode"
+        :nodes-connectable="isSelectMode ? false : true" -->
       <VueFlow
       <VueFlow
         ref="vueFlowRef"
         ref="vueFlowRef"
         v-model:nodes="nodes"
         v-model:nodes="nodes"
@@ -10,8 +12,8 @@
         :default-viewport="{ zoom: 1.5 }"
         :default-viewport="{ zoom: 1.5 }"
         :min-zoom="0.2"
         :min-zoom="0.2"
         :max-zoom="2.5"
         :max-zoom="2.5"
-        :nodes-draggable="isSelectMode"
-        :nodes-connectable="isSelectMode ? false : true"
+        :multi-selection-key-code="'Control'" 
+        :selection-key-code="'Shift'"
         @drop="customOnDrop"
         @drop="customOnDrop"
         @node-contextmenu="onNodeContextMenu"
         @node-contextmenu="onNodeContextMenu"
         @dragover="onDragOver"
         @dragover="onDragOver"
@@ -21,6 +23,8 @@
         @node-click="onNodeClick"
         @node-click="onNodeClick"
         @edge-double-click="onEdgeDoubleClick"
         @edge-double-click="onEdgeDoubleClick"
         @node-drag="onNodeDrag"
         @node-drag="onNodeDrag"
+        @nodes-selection-change="onNodesSelectionChange"
+        @edges-selection-change="onEdgesSelectionChange"
       >
       >
         <!-- 自定义节点类型为default的节点 -->
         <!-- 自定义节点类型为default的节点 -->
         <template #node-default="props">
         <template #node-default="props">
@@ -60,7 +64,7 @@ import {
   ElMessageBox
   ElMessageBox
 } from "element-plus"
 } from "element-plus"
 import { DeleteFilled } from "@element-plus/icons-vue"
 import { DeleteFilled } from "@element-plus/icons-vue"
-
+import { onBeforeRouteLeave } from "vue-router"
 import { request, uploadFile, getImageBase64 } from "@/utils/request"
 import { request, uploadFile, getImageBase64 } from "@/utils/request"
 import { Background } from "@vue-flow/background"
 import { Background } from "@vue-flow/background"
 import "./main.css" //重置样式
 import "./main.css" //重置样式
@@ -68,12 +72,13 @@ import defaultnode from "./defaultnode.vue"
 import PointOnlyNode from "./pointonlynode.vue"
 import PointOnlyNode from "./pointonlynode.vue"
 import useDragAndDrop from "./useDnD"
 import useDragAndDrop from "./useDnD"
 import { useProjectStore } from "@/store/project"
 import { useProjectStore } from "@/store/project"
+import { useClipboardStore } from '@/store/clipboard'
 import emitter from "@/utils/emitter"
 import emitter from "@/utils/emitter"
-import { onMounted, onUnmounted, nextTick } from "vue"
+import { onMounted, onUnmounted, onBeforeUnmount, nextTick } from "vue"
 import { Splitpanes, Pane } from "splitpanes"
 import { Splitpanes, Pane } from "splitpanes"
 import "splitpanes/dist/splitpanes.css"
 import "splitpanes/dist/splitpanes.css"
 import asideData from "./aside/asideData.vue"
 import asideData from "./aside/asideData.vue"
-
+import { copyNodes, cutNodes, pasteNodes  } from "./utils/clipboardUtils";
 import node from "@/assets/img/node.png"
 import node from "@/assets/img/node.png"
 import nodePoint from "@/assets/img/node.png"
 import nodePoint from "@/assets/img/node.png"
 import tempic from "@/assets/img/temp.png"
 import tempic from "@/assets/img/temp.png"
@@ -82,11 +87,14 @@ import { debounce } from 'lodash-es'
 
 
 // 常量定义
 // 常量定义
 const GRID_SIZE = 10
 const GRID_SIZE = 10
-const DEBOUNCE_DELAY = 500
+const DEBOUNCE_DELAY = 1000
 const DEFAULT_LINE_COLOR = "#2267B1"
 const DEFAULT_LINE_COLOR = "#2267B1"
 const DEFAULT_LINE_WIDTH = 1
 const DEFAULT_LINE_WIDTH = 1
 
 
-// 获取路由实例(如果未来需要)
+// 定义一个新的 ref 用于存储边的选择状态
+const edgeSelectionState = ref({}); // 格式: { [edgeId]: { class: string, style: object } }
+
+// 获取路由实例
 const router = useRouter()
 const router = useRouter()
 
 
 const {
 const {
@@ -103,9 +111,13 @@ const {
   updateNodeInternals,
   updateNodeInternals,
   onNodeDrag,
   onNodeDrag,
   snapToGrid,
   snapToGrid,
-  snapGrid
+  snapGrid,
+  setInteractive,
+  removeNodes,
+  removeEdges,
+  selectedNodes: vueFlowSelectedNodes,
+  selectedEdges: vueFlowSelectedEdges,
 } = useVueFlow()
 } = useVueFlow()
-
 const { onDragOver, onDrop, onDragLeave, isDragOver } = useDragAndDrop()
 const { onDragOver, onDrop, onDragLeave, isDragOver } = useDragAndDrop()
 
 
 const dark = ref(false)
 const dark = ref(false)
@@ -114,6 +126,8 @@ const iconcolor = ref("#000")
 const edges = ref([])
 const edges = ref([])
 const nodes = ref([])
 const nodes = ref([])
 const mergedObj = ref("")
 const mergedObj = ref("")
+const selectedNodes = ref([]); // 选中节点
+const selectedEdges = ref([]); // 选中边
 
 
 // 选中状态
 // 选中状态
 const selectedNode = ref(null)
 const selectedNode = ref(null)
@@ -124,8 +138,13 @@ const lineWidth = ref(DEFAULT_LINE_WIDTH)
 const midNodeCounter = 0
 const midNodeCounter = 0
 
 
 const projectStore = useProjectStore()
 const projectStore = useProjectStore()
+const clipboardStore = useClipboardStore();
 const pid = computed(() => projectStore.pid || "")
 const pid = computed(() => projectStore.pid || "")
 
 
+// 删除组件和边的列表
+const pendingDelete = ref({ nodes: [], edges: [] });
+const isCutting = ref(false);
+
 const showPanel = ref(false)
 const showPanel = ref(false)
 const asideDataref = ref()
 const asideDataref = ref()
 
 
@@ -139,43 +158,52 @@ const props = defineProps({
 // 保存项目
 // 保存项目
 const saveproject = async () => {
 const saveproject = async () => {
   try {
   try {
-    const activeProject = projectStore.getActiveProject()
+    const activeProject = projectStore.getActiveProject();
     if (!activeProject) {
     if (!activeProject) {
-      throw new Error('当前没有激活的项目')
+      throw new Error("当前没有激活的项目");
     }
     }
 
 
     const obj = {
     const obj = {
-      nodes: toObject().nodes.map(node => ({
+      nodes: toObject().nodes.map((node) => ({
         ...node,
         ...node,
-        data: { ...node.data, image: undefined }
+        data: { ...node.data, image: undefined },
       })),
       })),
       edges: toObject().edges,
       edges: toObject().edges,
-    }
+    };
 
 
-    mergedObj.value = JSON.stringify(obj)
+    mergedObj.value = JSON.stringify(obj);
 
 
     const params = {
     const params = {
-      transCode: 'ES0002',
+      transCode: "ES0002",
       pid: pid.value,
       pid: pid.value,
       flow: mergedObj.value,
       flow: mergedObj.value,
-      name: activeProject.projectName || '',
-      remark: activeProject.remark || '',
-      keywords: activeProject.keywords || '',
+      name: activeProject.projectName || "",
+      remark: activeProject.remark || "",
+      keywords: activeProject.keywords || "",
+    };
+
+    // 保存前处理待删除的组件和边
+    for (const { pcId } of pendingDelete.value.nodes) {
+      await comdelete(pcId);
     }
     }
+    for (const { npcId, pcId } of pendingDelete.value.edges) {
+      await comlinedelete(npcId, pcId);
+    }
+    pendingDelete.value = { nodes: [], edges: [] }; // 清空待删除列表
 
 
-    await request(params)
+    await request(params);
 
 
     projectStore.updateProjectInfo(projectStore.activeProjectId, {
     projectStore.updateProjectInfo(projectStore.activeProjectId, {
       ...activeProject,
       ...activeProject,
       flow: mergedObj.value,
       flow: mergedObj.value,
-    })
+    });
 
 
-    ElMessage.success('保存成功')
+    ElMessage.success("保存成功");
   } catch (error) {
   } catch (error) {
-    console.error('保存失败:', error)
-    ElMessage.error('保存失败,请稍后重试')
+    console.error("保存失败:", error);
+    ElMessage.error("保存失败,请稍后重试");
   }
   }
-}
+};
 
 
 const debouncedSave = debounce(saveproject, DEBOUNCE_DELAY)
 const debouncedSave = debounce(saveproject, DEBOUNCE_DELAY)
 
 
@@ -184,52 +212,89 @@ let prevEdges = []
 let isInitializing = ref(true)
 let isInitializing = ref(true)
 
 
 // 合并的 watch:监听节点和边变化
 // 合并的 watch:监听节点和边变化
-watch([nodes, edges], ([newNodes, newEdges]) => {
-  if (isInitializing.value) {
-    prevNodes = [...newNodes]
-    prevEdges = [...newEdges]
-    isInitializing.value = false
-    return
-  }
+watch(
+  [nodes, edges],
+  ([newNodes, newEdges]) => {
+    if (isInitializing.value) {
+      prevNodes = [...newNodes];
+      prevEdges = [...newEdges];
+      isInitializing.value = false;
+      return;
+    }
 
 
-  // 处理节点删除
-  const deletedNodes = prevNodes.filter(n => !newNodes.some(nn => nn.id === n.id))
-  if (deletedNodes.length > 0) {
-    deletedNodes.forEach(node => {
-      if (node.data?.pcId) {
-        comdelete(node.data.pcId)
+    if (isCutting.value) {
+      // 剪切时不触发保存,仅记录删除
+      const deletedNodes = prevNodes.filter((n) => !newNodes.some((nn) => nn.id === n.id));
+      if (deletedNodes.length > 0) {
+        pendingDelete.value.nodes.push(...deletedNodes.map((node) => ({ pcId: node.data.pcId })));
       }
       }
-    })
-  }
 
 
-  // 处理边删除
-  const deletedEdges = prevEdges.filter(e => !newEdges.some(ne => ne.id === e.id))
-  if (deletedEdges.length > 0) {
-    deletedEdges.forEach(edge => {
-      const sourceNode = prevNodes.find(n => n.id === edge.source)
-      const targetNode = prevNodes.find(n => n.id === edge.target)
-      if (!sourceNode || !targetNode) return
-
-      let npcId = '', pcId = ''
-      if (sourceNode.data?.comId === '3') {
-        npcId = sourceNode.data.pcId
-        pcId = targetNode.data?.pcId
-      } else if (targetNode.data?.comId === '3') {
-        npcId = targetNode.data.pcId
-        pcId = sourceNode.data?.pcId
-      }
+      const deletedEdges = prevEdges.filter((e) => !newEdges.some((ne) => ne.id === e.id));
+      if (deletedEdges.length > 0) {
+        deletedEdges.forEach((edge) => {
+          const sourceNode = prevNodes.find((n) => n.id === edge.source);
+          const targetNode = prevNodes.find((n) => n.id === edge.target);
+          if (!sourceNode || !targetNode) return;
+
+          let npcId = "",
+            pcId = "";
+          if (sourceNode.data?.comId === "3") {
+            npcId = sourceNode.data.pcId;
+            pcId = targetNode.data?.pcId;
+          } else if (targetNode.data?.comId === "3") {
+            npcId = targetNode.data.pcId;
+            pcId = sourceNode.data?.pcId;
+          }
 
 
-      if (npcId && pcId) {
-        comlinedelete(npcId, pcId)
+          if (npcId && pcId) {
+            pendingDelete.value.edges.push({ npcId, pcId });
+          }
+        });
       }
       }
-    })
-  }
 
 
-  prevNodes = [...newNodes]
-  prevEdges = [...newEdges]
-  debouncedSave()
-}, { deep: true })
+      prevNodes = [...newNodes];
+      prevEdges = [...newEdges];
+      debouncedSave.cancel();
+      return; // 阻止 debouncedSave
+    }
+
+    // 其他操作(如添加、删除)触发保存
+    const deletedNodes = prevNodes.filter((n) => !newNodes.some((nn) => nn.id === n.id));
+    if (deletedNodes.length > 0) {
+      pendingDelete.value.nodes.push(...deletedNodes.map((node) => ({ pcId: node.data.pcId })));
+    }
+
+    const deletedEdges = prevEdges.filter((e) => !newEdges.some((ne) => ne.id === e.id));
+    if (deletedEdges.length > 0) {
+      deletedEdges.forEach((edge) => {
+        const sourceNode = prevNodes.find((n) => n.id === edge.source);
+        const targetNode = prevNodes.find((n) => n.id === edge.target);
+        if (!sourceNode || !targetNode) return;
+
+        let npcId = "",
+          pcId = "";
+        if (sourceNode.data?.comId === "3") {
+          npcId = sourceNode.data.pcId;
+          pcId = targetNode.data?.pcId;
+        } else if (targetNode.data?.comId === "3") {
+          npcId = targetNode.data.pcId;
+          pcId = sourceNode.data?.pcId;
+        }
+
+        if (npcId && pcId) {
+          pendingDelete.value.edges.push({ npcId, pcId });
+        }
+      });
+    }
+
+    prevNodes = [...newNodes];
+    prevEdges = [...newEdges];
+    debouncedSave();
+  },
+  { deep: false }
+);
 
 
+// 网格固定
 watch(
 watch(
   () => props.fixedGrid,
   () => props.fixedGrid,
   (newValue) => {
   (newValue) => {
@@ -238,23 +303,66 @@ watch(
   },
   },
   { immediate: true }
   { immediate: true }
 )
 )
+
 onMounted(() => {
 onMounted(() => {
   flowInit()
   flowInit()
   // 点击画布取消边选中
   // 点击画布取消边选中
   if (vueFlowRef.value) {
   if (vueFlowRef.value) {
     vueFlowRef.value.$el.addEventListener("click", (event) => {
     vueFlowRef.value.$el.addEventListener("click", (event) => {
       if (selectedEdge.value && !event.target.closest(".vue-flow__edge")) {
       if (selectedEdge.value && !event.target.closest(".vue-flow__edge")) {
-        cleanEdgeSelect()
+        cleanEdgeSelect();
+        selectedNodes.value = [];
+        selectedEdges.value = [];
       }
       }
     })
     })
   }
   }
+  console.log('Binding keydown event listener');
   window.addEventListener('keydown', handleKeyDown)
   window.addEventListener('keydown', handleKeyDown)
+  window.addEventListener('beforeunload', handleBeforeUnload) // 绑定页面关闭事件
+})
+
+onBeforeUnmount(() =>{
+  saveproject();
+  
 })
 })
 
 
 onUnmounted(() => {
 onUnmounted(() => {
   window.removeEventListener('keydown', handleKeyDown)
   window.removeEventListener('keydown', handleKeyDown)
+  window.removeEventListener('beforeunload', handleBeforeUnload) // 移除页面关闭事件
+})
+
+// 在路由切换前保存
+onBeforeRouteLeave((to, from, next) => {
+  console.log('onBeforeRouteLeave triggered, saving project')
+  debouncedSave.flush() // 立即执行任何待保存的操作
+  saveproject().then(() => {
+    console.log('Save completed before route leave')
+    next()
+  }).catch((error) => {
+    console.error('Save failed before route leave:', error)
+    ElMessage.error('保存失败,请检查网络后重试')
+    next() // 即使保存失败,也允许路由切换
+  })
 })
 })
 
 
+const handleBeforeUnload = (event) => {
+  console.log('beforeunload triggered, saving project')
+  debouncedSave.flush() // 立即执行待保存操作
+  // 注意:beforeunload 中无法等待异步 saveproject 完成
+  event.preventDefault()
+  event.returnValue = '项目尚未保存,确定要离开吗?' // 提示用户
+}
+
+// 新增:监听节点选择变化(包括 Ctrl+点击和框选)
+const onNodesSelectionChange = (event) => {
+  selectedNodes.value = Array.from(vueFlowSelectedNodes.value);
+};
+
+// 新增:监听边选择变化
+const onEdgesSelectionChange = (event) => {
+  selectedEdges.value = Array.from(vueFlowSelectedEdges.value);
+};
+
 // 网格吸附逻辑
 // 网格吸附逻辑
 onNodeDrag(({ node }) => {
 onNodeDrag(({ node }) => {
   if (!props.fixedGrid || node.type !== 'point-only') return
   if (!props.fixedGrid || node.type !== 'point-only') return
@@ -268,10 +376,37 @@ onNodeDrag(({ node }) => {
 
 
 // 键盘事件:Backspace 删除选中节点
 // 键盘事件:Backspace 删除选中节点
 const handleKeyDown = (event) => {
 const handleKeyDown = (event) => {
-  if (event.key === 'Backspace') {
-    removeNode()
+  if (event.ctrlKey) {
+    if (event.key === 'c') {
+      event.preventDefault(); // 阻止默认行为
+      event.stopPropagation(); // 阻止事件冒泡
+      copyNodes(selectedNodes.value, selectedEdges.value, edges.value, nodes.value, pid.value);
+    } else if (event.key === 'x') {
+      event.preventDefault(); // 阻止默认行为
+      event.stopPropagation(); // 阻止事件冒泡
+      isCutting.value = true;
+      cutNodes(selectedNodes.value, selectedEdges.value, nodes.value, edges.value, saveproject, removeNodes, removeEdges, clearSelection )
+      .finally(() => {
+        isCutting.value = false; // 重置标志
+      });
+    } else if (event.key === 'v') {
+      event.preventDefault(); // 阻止默认行为
+      event.stopPropagation(); // 阻止事件冒泡
+      pasteNodes(pid.value, nodes.value, edges.value, saveproject, createEdge, addNodes, addEdges, updateNodeInternals);
+    }
+  } else if (event.key === 'Backspace' && selectedNodes.value.length > 0) {
+    event.preventDefault(); // 阻止默认行为
+    event.stopPropagation(); // 阻止事件冒泡
+    removeNodes(selectedNodes.value.map((n) => n.id));
+    selectedNodes.value = [];
   }
   }
-}
+};
+
+const clearSelection = () => {
+  selectedNodes.value = [];
+  selectedEdges.value = [];
+};
+
 
 
 const resetTransform = () => {
 const resetTransform = () => {
   setViewport({ x: 0, y: 0, zoom: 1 })
   setViewport({ x: 0, y: 0, zoom: 1 })
@@ -292,7 +427,7 @@ const removeNode = () => {
 
 
   vueFlowRef.value.removeNodes(selectedNode.value.id)
   vueFlowRef.value.removeNodes(selectedNode.value.id)
   selectedNode.value = null
   selectedNode.value = null
-  saveproject()
+  debouncedSave();
 }
 }
 
 
 const removeEdge = () => {
 const removeEdge = () => {
@@ -303,7 +438,7 @@ const removeEdge = () => {
 
 
   vueFlowRef.value.removeEdges(selectedEdge.value.id)
   vueFlowRef.value.removeEdges(selectedEdge.value.id)
   selectedEdge.value = null
   selectedEdge.value = null
-  saveproject()
+  debouncedSave();
 }
 }
 
 
 const confirmDelete = () => {
 const confirmDelete = () => {
@@ -316,7 +451,7 @@ const confirmDelete = () => {
       vueFlowRef.value.removeNodes(nodes.value)
       vueFlowRef.value.removeNodes(nodes.value)
       vueFlowRef.value.removeEdges(edges.value)
       vueFlowRef.value.removeEdges(edges.value)
       ElMessage.success("所有节点和连线已删除")
       ElMessage.success("所有节点和连线已删除")
-      saveproject()
+      debouncedSave();
     })
     })
     .catch(() => {
     .catch(() => {
       ElMessage.info("已取消删除")
       ElMessage.info("已取消删除")
@@ -327,7 +462,6 @@ const comdelete = async (pcId) => {
   try {
   try {
     const params = { transCode: "ES0005", pcId }
     const params = { transCode: "ES0005", pcId }
     await request(params)
     await request(params)
-    console.log("组件删除成功")
   } catch (err) {
   } catch (err) {
     ElMessage.error(err.returnMsg || "组件删除失败")
     ElMessage.error(err.returnMsg || "组件删除失败")
   }
   }
@@ -337,7 +471,6 @@ const comlinedelete = async (npcId, pcId) => {
   try {
   try {
     const params = { transCode: "ES0007", npcId, pcId, type: 0 }
     const params = { transCode: "ES0007", npcId, pcId, type: 0 }
     await request(params)
     await request(params)
-    console.log("组件连线删除成功")
   } catch (err) {
   } catch (err) {
     ElMessage.error(err.returnMsg || "连线删除失败")
     ElMessage.error(err.returnMsg || "连线删除失败")
   }
   }
@@ -345,42 +478,145 @@ const comlinedelete = async (npcId, pcId) => {
 
 
 const customOnDrop = async (event) => {
 const customOnDrop = async (event) => {
   await onDrop(event)
   await onDrop(event)
-  saveproject()
+  debouncedSave();
 }
 }
 
 
 const onNodeContextMenu = (e) => {
 const onNodeContextMenu = (e) => {
   console.log("右键点击", e)
   console.log("右键点击", e)
 }
 }
 
 
+const getChainFromEdge = (edge) => {
+  const chainNodes = new Set(); // 用 Set 避免重复
+  const chainEdges = new Set(); // 用 Set 避免重复
+
+  // 添加当前边
+  chainEdges.add(edge);
+
+  // 获取源和目标节点
+  let currentSource = vueFlowRef.value.getNode(edge.source);
+  let currentTarget = vueFlowRef.value.getNode(edge.target);
+
+  chainNodes.add(currentSource);
+  chainNodes.add(currentTarget);
+
+  // 如果源是中间节点,向前扩展(找出源的输入边)
+  if (currentSource.data?.comId === '3') {
+    const inputEdges = edges.value.filter(e => e.target === currentSource.id);
+    inputEdges.forEach(inputEdge => {
+      chainEdges.add(inputEdge);
+      const prevNode = vueFlowRef.value.getNode(inputEdge.source);
+      chainNodes.add(prevNode);
+    });
+  }
+
+  // 如果目标是中间节点,向后扩展(找出目标的输出边)
+  if (currentTarget.data?.comId === '3') {
+    const outputEdges = edges.value.filter(e => e.source === currentTarget.id);
+    outputEdges.forEach(outputEdge => {
+      chainEdges.add(outputEdge);
+      const nextNode = vueFlowRef.value.getNode(outputEdge.target);
+      chainNodes.add(nextNode);
+    });
+  }
+
+  // 如果链条两端不是大节点,继续递归扩展(但假设单中间节点,此处简化)
+  // 如果有多个中间节点,可递归调用 getChainFromEdge 于新边
+
+  return {
+    nodes: Array.from(chainNodes),
+    edges: Array.from(chainEdges)
+  };
+};
+
+// 边点击事件
 const onEdgeClick = (e) => {
 const onEdgeClick = (e) => {
-  
-  // 恢复上一个选中边样式
+  // 恢复上一个选中边样式(原有逻辑)
   if (previousEdge.value) {
   if (previousEdge.value) {
-    previousEdge.value.style = {
-      ...previousEdge.value.style,
-      stroke: previousEdge.value.originalColor,
-      strokeWidth: previousEdge.value.originalWidth
-    }
+    edges.value = edges.value.map(edge => {
+      if (edge.id === previousEdge.value.id) {
+        return {
+          ...edge,
+          class: edge.class ? edge.class.replace("selected", "").trim() : "",
+          style: {
+            ...edge.style,
+            stroke: previousEdge.value.originalColor || DEFAULT_LINE_COLOR,
+            strokeWidth: previousEdge.value.originalWidth || DEFAULT_LINE_WIDTH
+          }
+        };
+      }
+      return edge;
+    });
   }
   }
 
 
-  // 设置当前选中边
-  selectedEdge.value = { ...e.edge } // 浅拷贝避免直接修改
-  selectedEdge.value.originalColor = selectedEdge.value.style?.stroke || DEFAULT_LINE_COLOR
-  selectedEdge.value.originalWidth = selectedEdge.value.style?.strokeWidth || DEFAULT_LINE_WIDTH
-
-  const isDefault = e.edge.data?.type === "default"
-  selectedEdge.value.style = {
-    ...selectedEdge.value.style,
-    stroke: isDefault ? "#2267B1" : "rgba(255, 255, 0, 0.3)",
-    strokeWidth: isDefault ? 2 : 6
+  // 设置当前选中边(原有逻辑)
+  selectedEdge.value = { ...e.edge };
+  selectedEdge.value.originalColor = e.edge.style?.stroke || DEFAULT_LINE_COLOR;
+  selectedEdge.value.originalWidth = e.edge.style?.strokeWidth || DEFAULT_LINE_WIDTH;
+
+  // 更新 selectedEdges.value(原有逻辑,支持 Ctrl 多选)
+  if (e.event.ctrlKey) {
+    const index = selectedEdges.value.findIndex((edge) => edge.id === e.edge.id);
+    if (index === -1) {
+      selectedEdges.value.push(e.edge);
+    } else {
+      selectedEdges.value.splice(index, 1); // 取消选择
+    }
+  } else {
+    selectedEdges.value = [e.edge];
+    selectedNodes.value = []; // 清空节点选择
   }
   }
 
 
-  previousEdge.value = selectedEdge.value
-}
+  // 新增:获取完整链条,并自动更新 selectedNodes 和 selectedEdges
+  const chain = getChainFromEdge(e.edge);
+  selectedNodes.value = chain.nodes; // 自动选中相关节点
+  selectedEdges.value = [...selectedEdges.value, ...chain.edges.filter(ce => ce.id !== e.edge.id)]; // 添加其他相关边
+
+  ElMessage.success('已自动选中边的相关节点、边和链条');
+
+  // 可选:如果需要立即保存项目
+  // saveproject();
+
+  // 更新边样式(原有逻辑,稍作调整以支持多边选中)
+  selectedEdges.value.forEach(selEdge => {
+    const isDefault = selEdge.data?.type === "default";
+    const newStyle = {
+      ...selEdge.style,
+      stroke: isDefault ? "#2267B1" : "rgba(255, 255, 0, 0.3)",
+      strokeWidth: isDefault ? 2 : 6
+    };
+
+    edges.value = edges.value.map(edge => {
+      if (edge.id === selEdge.id) {
+        return {
+          ...edge,
+          class: edge.class ? `${edge.class} selected` : "selected",
+          style: newStyle
+        };
+      }
+      return edge;
+    });
+  });
+
+  previousEdge.value = { ...selectedEdge.value, style: { ...selectedEdge.value.style } };
+};
 
 
+// 点击节点
 const onNodeClick = (event) => {
 const onNodeClick = (event) => {
-  selectedNode.value = event.node
-}
+
+  if (event.event.ctrlKey) {
+    // 支持多选节点
+    const index = selectedNodes.value.findIndex((n) => n.id === event.node.id);
+    if (index === -1) {
+      selectedNodes.value.push(event.node);
+    } else {
+      selectedNodes.value.splice(index, 1); // 取消选择
+    }
+  } else {
+    // 单选节点,清空边选择
+    selectedNodes.value = [event.node];
+    selectedEdges.value = [];
+  }
+};
 
 
 const onNodeDoubleClick = (event) => {
 const onNodeDoubleClick = (event) => {
   showPanel.value = true
   showPanel.value = true
@@ -477,13 +713,13 @@ onConnect(async (connection) => {
     await handleIndirectConnection(connection, sourceNode, targetNode)
     await handleIndirectConnection(connection, sourceNode, targetNode)
   }
   }
 
 
-  saveproject()
+  debouncedSave();
 })
 })
 
 
 const handleDirectConnection = async (connection, sourceNode, targetNode) => {
 const handleDirectConnection = async (connection, sourceNode, targetNode) => {
   const edgeId = `${lineType.value}-${connection.source}-${connection.sourceHandle}-${connection.target}-${connection.targetHandle}`
   const edgeId = `${lineType.value}-${connection.source}-${connection.sourceHandle}-${connection.target}-${connection.targetHandle}`
   connection.id = edgeId
   connection.id = edgeId
-  connection.type = "step"
+  connection.type = "smoothstep"
   connection.color = lineColor.value
   connection.color = lineColor.value
   connection.style = { strokeWidth: lineWidth.value, stroke: lineColor.value }
   connection.style = { strokeWidth: lineWidth.value, stroke: lineColor.value }
 
 
@@ -512,7 +748,7 @@ const handleDirectConnection = async (connection, sourceNode, targetNode) => {
 }
 }
 
 
 const handleIndirectConnection = async (connection, sourceNode, targetNode) => {
 const handleIndirectConnection = async (connection, sourceNode, targetNode) => {
-  const pointNodeId = `point-${connection.source}-${connection.target}` // 修复多余 }
+  const pointNodeId = `point-${connection.source}-${connection.target}`
   const sourceCenterX = sourceNode.position.x + 60 / 2
   const sourceCenterX = sourceNode.position.x + 60 / 2
   const sourceCenterY = sourceNode.position.y + 58 / 2
   const sourceCenterY = sourceNode.position.y + 58 / 2
   const targetCenterX = targetNode.position.x + 60 / 2
   const targetCenterX = targetNode.position.x + 60 / 2
@@ -569,7 +805,7 @@ const handleIndirectConnection = async (connection, sourceNode, targetNode) => {
       sourceHandle: connection.sourceHandle,
       sourceHandle: connection.sourceHandle,
       target: pointNodeId,
       target: pointNodeId,
       targetHandle: `source-${sourceDirection}`,
       targetHandle: `source-${sourceDirection}`,
-      type: "step",
+      type: "smoothstep",
       color: lineColor.value,
       color: lineColor.value,
       style: { strokeWidth: lineWidth.value, stroke: lineColor.value }
       style: { strokeWidth: lineWidth.value, stroke: lineColor.value }
     }
     }
@@ -582,7 +818,7 @@ const handleIndirectConnection = async (connection, sourceNode, targetNode) => {
       sourceHandle: `source-${targetDirection}`,
       sourceHandle: `source-${targetDirection}`,
       target: connection.target,
       target: connection.target,
       targetHandle: connection.targetHandle,
       targetHandle: connection.targetHandle,
-      type: "step",
+      type: "smoothstep",
       color: lineColor.value,
       color: lineColor.value,
       style: { strokeWidth: lineWidth.value, stroke: lineColor.value }
       style: { strokeWidth: lineWidth.value, stroke: lineColor.value }
     }
     }
@@ -679,8 +915,6 @@ const flowInit = async () => {
     prevNodes = [...updatedNodes]
     prevNodes = [...updatedNodes]
     prevEdges = [...flow.edges]
     prevEdges = [...flow.edges]
     isInitializing.value = true
     isInitializing.value = true
-
-    console.log('加载项目成功')
   } catch (error) {
   } catch (error) {
     console.error('加载项目失败:', error)
     console.error('加载项目失败:', error)
     ElMessage.error('加载项目失败,请检查项目数据')
     ElMessage.error('加载项目失败,请检查项目数据')
@@ -689,15 +923,24 @@ const flowInit = async () => {
 
 
 const cleanEdgeSelect = () => {
 const cleanEdgeSelect = () => {
   if (selectedEdge.value) {
   if (selectedEdge.value) {
-    selectedEdge.value.style = {
-      ...selectedEdge.value.style,
-      stroke: selectedEdge.value.originalColor,
-      strokeWidth: selectedEdge.value.originalWidth
-    }
-    selectedEdge.value = null
-    previousEdge.value = null
+    edges.value = edges.value.map(edge => {
+      if (edge.id === selectedEdge.value.id) {
+        return {
+          ...edge,
+          class: edge.class ? edge.class.replace("selected", "").trim() : "",
+          style: {
+            ...edge.style,
+            stroke: selectedEdge.value.originalColor || DEFAULT_LINE_COLOR,
+            strokeWidth: selectedEdge.value.originalWidth || DEFAULT_LINE_WIDTH
+          }
+        };
+      }
+      return edge;
+    });
+    selectedEdge.value = null;
+    previousEdge.value = null;
   }
   }
-}
+};
 
 
 const closePanel = () => {
 const closePanel = () => {
   nextTick(() => {
   nextTick(() => {
@@ -713,7 +956,13 @@ defineExpose({
   removeNode,
   removeNode,
   removeEdge,
   removeEdge,
   confirmDelete,
   confirmDelete,
-  asideDataref
+  asideDataref,
+  copyNodes: () => copyNodes(selectedNodes.value, selectedEdges.value, edges.value, nodes.value, pid.value),
+  cutNodes: () => cutNodes(selectedNodes.value, selectedEdges.value, nodes.value, edges.value, saveproject, removeNodes, removeEdges, clearSelection)
+    .finally(() => {
+      isCutting.value = false;
+    }),
+  pasteNodes: () => pasteNodes(pid.value, nodes.value, edges.value, saveproject, createEdge, addNodes, addEdges, updateNodeInternals)
 })
 })
 </script>
 </script>
 
 

+ 357 - 0
src/views/model/vueflow/utils/clipboardUtils.js

@@ -0,0 +1,357 @@
+import { ElMessage } from "element-plus";
+import { useClipboardStore } from "@/store/clipboard";
+import { useProjectStore } from "@/store/project"
+import { request, getImageBase64 } from "@/utils/request"
+import nodePoint from "@/assets/img/node.png"
+import tempic from "@/assets/img/temp.png"
+const clipboardStore = useClipboardStore();
+
+const projectStore = useProjectStore();
+
+const getChainFromEdge = (selectedEdges, allEdges, allNodes) => {
+  if (!Array.isArray(allEdges) || !Array.isArray(allNodes)) {
+    console.warn('getChainFromEdge: allEdges 或 allNodes 无效', { allEdges, allNodes });
+    return { nodes: [], edges: [] };
+  }
+
+  const chainNodes = new Set();
+  const chainEdges = new Set(selectedEdges.map(edge => edge.id));
+
+  selectedEdges.forEach(edge => {
+    const edgeData = allEdges.find(e => e.id === edge.id);
+    if (!edgeData) {
+      console.warn(`边 ${edge.id} 未找到`);
+      return;
+    }
+
+    const sourceNode = allNodes.find(n => n.id === edgeData.source);
+    const targetNode = allNodes.find(n => n.id === edgeData.target);
+    if (sourceNode) chainNodes.add(sourceNode.id);
+    if (targetNode) chainNodes.add(targetNode.id);
+
+    if (sourceNode?.data?.comId === '3') {
+      const inputEdges = allEdges.filter(e => e.target === sourceNode.id);
+      inputEdges.forEach(inputEdge => {
+        chainEdges.add(inputEdge.id);
+        const prevNode = allNodes.find(n => n.id === inputEdge.source);
+        if (prevNode) chainNodes.add(prevNode.id);
+      });
+    }
+
+    if (targetNode?.data?.comId === '3') {
+      const outputEdges = allEdges.filter(e => e.source === targetNode.id);
+      outputEdges.forEach(outputEdge => {
+        chainEdges.add(outputEdge.id);
+        const nextNode = allNodes.find(n => n.id === outputEdge.target);
+        if (nextNode) chainNodes.add(nextNode.id);
+      });
+    }
+  });
+
+  return {
+    nodes: allNodes.filter(n => chainNodes.has(n.id)),
+    edges: allEdges.filter(e => chainEdges.has(e.id))
+  };
+};
+
+export const copyNodes = async (selectedNodes, selectedEdges, edges, nodes, pid) => {
+  if (!Array.isArray(selectedNodes) || !Array.isArray(selectedEdges) || !Array.isArray(edges) || !Array.isArray(nodes)) {
+    ElMessage.warning('复制失败:无效的节点或边数据');
+    console.error('copyNodes: 无效的输入参数', { selectedNodes, selectedEdges, edges, nodes });
+    return false;
+  }
+
+  if (selectedNodes.length === 0 && selectedEdges.length === 0) {
+    ElMessage.warning('未选中任何节点或边');
+    return false;
+  }
+
+  // 处理单节点复制,跳过链条扩展如果没有边
+  let nodesToCopy = [...selectedNodes];
+  let clipboardEdges = [];
+
+  if (selectedEdges.length > 0) {
+    const { nodes: chainNodes, edges: chainEdges } = getChainFromEdge(selectedEdges, edges, nodes);
+    const nodeIds = new Set([...selectedNodes.map(n => n.id), ...chainNodes.map(n => n.id)]);
+    nodesToCopy = [...selectedNodes, ...chainNodes.filter(n => !nodeIds.has(n.id))];
+    
+    chainEdges.forEach(edge => {
+      const edgeData = edges.find(e => e.id === edge.id);
+      if (!edgeData) {
+        console.warn(`边 ${edge.id} 未找到`);
+        return;
+      }
+
+      const sourceNode = nodes.find(n => n.id === edgeData.source);
+      const targetNode = nodes.find(n => n.id === edgeData.target);
+
+      if (!sourceNode) {
+        console.warn(`源节点 ${edgeData.source} 未找到`);
+      } else if (!nodeIds.has(sourceNode.id)) {
+        nodesToCopy.push(sourceNode);
+        nodeIds.add(sourceNode.id);
+      }
+
+      if (!targetNode) {
+        console.warn(`目标节点 ${edgeData.target} 未找到`);
+      } else if (!nodeIds.has(targetNode.id)) {
+        nodesToCopy.push(targetNode);
+        nodeIds.add(targetNode.id);
+      }
+    });
+
+    clipboardEdges = chainEdges.map(edge => {
+      const edgeData = edges.find(e => e.id === edge.id);
+      console.log("edgeData: ", edgeData);
+
+      const cleanedSourceNode = edgeData.sourceNode
+        ? {
+            ...edgeData.sourceNode,
+            data: {
+              ...edgeData.sourceNode.data,
+              image: undefined
+            }
+          }
+        : undefined;
+
+      const cleanedTargetNode = edgeData.targetNode
+        ? {
+            ...edgeData.targetNode,
+            data: {
+              ...edgeData.targetNode.data,
+              image: undefined
+            }
+          }
+        : undefined;
+
+      return edgeData
+        ? {
+            ...edgeData,
+            sourceNode: cleanedSourceNode,
+            targetNode: cleanedTargetNode,
+            data: {
+              ...edgeData.data
+            }
+          }
+        : null;
+    }).filter(edge => edge !== null);
+  }
+
+  if (nodesToCopy.length === 0) {
+    ElMessage.warning('未找到有效节点');
+    return false;
+  }
+
+  const clipboardNodes = nodesToCopy.map(node => ({
+    ...node,
+    data: {
+      ...node.data,
+      image: undefined,
+      pcId: node.data.pcId,
+      label: node.data.label || '默认节点',
+      imageIdentify: node.data.imageIdentify || node.data.comId,
+      uid: node.data.uid || undefined,
+      ptsite: node.data.ptsite || undefined
+    }
+  }));
+
+  const clipboardData = { nodes: clipboardNodes, edges: clipboardEdges, pid };
+
+  try {
+    await navigator.clipboard.writeText(JSON.stringify(clipboardData));
+    clipboardStore.setClipboard(clipboardNodes, clipboardEdges, pid);
+    // console.log('复制到剪贴板:', JSON.stringify(clipboardData, null, 2));
+    ElMessage.success('已复制到剪贴板并存储');
+    return true;
+  } catch (err) {
+    console.error('复制或存储失败:', err.message, err.stack);
+    ElMessage.error('复制失败,请确保浏览器支持剪贴板操作');
+    return false;
+  }
+};
+
+export const cutNodes = async (selectedNodes, selectedEdges, nodes, edges, saveproject, removeNodes, removeEdges,  clearSelection ) => {
+  if (selectedNodes.length === 0) {
+    ElMessage.warning("未选中任何节点");
+    return;
+  }
+
+  // 筛选与选中节点直接相关的边
+  const relatedEdges = edges.filter(edge => 
+    selectedNodes.some(node => node.id === edge.source || node.id === edge.target)
+  );
+
+  // 调用 copyNodes,传递完整节点列表
+  const copySuccess = await copyNodes(selectedNodes, selectedEdges.length > 0 ? selectedEdges : relatedEdges, edges, nodes, projectStore.pid);
+  
+  if (!copySuccess) {
+    ElMessage.error('复制失败,无法执行剪切操作');
+    return;
+  }
+
+  try {
+    // 删除选中节点
+    removeNodes(selectedNodes.map(n => n.id));
+
+    // 删除与选中节点相关的边
+    removeEdges(relatedEdges.map(e => e.id));
+
+    // 清空选中状态
+    if (clearSelection) {
+      clearSelection();
+    }
+    ElMessage.success("已剪切");
+  } catch (err) {
+    console.error('剪切失败:', err.message, err.stack);
+    ElMessage.error('剪切失败,请重试');
+  }
+};
+
+// 粘贴节点和边
+export const pasteNodes = async (
+  pid,
+  nodes,
+  edges,
+  saveproject,
+  createEdge,
+  addNodes,
+  addEdges,
+  updateNodeInternals
+) => {
+  const clipboardData = clipboardStore.getClipboard();
+  console.log("clipboardData", clipboardData);
+  
+  if (!clipboardData || !Array.isArray(clipboardData.nodes) || !Array.isArray(clipboardData.edges)) {
+    ElMessage.warning('剪贴板数据格式无效');
+    console.error('clipboardData:', clipboardData);
+    return;
+  }
+
+  if (!Array.isArray(nodes) || !Array.isArray(edges)) {
+    ElMessage.warning('粘贴失败:流程图数据无效');
+    console.error('pasteNodes: 无效的 nodes 或 edges', { nodes, edges });
+    return;
+  }
+
+  // 提取接口所需参数
+  const extractedData = {
+    pid: clipboardData.pid,
+    npid: projectStore.pid,
+    pcids: clipboardData.nodes.map(node => ({ pcid: node.data.pcId })),
+    pccids: clipboardData.edges
+      .filter(edge => edge.data && edge.data.pcid)
+      .map(edge => ({ pccid: edge.data.pcid }))
+  };
+
+  // 调用后端接口获取 ID 映射
+  let cpjson;
+  try {
+    const response = await request({
+      transCode: 'ES0029',
+      ...extractedData
+    });
+    cpjson = JSON.parse(response.cpjson);
+    console.log('后端返回的 cpjson:', cpjson);
+  } catch (err) {
+    console.error('获取 ID 映射失败:', err);
+    ElMessage.error('获取 ID 映射失败,请重试');
+    return;
+  }
+
+  // 解析 cpjson 生成节点和边的 ID 映射
+  const nodeIdMap = new Map();
+  const edgeIdMap = new Map();
+  cpjson.forEach(item => {
+    if (item.type === 1) {
+      nodeIdMap.set(item.oldId, item.newId); // 节点: oldId 是 pcId,newId 是新 pcId
+    } else if (item.type === 2) {
+      edgeIdMap.set(item.oldId, item.newId); // 边: oldId 是 pcid,newId 是新 pcid
+    }
+  });
+
+  // 生成新节点并请求图片
+  const newNodes = await Promise.all(
+    clipboardData.nodes.map(async node => {
+      const newPcId = nodeIdMap.get(node.data.pcId);
+      if (!newPcId) {
+        console.warn(`节点 pcId ${node.data.pcId} 未找到新 ID`);
+        return null;
+      }
+      const newNodeId = `${node.data.comId}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
+
+      // 处理图片
+      let image = tempic;
+      if (node.data?.imageIdentify) {
+        if (node.data.imageIdentify === 'point-only') {
+          image = nodePoint;
+        } else {
+          try {
+            const imageData = await getImageBase64(node.data.imageIdentify);
+            image = imageData || tempic;
+          } catch (err) {
+            console.error(`获取图片 ${node.data.imageIdentify} 失败:`, err.message);
+            image = tempic;
+          }
+        }
+      }
+
+      return {
+        ...node,
+        id: newNodeId,
+        position: {
+          x: node.position.x + 20,
+          y: node.position.y + 20
+        },
+        data: {
+          ...node.data,
+          pcId: newPcId,
+          image
+        }
+      };
+    })
+  ).then(nodes => nodes.filter(node => node !== null));
+
+  // 更新边的 source、target 和 pcid
+  const newEdges = clipboardData.edges.map(edge => {
+    const newPcid = edgeIdMap.get(edge.data.pcid);
+    const newSource = newNodes.find(n => n.data.pcId === nodeIdMap.get(clipboardData.nodes.find(node => node.id === edge.source)?.data.pcId))?.id;
+    const newTarget = newNodes.find(n => n.data.pcId === nodeIdMap.get(clipboardData.nodes.find(node => node.id === edge.target)?.data.pcId))?.id;
+
+    if (!newSource || !newTarget || !newPcid) {
+      console.warn(`边 ${edge.id} 的源、目标或 pcid 未找到`, { newSource, newTarget, newPcid });
+      return null;
+    }
+
+    const newEdgeId = `default-${newSource}-${edge.sourceHandle}-${newTarget}`;
+    return {
+      ...edge,
+      id: newEdgeId,
+      source: newSource,
+      target: newTarget,
+      data: {
+        ...edge.data,
+        pcid: newPcid,
+        frompcId: newNodes.find(n => n.id === newSource)?.data.pcId,
+        topcId: newNodes.find(n => n.id === newTarget)?.data.pcId
+      }
+    };
+  }).filter(edge => edge !== null);
+
+  try {
+    // 添加节点
+    addNodes(newNodes);
+
+    // 添加边
+    addEdges(newEdges);
+
+    // 更新节点内部状态
+    updateNodeInternals(newNodes.map(n => n.id));
+
+    // 保存项目
+    await saveproject();
+    ElMessage.success('粘贴成功');
+  } catch (err) {
+    console.error('粘贴失败:', err);
+    ElMessage.error('粘贴失败,请重试');
+  }
+};