|
|
@@ -1,6 +1,8 @@
|
|
|
<template>
|
|
|
<splitpanes class="default-theme">
|
|
|
<pane min-size="50" size="100" max-size="100">
|
|
|
+ <!-- :nodes-draggable="isSelectMode"
|
|
|
+ :nodes-connectable="isSelectMode ? false : true" -->
|
|
|
<VueFlow
|
|
|
ref="vueFlowRef"
|
|
|
v-model:nodes="nodes"
|
|
|
@@ -10,8 +12,8 @@
|
|
|
:default-viewport="{ zoom: 1.5 }"
|
|
|
:min-zoom="0.2"
|
|
|
:max-zoom="2.5"
|
|
|
- :nodes-draggable="isSelectMode"
|
|
|
- :nodes-connectable="isSelectMode ? false : true"
|
|
|
+ :multi-selection-key-code="'Control'"
|
|
|
+ :selection-key-code="'Shift'"
|
|
|
@drop="customOnDrop"
|
|
|
@node-contextmenu="onNodeContextMenu"
|
|
|
@dragover="onDragOver"
|
|
|
@@ -21,6 +23,8 @@
|
|
|
@node-click="onNodeClick"
|
|
|
@edge-double-click="onEdgeDoubleClick"
|
|
|
@node-drag="onNodeDrag"
|
|
|
+ @nodes-selection-change="onNodesSelectionChange"
|
|
|
+ @edges-selection-change="onEdgesSelectionChange"
|
|
|
>
|
|
|
<!-- 自定义节点类型为default的节点 -->
|
|
|
<template #node-default="props">
|
|
|
@@ -60,7 +64,7 @@ import {
|
|
|
ElMessageBox
|
|
|
} from "element-plus"
|
|
|
import { DeleteFilled } from "@element-plus/icons-vue"
|
|
|
-
|
|
|
+import { onBeforeRouteLeave } from "vue-router"
|
|
|
import { request, uploadFile, getImageBase64 } from "@/utils/request"
|
|
|
import { Background } from "@vue-flow/background"
|
|
|
import "./main.css" //重置样式
|
|
|
@@ -68,12 +72,13 @@ import defaultnode from "./defaultnode.vue"
|
|
|
import PointOnlyNode from "./pointonlynode.vue"
|
|
|
import useDragAndDrop from "./useDnD"
|
|
|
import { useProjectStore } from "@/store/project"
|
|
|
+import { useClipboardStore } from '@/store/clipboard'
|
|
|
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/dist/splitpanes.css"
|
|
|
import asideData from "./aside/asideData.vue"
|
|
|
-
|
|
|
+import { copyNodes, cutNodes, pasteNodes } from "./utils/clipboardUtils";
|
|
|
import node from "@/assets/img/node.png"
|
|
|
import nodePoint from "@/assets/img/node.png"
|
|
|
import tempic from "@/assets/img/temp.png"
|
|
|
@@ -82,11 +87,14 @@ import { debounce } from 'lodash-es'
|
|
|
|
|
|
// 常量定义
|
|
|
const GRID_SIZE = 10
|
|
|
-const DEBOUNCE_DELAY = 500
|
|
|
+const DEBOUNCE_DELAY = 1000
|
|
|
const DEFAULT_LINE_COLOR = "#2267B1"
|
|
|
const DEFAULT_LINE_WIDTH = 1
|
|
|
|
|
|
-// 获取路由实例(如果未来需要)
|
|
|
+// 定义一个新的 ref 用于存储边的选择状态
|
|
|
+const edgeSelectionState = ref({}); // 格式: { [edgeId]: { class: string, style: object } }
|
|
|
+
|
|
|
+// 获取路由实例
|
|
|
const router = useRouter()
|
|
|
|
|
|
const {
|
|
|
@@ -103,9 +111,13 @@ const {
|
|
|
updateNodeInternals,
|
|
|
onNodeDrag,
|
|
|
snapToGrid,
|
|
|
- snapGrid
|
|
|
+ snapGrid,
|
|
|
+ setInteractive,
|
|
|
+ removeNodes,
|
|
|
+ removeEdges,
|
|
|
+ selectedNodes: vueFlowSelectedNodes,
|
|
|
+ selectedEdges: vueFlowSelectedEdges,
|
|
|
} = useVueFlow()
|
|
|
-
|
|
|
const { onDragOver, onDrop, onDragLeave, isDragOver } = useDragAndDrop()
|
|
|
|
|
|
const dark = ref(false)
|
|
|
@@ -114,6 +126,8 @@ const iconcolor = ref("#000")
|
|
|
const edges = ref([])
|
|
|
const nodes = ref([])
|
|
|
const mergedObj = ref("")
|
|
|
+const selectedNodes = ref([]); // 选中节点
|
|
|
+const selectedEdges = ref([]); // 选中边
|
|
|
|
|
|
// 选中状态
|
|
|
const selectedNode = ref(null)
|
|
|
@@ -124,8 +138,13 @@ const lineWidth = ref(DEFAULT_LINE_WIDTH)
|
|
|
const midNodeCounter = 0
|
|
|
|
|
|
const projectStore = useProjectStore()
|
|
|
+const clipboardStore = useClipboardStore();
|
|
|
const pid = computed(() => projectStore.pid || "")
|
|
|
|
|
|
+// 删除组件和边的列表
|
|
|
+const pendingDelete = ref({ nodes: [], edges: [] });
|
|
|
+const isCutting = ref(false);
|
|
|
+
|
|
|
const showPanel = ref(false)
|
|
|
const asideDataref = ref()
|
|
|
|
|
|
@@ -139,43 +158,52 @@ const props = defineProps({
|
|
|
// 保存项目
|
|
|
const saveproject = async () => {
|
|
|
try {
|
|
|
- const activeProject = projectStore.getActiveProject()
|
|
|
+ const activeProject = projectStore.getActiveProject();
|
|
|
if (!activeProject) {
|
|
|
- throw new Error('当前没有激活的项目')
|
|
|
+ throw new Error("当前没有激活的项目");
|
|
|
}
|
|
|
|
|
|
const obj = {
|
|
|
- nodes: toObject().nodes.map(node => ({
|
|
|
+ nodes: toObject().nodes.map((node) => ({
|
|
|
...node,
|
|
|
- data: { ...node.data, image: undefined }
|
|
|
+ data: { ...node.data, image: undefined },
|
|
|
})),
|
|
|
edges: toObject().edges,
|
|
|
- }
|
|
|
+ };
|
|
|
|
|
|
- mergedObj.value = JSON.stringify(obj)
|
|
|
+ mergedObj.value = JSON.stringify(obj);
|
|
|
|
|
|
const params = {
|
|
|
- transCode: 'ES0002',
|
|
|
+ transCode: "ES0002",
|
|
|
pid: pid.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, {
|
|
|
...activeProject,
|
|
|
flow: mergedObj.value,
|
|
|
- })
|
|
|
+ });
|
|
|
|
|
|
- ElMessage.success('保存成功')
|
|
|
+ ElMessage.success("保存成功");
|
|
|
} catch (error) {
|
|
|
- console.error('保存失败:', error)
|
|
|
- ElMessage.error('保存失败,请稍后重试')
|
|
|
+ console.error("保存失败:", error);
|
|
|
+ ElMessage.error("保存失败,请稍后重试");
|
|
|
}
|
|
|
-}
|
|
|
+};
|
|
|
|
|
|
const debouncedSave = debounce(saveproject, DEBOUNCE_DELAY)
|
|
|
|
|
|
@@ -184,52 +212,89 @@ let prevEdges = []
|
|
|
let isInitializing = ref(true)
|
|
|
|
|
|
// 合并的 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(
|
|
|
() => props.fixedGrid,
|
|
|
(newValue) => {
|
|
|
@@ -238,23 +303,66 @@ watch(
|
|
|
},
|
|
|
{ immediate: true }
|
|
|
)
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
flowInit()
|
|
|
// 点击画布取消边选中
|
|
|
if (vueFlowRef.value) {
|
|
|
vueFlowRef.value.$el.addEventListener("click", (event) => {
|
|
|
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('beforeunload', handleBeforeUnload) // 绑定页面关闭事件
|
|
|
+})
|
|
|
+
|
|
|
+onBeforeUnmount(() =>{
|
|
|
+ saveproject();
|
|
|
+
|
|
|
})
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
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 }) => {
|
|
|
if (!props.fixedGrid || node.type !== 'point-only') return
|
|
|
@@ -268,10 +376,37 @@ onNodeDrag(({ node }) => {
|
|
|
|
|
|
// 键盘事件:Backspace 删除选中节点
|
|
|
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 = () => {
|
|
|
setViewport({ x: 0, y: 0, zoom: 1 })
|
|
|
@@ -292,7 +427,7 @@ const removeNode = () => {
|
|
|
|
|
|
vueFlowRef.value.removeNodes(selectedNode.value.id)
|
|
|
selectedNode.value = null
|
|
|
- saveproject()
|
|
|
+ debouncedSave();
|
|
|
}
|
|
|
|
|
|
const removeEdge = () => {
|
|
|
@@ -303,7 +438,7 @@ const removeEdge = () => {
|
|
|
|
|
|
vueFlowRef.value.removeEdges(selectedEdge.value.id)
|
|
|
selectedEdge.value = null
|
|
|
- saveproject()
|
|
|
+ debouncedSave();
|
|
|
}
|
|
|
|
|
|
const confirmDelete = () => {
|
|
|
@@ -316,7 +451,7 @@ const confirmDelete = () => {
|
|
|
vueFlowRef.value.removeNodes(nodes.value)
|
|
|
vueFlowRef.value.removeEdges(edges.value)
|
|
|
ElMessage.success("所有节点和连线已删除")
|
|
|
- saveproject()
|
|
|
+ debouncedSave();
|
|
|
})
|
|
|
.catch(() => {
|
|
|
ElMessage.info("已取消删除")
|
|
|
@@ -327,7 +462,6 @@ const comdelete = async (pcId) => {
|
|
|
try {
|
|
|
const params = { transCode: "ES0005", pcId }
|
|
|
await request(params)
|
|
|
- console.log("组件删除成功")
|
|
|
} catch (err) {
|
|
|
ElMessage.error(err.returnMsg || "组件删除失败")
|
|
|
}
|
|
|
@@ -337,7 +471,6 @@ const comlinedelete = async (npcId, pcId) => {
|
|
|
try {
|
|
|
const params = { transCode: "ES0007", npcId, pcId, type: 0 }
|
|
|
await request(params)
|
|
|
- console.log("组件连线删除成功")
|
|
|
} catch (err) {
|
|
|
ElMessage.error(err.returnMsg || "连线删除失败")
|
|
|
}
|
|
|
@@ -345,42 +478,145 @@ const comlinedelete = async (npcId, pcId) => {
|
|
|
|
|
|
const customOnDrop = async (event) => {
|
|
|
await onDrop(event)
|
|
|
- saveproject()
|
|
|
+ debouncedSave();
|
|
|
}
|
|
|
|
|
|
const onNodeContextMenu = (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) => {
|
|
|
-
|
|
|
- // 恢复上一个选中边样式
|
|
|
+ // 恢复上一个选中边样式(原有逻辑)
|
|
|
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) => {
|
|
|
- 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) => {
|
|
|
showPanel.value = true
|
|
|
@@ -477,13 +713,13 @@ onConnect(async (connection) => {
|
|
|
await handleIndirectConnection(connection, sourceNode, targetNode)
|
|
|
}
|
|
|
|
|
|
- saveproject()
|
|
|
+ debouncedSave();
|
|
|
})
|
|
|
|
|
|
const handleDirectConnection = async (connection, sourceNode, targetNode) => {
|
|
|
const edgeId = `${lineType.value}-${connection.source}-${connection.sourceHandle}-${connection.target}-${connection.targetHandle}`
|
|
|
connection.id = edgeId
|
|
|
- connection.type = "step"
|
|
|
+ connection.type = "smoothstep"
|
|
|
connection.color = 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 pointNodeId = `point-${connection.source}-${connection.target}` // 修复多余 }
|
|
|
+ const pointNodeId = `point-${connection.source}-${connection.target}`
|
|
|
const sourceCenterX = sourceNode.position.x + 60 / 2
|
|
|
const sourceCenterY = sourceNode.position.y + 58 / 2
|
|
|
const targetCenterX = targetNode.position.x + 60 / 2
|
|
|
@@ -569,7 +805,7 @@ const handleIndirectConnection = async (connection, sourceNode, targetNode) => {
|
|
|
sourceHandle: connection.sourceHandle,
|
|
|
target: pointNodeId,
|
|
|
targetHandle: `source-${sourceDirection}`,
|
|
|
- type: "step",
|
|
|
+ type: "smoothstep",
|
|
|
color: lineColor.value,
|
|
|
style: { strokeWidth: lineWidth.value, stroke: lineColor.value }
|
|
|
}
|
|
|
@@ -582,7 +818,7 @@ const handleIndirectConnection = async (connection, sourceNode, targetNode) => {
|
|
|
sourceHandle: `source-${targetDirection}`,
|
|
|
target: connection.target,
|
|
|
targetHandle: connection.targetHandle,
|
|
|
- type: "step",
|
|
|
+ type: "smoothstep",
|
|
|
color: lineColor.value,
|
|
|
style: { strokeWidth: lineWidth.value, stroke: lineColor.value }
|
|
|
}
|
|
|
@@ -679,8 +915,6 @@ const flowInit = async () => {
|
|
|
prevNodes = [...updatedNodes]
|
|
|
prevEdges = [...flow.edges]
|
|
|
isInitializing.value = true
|
|
|
-
|
|
|
- console.log('加载项目成功')
|
|
|
} catch (error) {
|
|
|
console.error('加载项目失败:', error)
|
|
|
ElMessage.error('加载项目失败,请检查项目数据')
|
|
|
@@ -689,15 +923,24 @@ const flowInit = async () => {
|
|
|
|
|
|
const cleanEdgeSelect = () => {
|
|
|
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 = () => {
|
|
|
nextTick(() => {
|
|
|
@@ -713,7 +956,13 @@ defineExpose({
|
|
|
removeNode,
|
|
|
removeEdge,
|
|
|
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>
|
|
|
|