From c9a6256465d05e5d7fe51808b7dc4406346d8a4b Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Thu, 30 Jul 2026 10:27:27 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20git=20=E5=BF=BD=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 4 ++-- .gitignore | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.env.development b/.env.development index ab0a6cc..51d4ce3 100644 --- a/.env.development +++ b/.env.development @@ -7,8 +7,8 @@ VITE_APP_ENV = 'development' # 招商车研物联网平台/开发环境 VITE_APP_BASE_API = '/dev-api' -VITE_API_URL = http://localhost:13080 -VITE_WS_URL = ws://localhost:13080/ws +VITE_API_URL = http://192.168.0.222:13080 +VITE_WS_URL = ws://192.168.0.222:13080/ws VITE_PORT = 80 VITE_INSPECTION_TYPE = "inspection" VITE_MINIO_URL = "http://192.168.28.10:9000/cmvr-iot/" diff --git a/.gitignore b/.gitignore index 78a752d..dc89b16 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ selenium-debug.log package-lock.json yarn.lock + +.history From 758d5f55e600f26ae84ff1bc306f67930681b7d8 Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Mon, 3 Aug 2026 13:38:57 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/test/log/index.vue | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/views/test/log/index.vue b/src/views/test/log/index.vue index 39de363..d2a4255 100644 --- a/src/views/test/log/index.vue +++ b/src/views/test/log/index.vue @@ -170,9 +170,6 @@ border direction="vertical" > - {{ - item.logLevel - }} {{ item.nodeType }} @@ -180,7 +177,10 @@ item.message }} - {{ JSON.parse(item.params) }} + {{ JSON.parse(item.paramsIn) }} + + + {{ JSON.parse(item.paramsOut) }} @@ -297,6 +297,7 @@ function getInstList() { function cancel() { open.value = false; reset(); + console.log(open.value) } // 表单重置 @@ -404,6 +405,8 @@ function getLogList() { } function handleOpenLogDialog(row) { + console.log(row) + console.log('open', open.value) logQueryParams.value.instId = row.id; open.value = true; } From e501d8095340adebe8f177ded99bdc4a6f2d6f93 Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Mon, 3 Aug 2026 14:43:09 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=E6=9C=BA=E6=A2=B0=E8=87=82?= =?UTF-8?q?=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/websocket.js | 147 +++-- .../register/components/Camera/index.vue | 582 ++++++++++++------ .../components/params/BasicNodeParams.vue | 16 +- 3 files changed, 500 insertions(+), 245 deletions(-) diff --git a/src/plugins/websocket.js b/src/plugins/websocket.js index 74de402..f3ca635 100644 --- a/src/plugins/websocket.js +++ b/src/plugins/websocket.js @@ -1,4 +1,8 @@ -import { throttle } from 'lodash'; +const MEDIA_HEADER_SIZE = 26; +const MEDIA_MAGIC = [0x43, 0x4d, 0x56, 0x46]; // CMVF +const MEDIA_MIME_TYPES = { + 1: 'video/mp4; codecs="avc1.42C01E"' +}; class WebSocketManager { constructor(url, userId) { @@ -10,7 +14,8 @@ class WebSocketManager { this.heartbeatTimer = null; this.isManualClose = false; this.messageQueue = []; - this.maxQueueSize = 1; + this.processingMessages = false; + this.maxQueueSize = 100; this.status = false; } @@ -28,21 +33,28 @@ class WebSocketManager { this.status = true; this.startHeartbeat(); this.processMessageQueue(); + this.dispatch('open'); }; - this.ws.onmessage = (event) => { - if (this.messageQueue.length < this.maxQueueSize) { - this.messageQueue.push(event); - this.processMessageQueue(); - } else { - console.warn('消息队列已满,丢弃消息'); + this.ws.onmessage = async (event) => { + const data = event.data instanceof Blob ? await event.data.arrayBuffer() : event.data; + if (data instanceof ArrayBuffer && this.isMediaPacket(data)) { + this.dispatchMediaPacket(data); + return; } + if (this.messageQueue.length >= this.maxQueueSize) { + console.warn('WebSocket控制消息队列已满,丢弃最旧消息'); + this.messageQueue.shift(); + } + this.messageQueue.push(data); + this.processMessageQueue(); }; this.ws.onclose = () => { console.log('WebSocket 断开'); this.status = false; this.stopHeartbeat(); + this.dispatch('close'); if (!this.isManualClose) { this.reconnect(); } @@ -53,65 +65,80 @@ class WebSocketManager { }; } - async processMessageQueue() { - if (!this.ws || this.messageQueue.length === 0) return; + isMediaPacket(buffer) { + if (buffer.byteLength < MEDIA_HEADER_SIZE) return false; + const bytes = new Uint8Array(buffer, 0, MEDIA_MAGIC.length); + return MEDIA_MAGIC.every((value, index) => bytes[index] === value); + } - const event = this.messageQueue.shift(); + dispatchMediaPacket(buffer) { try { - // 处理消息数据 - console.log('收到消息数据类型:', event.data.constructor.name); - let compressedData; - - // 处理 Blob 或 ArrayBuffer - if (event.data instanceof Blob) { - console.log('处理 Blob 数据,大小:', event.data.size); - compressedData = new Uint8Array(await event.data.arrayBuffer()); - } else if (event.data instanceof ArrayBuffer) { - console.log('处理 ArrayBuffer 数据,大小:', event.data.byteLength); - compressedData = new Uint8Array(event.data); - } else { - throw new Error('不支持的数据类型: ' + event.data.constructor.name); + const view = new DataView(buffer); + const version = view.getUint8(4); + const mediaType = view.getUint8(5); + const timestamp = view.getUint32(8) * 0x100000000 + view.getUint32(12); + const sequence = view.getUint32(16); + const channelLength = view.getUint16(20); + const payloadLength = view.getUint32(22); + const payloadOffset = MEDIA_HEADER_SIZE + channelLength; + if (version !== 1 || payloadOffset + payloadLength !== buffer.byteLength) { + throw new Error('媒体数据包头无效'); } - - // 使用 DecompressionStream 解压 GZIP 数据 - const decompressionStream = new DecompressionStream('gzip'); - const writer = decompressionStream.writable.getWriter(); - writer.write(compressedData); - writer.close(); - - const decompressedStream = decompressionStream.readable; - const reader = decompressedStream.getReader(); - let decompressedData = new Uint8Array(0); - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - const newData = new Uint8Array(decompressedData.length + value.length); - newData.set(decompressedData); - newData.set(value, decompressedData.length); - decompressedData = newData; + const channel = new TextDecoder().decode(new Uint8Array(buffer, MEDIA_HEADER_SIZE, channelLength)); + const mimeType = MEDIA_MIME_TYPES[mediaType]; + if (!mimeType) { + throw new Error(`不支持的媒体类型: ${mediaType}`); } + this.dispatch(channel, { + media: true, + mimeType, + sequence, + timestamp, + data: buffer.slice(payloadOffset, payloadOffset + payloadLength) + }); + } catch (error) { + console.error('WebSocket媒体包解析失败:', error); + } + } - // 将解压后的数据转换为字符串 - const textDecoder = new TextDecoder(); - const decompressedString = textDecoder.decode(decompressedData); - console.log('解压后的数据:', JSON.parse(decompressedString)); - - // 解析 JSON - const { type, channel, payload,timestamp } = JSON.parse(decompressedString); - console.log('后端到前端延迟:', ((new Date()).getTime() - timestamp)); - - if (channel && this.events[channel]) { - this.events[channel].forEach(callback => callback(payload)); - } else if (type && this.events[type]) { - this.events[type].forEach(callback => callback(payload)); + async processMessageQueue() { + if (this.processingMessages || !this.ws) return; + this.processingMessages = true; + try { + while (this.messageQueue.length > 0) { + const data = this.messageQueue.shift(); + let message; + if (typeof data === 'string') { + message = JSON.parse(data); + } else if (data instanceof ArrayBuffer) { + const decompressedStream = new Blob([data]) + .stream() + .pipeThrough(new DecompressionStream('gzip')); + const decompressedData = await new Response(decompressedStream).arrayBuffer(); + message = JSON.parse(new TextDecoder().decode(decompressedData)); + } else { + throw new Error(`不支持的WebSocket数据类型: ${typeof data}`); + } + if (message.channel) { + this.dispatch(message.channel, message.payload); + } else if (message.type) { + this.dispatch(message.type, message.payload ?? message.content ?? message); + } } - setTimeout(() => this.processMessageQueue(), 0); } catch (err) { - console.error('WebSocket 消息解析失败:', err); - console.error('错误堆栈:', err.stack); - console.error('原始数据类型:', event.data.constructor.name); - console.error('原始数据大小:', event.data instanceof Blob ? event.data.size : event.data.byteLength || '未知'); + console.error('WebSocket消息解析失败:', err); + } finally { + this.processingMessages = false; + if (this.messageQueue.length > 0) { + queueMicrotask(() => this.processMessageQueue()); + } + } + } + + dispatch(eventName, payload) { + const callbacks = this.events[eventName]; + if (callbacks) { + callbacks.slice().forEach(callback => callback(payload)); } } diff --git a/src/views/device/register/components/Camera/index.vue b/src/views/device/register/components/Camera/index.vue index 466ccb9..a7becbe 100644 --- a/src/views/device/register/components/Camera/index.vue +++ b/src/views/device/register/components/Camera/index.vue @@ -14,30 +14,20 @@ - - + + - - + + {{ statusText(streamStatus.getDepthImageStream) }} - + - - + + {{ statusText(streamStatus.getRGBImageStream) }} - - - - - - - - - - @@ -47,18 +37,17 @@ @@ -276,34 +492,19 @@ onUnmounted(() => { flex-direction: column; flex-grow: 1; overflow: hidden; - .single-stream-container { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - flex-grow: 1; - .stream-wrapper { - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - overflow: hidden; - position: relative; - video { - width: auto; - height: 100%; - object-fit: contain; - transition: opacity 0.1s ease-in-out; /* 平滑过渡 */ - } - } - } .stream-container { display: flex; flex-direction: column; flex-grow: 1; + min-height: 0; + &.dual { + .stream-item { + min-height: 50%; + } + } .stream-item { flex: 1; + min-height: 0; display: flex; flex-direction: column; justify-content: center; @@ -317,14 +518,27 @@ onUnmounted(() => { overflow: hidden; position: relative; video { - width: auto; + width: 100%; height: 100%; object-fit: contain; - transition: opacity 0.1s ease-in-out; /* 平滑过渡 */ + background: #000; + } + .stream-state { + position: absolute; + top: 12px; + right: 12px; + padding: 4px 9px; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 4px; + background: rgba(0, 0, 0, 0.58); + color: rgba(255, 255, 255, 0.9); + font-size: 12px; + line-height: 18px; + pointer-events: none; } } } } } } - \ No newline at end of file + diff --git a/src/views/flow/components/params/BasicNodeParams.vue b/src/views/flow/components/params/BasicNodeParams.vue index ee8eef7..b79b875 100644 --- a/src/views/flow/components/params/BasicNodeParams.vue +++ b/src/views/flow/components/params/BasicNodeParams.vue @@ -219,7 +219,21 @@ const testRule = (property) => { } else { return Promise.reject(new Error('加速度必须大于速度')) } - }, trigger: 'blur' } + }, trigger: 'change' } + ] + + } + + if (property?.name === 'velocity') { + return [ + { required: true, message: '请输入参数值', trigger: 'blur' }, + { validator: () => { + if (formData.nodeParams.find(param => param.name === 'acceleration')?.input > formData.nodeParams.find(param => param.name === 'velocity')?.input) { + return Promise.resolve() + } else { + return Promise.reject(new Error('加速度必须大于速度')) + } + }, trigger: 'change' } ] } From 1a26063ce26c3971f57cb6769de5baa1d34f1a66 Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Mon, 3 Aug 2026 16:53:30 +0800 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20agv=E8=8E=B7=E5=8F=96=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/device/flow.js | 8 ++++++ .../components/params/BasicNodeParams.vue | 28 +++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/api/device/flow.js b/src/api/device/flow.js index f309674..5985597 100644 --- a/src/api/device/flow.js +++ b/src/api/device/flow.js @@ -68,4 +68,12 @@ export function getArmStatus(params) { method: 'get', params: params }) +} + +export function getAgvStatus(params) { + return request({ + url: '/api/agv/getRuntimeState', + method: 'get', + params: params + }) } \ No newline at end of file diff --git a/src/views/flow/components/params/BasicNodeParams.vue b/src/views/flow/components/params/BasicNodeParams.vue index b79b875..cbdc827 100644 --- a/src/views/flow/components/params/BasicNodeParams.vue +++ b/src/views/flow/components/params/BasicNodeParams.vue @@ -106,7 +106,7 @@ import { Plus, Minus } from '@element-plus/icons-vue' import FormItemRecursive from '../FormItemRecursive.vue' import { getInput } from '@/utils/flow' import { useQuote } from './useQuote.js' -import { getArmStatus } from '@/api/device/flow' +import { getArmStatus, getAgvStatus } from '@/api/device/flow' import { de } from 'element-plus/es/locale/index.mjs' const props = defineProps({ @@ -244,7 +244,31 @@ const robotFormAction = ref() const submitRobotForm = () => { if (robotFormAction.value === 'AGV_MOVE_TO_POINT') { - + robotFormRef.value.validate(async (valid) => { + if (valid) { + try { + const response = await getAgvStatus({ deviceId: robotForm.deviceId, terminalId: robotForm.terminalId }) + if (response && response.data) { + dialogVisible.value = false + const { x, y, theta } = response.data.pose + const testMap = { + x: x, + y: y, + theta: theta, + deviceId: robotForm.deviceId + } + // 将获取到的位置信息设置到 formData 中 + formData.nodeParams.forEach(param => { + if (testMap.hasOwnProperty(param.name)) { + param.input = testMap[param.name] + } + }) + } + } catch (error) { + console.error('获取位置信息失败:', error) + } + } + }) } else if (robotFormAction.value === 'ARM_MOVE_TO_POINT') { robotFormRef.value.validate(async (valid) => { if (valid) { From 05f4f007a7b8258c9ffd4405317091b14c149f3c Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Tue, 4 Aug 2026 09:05:39 +0800 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=E9=80=9A=E7=94=A8=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E6=8C=87=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/flow/components/ParamsDrawer.vue | 2 + .../components/params/Device_universal.vue | 463 ++++++++++++++++++ src/views/flow/config.js | 17 + .../flow/nodes/function/device_universal.vue | 135 +++++ src/views/flow/vue-flow/FlowNode.vue | 2 + 5 files changed, 619 insertions(+) create mode 100644 src/views/flow/components/params/Device_universal.vue create mode 100644 src/views/flow/nodes/function/device_universal.vue diff --git a/src/views/flow/components/ParamsDrawer.vue b/src/views/flow/components/ParamsDrawer.vue index 8bf7661..af78070 100644 --- a/src/views/flow/components/ParamsDrawer.vue +++ b/src/views/flow/components/ParamsDrawer.vue @@ -31,6 +31,7 @@ import CodeNodeParams from './params/CodeNodeParams.vue' import HttpNodeParams from './params/HttpNodeParams.vue' import SdAgentNodeParams from './params/SdAgentNodeParams.vue' import RecognizeNodeParams from './params/RecognizeNodeParams.vue' +import DeviceUniversal from './params/device_universal.vue' const props = defineProps({ drawer: Boolean, @@ -49,6 +50,7 @@ const currentComponent = computed(() => { if (type === 'http') return HttpNodeParams if (type === 'sdAgent') return SdAgentNodeParams if (type === 'recognize') return RecognizeNodeParams + if (type === 'device_universal') return DeviceUniversal return null }) diff --git a/src/views/flow/components/params/Device_universal.vue b/src/views/flow/components/params/Device_universal.vue new file mode 100644 index 0000000..165b95c --- /dev/null +++ b/src/views/flow/components/params/Device_universal.vue @@ -0,0 +1,463 @@ + + + + + + 输入参数 + + + + + + + + + + + + + + + + + + + + + + + + + + + visibleChange(visible, index, property.quote) + " + @change="(value) => cascaderChange(value, index, formData, 'nodeParams')" + /> + + + + + + + + + + + + + + + + 输出参数 + + + + + + addFormItem(e, 'outputParams')" + class="addFormItem" + > + + + + + + deleteTopLevelItem(path, 'outputParams')" + /> + + + + + + + + + + \ No newline at end of file diff --git a/src/views/flow/config.js b/src/views/flow/config.js index 02cb59b..a82eafe 100644 --- a/src/views/flow/config.js +++ b/src/views/flow/config.js @@ -323,6 +323,23 @@ function handler(params) { ], outputType: 'json' }, + { + icon: cameraSvg, + name: "通用指令", + type: "device_universal", + desc: "执行设备通用指令", + action: 'DEVICE_EXECUTE_JSON_COMMAND', + nodeType: 'EDGE', + nodeParams: [ + { name: "terminalId", type: "input", input: "", disabled: true }, + { name: "deviceId", type: "input", input: "", required: false, disabled: true }, + { name: "requestJson", type: "input", input: `{ + "a":123 +}`, required: false, disabled: true }, + ], + outputParams: [], + outputType: 'json' + }, { icon: videoSvg, name: "机械臂", diff --git a/src/views/flow/nodes/function/device_universal.vue b/src/views/flow/nodes/function/device_universal.vue new file mode 100644 index 0000000..3b06103 --- /dev/null +++ b/src/views/flow/nodes/function/device_universal.vue @@ -0,0 +1,135 @@ + + + + + + + + + + + + + {{ errorInfoData }} + + + + + + + + + diff --git a/src/views/flow/vue-flow/FlowNode.vue b/src/views/flow/vue-flow/FlowNode.vue index 93f36d1..43e4af1 100644 --- a/src/views/flow/vue-flow/FlowNode.vue +++ b/src/views/flow/vue-flow/FlowNode.vue @@ -74,6 +74,7 @@ import CodeNode from "../nodes/function/codeNode.vue"; import CurrentLoopNode from "../nodes/function/currentLoop.vue"; import SdAgentNode from "../nodes/function/sdAgent.vue"; import RecognizeNode from "../nodes/function/recognize.vue"; +import DeviceUniversal from "../nodes/function/device_universal.vue"; const props = defineProps({ id: String, @@ -98,6 +99,7 @@ const componentMap = { currentLoop: CurrentLoopNode, sdAgent: SdAgentNode, recognize: RecognizeNode, + device_universal: DeviceUniversal, }; const flowStore = useFlowStore(); From a3b3f7a1c358dbf3cb89942905de527eef6fe650 Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Tue, 4 Aug 2026 10:08:57 +0800 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=A4=8D?= =?UTF-8?q?=E5=88=B6=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/flow/index.vue | 85 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 78 insertions(+), 7 deletions(-) diff --git a/src/views/flow/index.vue b/src/views/flow/index.vue index 1aa9636..c1554e6 100644 --- a/src/views/flow/index.vue +++ b/src/views/flow/index.vue @@ -193,7 +193,8 @@ v-if="property.componentType === 'number'" v-model="property.input" :min="0" - :controls="false" + :max="property.max || Infinity" + :step="property.step || 1" :step-strictly="true" placeholder="请输入" clearable @@ -880,6 +881,7 @@ const nodeAction = ref(""); const singleNodeExecution = (e) => { const { name, nodeType, nodeParams, action } = e.detail; + console.log('nodeParams', nodeParams) nodeName.value = name; visible.value = true; nodeAction.value = action; @@ -932,11 +934,80 @@ const clearRun = () => { totalRunningTime.value = 0; } -const handleDeleteKey = (event) => { - const activeElement = document.activeElement; - if (["INPUT", "TEXTAREA"].includes(activeElement?.tagName) || activeElement?.isContentEditable) return; +const NODE_PASTE_OFFSET = 40; +const NON_COPYABLE_NODE_TYPES = new Set(["start", "end", "selectArea"]); +let copiedNodes = []; +let pasteCount = 0; - if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "z") { +const isTextEditing = (event) => { + const element = event.target || document.activeElement; + return ["INPUT", "TEXTAREA", "SELECT"].includes(element?.tagName) + || element?.isContentEditable; +}; + +const copySelectedNodes = (event) => { + const nodes = lf.getSelectElements().nodes.filter( + (node) => !NON_COPYABLE_NODE_TYPES.has(node.type), + ); + if (!nodes.length) return; + + event.preventDefault(); + copiedNodes = JSON.parse(JSON.stringify(nodes)); + pasteCount = 0; +}; + +const pasteCopiedNodes = (event) => { + if (!copiedNodes.length || flowStore.disableForm) return; + + event.preventDefault(); + pasteCount += 1; + const offset = NODE_PASTE_OFFSET * pasteCount; + const pastedNodes = copiedNodes.map((sourceNode) => { + const properties = JSON.parse(JSON.stringify(sourceNode.properties || {})); + const parentId = properties.parentId; + delete properties.parentId; + + const newNode = lf.addNode({ + type: sourceNode.type, + x: Number(sourceNode.x || 0) + offset, + y: Number(sourceNode.y || 0) + offset, + properties, + }); + + if (sourceNode.type === "branch" && Array.isArray(properties.anchor)) { + const oldElseId = `${sourceNode.id}_else`; + const newElseId = `${newNode.id}_else`; + const anchors = properties.anchor.map((anchor) => ( + anchor.id === oldElseId ? { ...anchor, id: newElseId } : anchor + )); + lf.setProperties(newNode.id, { ...properties, anchor: anchors }); + } + + if (parentId && lf.getNodeModelById(parentId)) { + lf.addToGroup(parentId, newNode.id, { x: newNode.x, y: newNode.y }); + } + return newNode; + }); + + lf.clearSelectElements(); + if (pastedNodes.length === 1) lf.selectElementById(pastedNodes[0].id); +}; + +const handleKeyboardShortcut = (event) => { + if (isTextEditing(event)) return; + + const isModifierKey = event.ctrlKey || event.metaKey; + const key = event.key.toLowerCase(); + if (isModifierKey && key === "c") { + if (!flowStore.disableForm) copySelectedNodes(event); + return; + } + if (isModifierKey && key === "v") { + pasteCopiedNodes(event); + return; + } + + if (isModifierKey && key === "z") { if (flowStore.disableForm) return; event.preventDefault(); if (event.shiftKey) lf.redo(); @@ -972,7 +1043,7 @@ onMounted(() => { initFlow(); document.addEventListener("singleNodeExecution", singleNodeExecution); registerBeforeUnload(); - window.addEventListener("keydown", handleDeleteKey); + window.addEventListener("keydown", handleKeyboardShortcut); onFlowEvent("openParamsDrawer", (node) => { showParamsDrawer.value = true; @@ -1024,7 +1095,7 @@ onUnmounted(() => { if (window.lf === lf) delete window.lf; window.removeEventListener("beforeunload", handleBeforeUnload); - window.removeEventListener("keydown", handleDeleteKey); + window.removeEventListener("keydown", handleKeyboardShortcut); document.removeEventListener("singleNodeExecution", singleNodeExecution); });
{{ JSON.parse(item.params) }}
{{ JSON.parse(item.paramsIn) }}
{{ JSON.parse(item.paramsOut) }}