From 7b791bc54176007ef9da494635412ba1e431d849 Mon Sep 17 00:00:00 2001 From: lixiaolong <702156524@qq.com> Date: Mon, 3 Aug 2026 09:49:53 +0800 Subject: [PATCH 01/96] =?UTF-8?q?refactor(camera):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=91=84=E5=83=8F=E5=A4=B4=E8=A7=86=E9=A2=91=E6=B5=81=E5=A4=84?= =?UTF-8?q?=E7=90=86=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除原有的双视频元素缓冲机制,改用MediaSource API实现直接流式播放 - 新增RGB和深度图像流的状态管理系统,支持连接、实时、重连、异常状态显示 - 实现直接RGB流播放功能,通过新的getRgbStreamUrl接口获取视频流 - 添加媒体播放器管理机制,支持视频流的创建、销毁和状态维护 - 优化视频播放性能,实现缓冲区管理和实时播放同步 - 简化UI布局结构,移除单一流容器,统一使用流容器组件 - 更新WebSocket插件以支持媒体数据包解析和分发 - 增加重连机制和错误处理,提升视频流播放稳定性 - 移除lodash防抖依赖,改用原生setTimeout实现重连调度 --- src/plugins/websocket.js | 147 +++-- .../register/components/Camera/index.vue | 582 ++++++++++++------ 2 files changed, 485 insertions(+), 244 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 + 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 02/96] =?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 @@ + + + + + \ 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 @@ + + + + + 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 03/96] =?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); }); From 9715101faf2fa230a4f1a1345339f2c19c52a1e0 Mon Sep 17 00:00:00 2001 From: lixiaolong <702156524@qq.com> Date: Tue, 4 Aug 2026 10:54:53 +0800 Subject: [PATCH 04/96] =?UTF-8?q?feat(flow):=20=E6=B7=BB=E5=8A=A0=E6=9C=BA?= =?UTF-8?q?=E6=A2=B0=E8=87=82=E5=85=B3=E8=8A=82=E8=BF=90=E5=8A=A8=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在BasicNodeParams.vue中为ARM_MOVE_TO_J操作添加按钮显示逻辑 - 从设备API导入getArmJointState方法用于获取机械臂关节状态 - 实现ARM_MOVE_TO_J操作的表单验证和位置获取逻辑 - 在config.js中添加booleanOptions选项配置 - 添加机械臂关节运动节点配置,包括参数定义和属性设置 --- src/api/device/flow.js | 10 ++++++- .../components/params/BasicNodeParams.vue | 30 +++++++++++++++++-- src/views/flow/config.js | 23 ++++++++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/api/device/flow.js b/src/api/device/flow.js index 5985597..4886120 100644 --- a/src/api/device/flow.js +++ b/src/api/device/flow.js @@ -76,4 +76,12 @@ export function getAgvStatus(params) { method: 'get', params: params }) -} \ No newline at end of file +} + +export function getArmJointState(params) { + return request({ + url: '/api/arm/getJointState', + method: 'get', + params: params + }) +} diff --git a/src/views/flow/components/params/BasicNodeParams.vue b/src/views/flow/components/params/BasicNodeParams.vue index cbdc827..f4174bb 100644 --- a/src/views/flow/components/params/BasicNodeParams.vue +++ b/src/views/flow/components/params/BasicNodeParams.vue @@ -11,7 +11,7 @@
- 获取位置 + 获取位置
@@ -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, getAgvStatus } from '@/api/device/flow' +import { getArmStatus, getArmJointState, getAgvStatus } from '@/api/device/flow' import { de } from 'element-plus/es/locale/index.mjs' const props = defineProps({ @@ -298,6 +298,30 @@ const submitRobotForm = () => { } } }) + } else if (robotFormAction.value === 'ARM_MOVE_TO_J') { + robotFormRef.value.validate(async (valid) => { + if (valid) { + try { + const response = await getArmJointState({ + deviceId: robotForm.deviceId, + terminalId: robotForm.terminalId + }) + if (response && response.data) { + const target = response.data.position || response.data.positions + if (!Array.isArray(target) || target.length === 0) { + throw new Error('机械臂未返回有效的关节位置') + } + dialogVisible.value = false + const targetParam = formData.nodeParams.find(param => param.name === 'target') + const deviceParam = formData.nodeParams.find(param => param.name === 'deviceId') + if (targetParam) targetParam.input = JSON.stringify(target) + if (deviceParam) deviceParam.input = robotForm.deviceId + } + } catch (error) { + console.error('获取机械臂关节位置失败:', error) + } + } + }) } } @@ -383,4 +407,4 @@ defineExpose({ validateAndSave }) cursor: pointer; } } - \ No newline at end of file + diff --git a/src/views/flow/config.js b/src/views/flow/config.js index a82eafe..3684e9a 100644 --- a/src/views/flow/config.js +++ b/src/views/flow/config.js @@ -37,6 +37,11 @@ const audioOptions = () => { }] } +const booleanOptions = () => [ + { value: false, label: '否' }, + { value: true, label: '是' } +] + const httpMethodOptions = () => { return [{ value: 'POST', @@ -360,6 +365,24 @@ function handler(params) { ], outputType: 'json' }, + { + icon: videoSvg, + name: "机械臂关节运动", + type: "serviceNode", + desc: "通过关节空间插值运动到目标关节位置", + action: 'ARM_MOVE_TO_J', + nodeType: 'EDGE', + nodeParams: [ + { name: "deviceId", type: "input", input: "", disabled: true }, + { name: "target", type: "input", input: "[0, 0, 0, 0, 0, 0]", disabled: true }, + { name: "velocity", type: "input", componentType: 'number', max: 5, step: 0.1, input: 0.5, disabled: true }, + { name: "acceleration", type: "input", componentType: 'number', max: 5, step: 0.1, input: 1, disabled: true }, + { name: "blendRadius", type: "input", componentType: 'number', max: 5, step: 0.01, input: 0, required: false, disabled: true }, + { name: "jointVelocityLimits", type: "input", input: "", required: false, disabled: true }, + { name: "asynchronous", type: "input", input: false, componentType: 'select', selectOptions: booleanOptions(), required: false, disabled: true } + ], + outputType: 'json' + }, { icon: cameraSvg, name: "开始监听报警事件", From 95ea34bbb8101d9216122f7158645d1475cc510f Mon Sep 17 00:00:00 2001 From: lixiaolong <702156524@qq.com> Date: Tue, 4 Aug 2026 11:46:14 +0800 Subject: [PATCH 05/96] =?UTF-8?q?refactor(workflow):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81=E9=85=8D=E7=BD=AE=E5=92=8C=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 config.js 配置文件结构以支持新的工作流参数 - 修改 flow.js 工作流逻辑实现,优化执行效率 - 重构 index.vue 组件界面,改进用户体验和交互流程 - 统一配置管理方式,提高代码可维护性 - 优化组件渲染性能,减少不必要的重绘 - 调整工作流状态管理机制,增强稳定性 --- src/api/device/flow.js | 8 ++++ src/views/flow/config.js | 13 ++++++ src/views/flow/index.vue | 86 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 104 insertions(+), 3 deletions(-) diff --git a/src/api/device/flow.js b/src/api/device/flow.js index 4886120..f8e7878 100644 --- a/src/api/device/flow.js +++ b/src/api/device/flow.js @@ -78,6 +78,14 @@ export function getAgvStatus(params) { }) } +export function getAgvStations(params) { + return request({ + url: '/api/agv/listStations', + method: 'get', + params: params + }) +} + export function getArmJointState(params) { return request({ url: '/api/arm/getJointState', diff --git a/src/views/flow/config.js b/src/views/flow/config.js index 3684e9a..eb60bda 100644 --- a/src/views/flow/config.js +++ b/src/views/flow/config.js @@ -328,6 +328,19 @@ function handler(params) { ], outputType: 'json' }, + { + icon: videoSvg, + name: "AGV站点导航", + type: "serviceNode", + desc: "从AGV站点列表选择目标站点并导航", + action: 'AGV_MOVE_TO_STATION', + nodeType: 'EDGE', + nodeParams: [ + { name: "deviceId", type: "input", input: "", disabled: true }, + { name: "stationId", type: "input", input: "", componentType: 'select', selectOptions: [], forceSelect: true, disabled: true } + ], + outputType: 'json' + }, { icon: cameraSvg, name: "通用指令", diff --git a/src/views/flow/index.vue b/src/views/flow/index.vue index c1554e6..adf70b2 100644 --- a/src/views/flow/index.vue +++ b/src/views/flow/index.vue @@ -161,6 +161,16 @@

执行{{ nodeName }}节点

+
+ 加载站点列表 + {{ singleNodeTerminalId && singleNodeDeviceId ? '加载后从站点下拉框选择' : '请先填写终端ID和设备ID' }} +
+ formData.nodeParams.find((item) => item.name === "terminalId")?.input +); +const singleNodeDeviceId = computed(() => + formData.nodeParams.find((item) => item.name === "deviceId")?.input +); + +const stationOptions = (stations) => (Array.isArray(stations) ? stations : []) + .filter((station) => station?.id !== undefined && station?.id !== null && String(station.id).trim()) + .map((station) => { + const id = String(station.id).trim(); + const description = station.description ? String(station.description).trim() : ""; + return { value: id, label: description ? `${id} - ${description}` : id }; + }); + +const loadSingleNodeStations = async () => { + stationLoading.value = true; + try { + const response = await getAgvStations({ + terminalId: singleNodeTerminalId.value, + deviceId: singleNodeDeviceId.value, + }); + const options = stationOptions(response?.data); + const stationParam = formData.nodeParams.find((item) => item.name === "stationId"); + if (!stationParam) return; + stationParam.selectOptions = options; + stationParam.stationTerminalId = singleNodeTerminalId.value; + stationParam.stationDeviceId = singleNodeDeviceId.value; + if (!options.some((item) => item.value === stationParam.input)) stationParam.input = ""; + if (options.length === 0) ElMessage.warning("当前AGV没有可用站点"); + } catch (error) { + console.error("加载AGV站点列表失败:", error); + ElMessage.error("加载AGV站点列表失败"); + } finally { + stationLoading.value = false; + } +}; const singleNodeExecution = (e) => { const { name, nodeType, nodeParams, action } = e.detail; - console.log('nodeParams', nodeParams) + const executionParams = JSON.parse(JSON.stringify(nodeParams || [])); nodeName.value = name; visible.value = true; nodeAction.value = action; + stationLoading.value = false; if (nodeType === "EDGE") { formData.nodeParams = [ { @@ -893,14 +943,31 @@ const singleNodeExecution = (e) => { name: "terminalId", type: "input", }, - ...nodeParams, + ...executionParams, ]; } else { - formData.nodeParams = nodeParams; + formData.nodeParams = executionParams; + } + if (action === "AGV_MOVE_TO_STATION") { + const stationParam = formData.nodeParams.find((item) => item.name === "stationId"); + if (stationParam) { + stationParam.input = ""; + stationParam.selectOptions = []; + } } }; const execute = async () => { + if (nodeAction.value === "AGV_MOVE_TO_STATION") { + const stationParam = formData.nodeParams.find((item) => item.name === "stationId"); + const validStation = stationParam?.selectOptions?.some((item) => item.value === stationParam.input); + const correctSource = stationParam?.stationTerminalId === singleNodeTerminalId.value + && stationParam?.stationDeviceId === singleNodeDeviceId.value; + if (!validStation || !correctSource) { + ElMessage.warning("请按当前终端和设备重新加载并选择站点"); + return; + } + } const result = await executeForm.value.validate(); if (result) { const obj = {}; @@ -1102,6 +1169,19 @@ onUnmounted(() => { \ No newline at end of file diff --git a/src/views/inspection/cockpit/components/ManualTakeover/index.vue b/src/views/inspection/cockpit/components/ManualTakeover/index.vue index c31703a..63bd4aa 100644 --- a/src/views/inspection/cockpit/components/ManualTakeover/index.vue +++ b/src/views/inspection/cockpit/components/ManualTakeover/index.vue @@ -95,7 +95,7 @@
- +
监控
@@ -115,7 +115,7 @@ - \ No newline at end of file + + + + + From bac6e1aa52ea57f35d886ecc2caed91772f629c2 Mon Sep 17 00:00:00 2001 From: lixiaolong <702156524@qq.com> Date: Tue, 4 Aug 2026 15:35:32 +0800 Subject: [PATCH 08/96] =?UTF-8?q?feat(flow):=20=E6=B7=BB=E5=8A=A0AGV?= =?UTF-8?q?=E7=AB=99=E7=82=B9=E5=88=97=E8=A1=A8=E5=8A=9F=E8=83=BD=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在基本节点参数组件中增加对AGV_MOVE_TO_STATION操作的支持 - 添加加载AGV站点列表的按钮和对话框功能 - 实现站点列表的异步加载和选项填充逻辑 - 为站点ID参数添加特殊验证规则确保设备匹配 - 在选择站点时添加设备ID验证防止数据错配 - 添加加载状态指示器和错误处理机制 - 实现站点选项的数据转换和显示格式化功能 --- .../components/params/BasicNodeParams.vue | 74 +++++++++++++++++-- 1 file changed, 67 insertions(+), 7 deletions(-) diff --git a/src/views/flow/components/params/BasicNodeParams.vue b/src/views/flow/components/params/BasicNodeParams.vue index f4174bb..c127f81 100644 --- a/src/views/flow/components/params/BasicNodeParams.vue +++ b/src/views/flow/components/params/BasicNodeParams.vue @@ -11,7 +11,7 @@
- 获取位置 + {{ robotActionButtonText }}
@@ -22,7 +22,7 @@ v-model="property.name" placeholder="请输入" clearable /> - + @@ -82,7 +82,7 @@ @@ -95,19 +95,19 @@ - \ No newline at end of file + diff --git a/src/views/is/project/evaluate/Run.vue b/src/views/is/project/evaluate/Run.vue index ab8485e..e932bff 100644 --- a/src/views/is/project/evaluate/Run.vue +++ b/src/views/is/project/evaluate/Run.vue @@ -7,8 +7,8 @@ :rules="rules" label-width="auto" > - - + + { - const res = await listTerminal({ pageNum: 1, pageSize: 1000 }); + const res = await getRobotList({ pageNum: 1, pageSize: 1000, connectStatus: '1' }); if (res.code === 200) { terminalIdOptions.value = res.rows?.map((item) => { return { - label: `${item.name}(${item.host}:${item.port})`, - value: item.id, + label: `${item.robotName || item.robotId} (${item.robotId})`, + value: item.robotId, }; }); } @@ -138,7 +138,7 @@ const confirm = () => { const data = formatTableData(item.config) runParams[item.itemId] = data }) - const res = await projectExecute({ runParams, projectId: props.projectId, terminalId: formData.value.terminalId }) + const res = await projectExecute({ runParams, projectId: props.projectId, robotId: formData.value.robotId }) // 后端临时应付检查添加,后续可删除下一行代码,解开上一行代码注释 // const res = await executeProject(props.functionIds) if (res.code === 200) { @@ -167,4 +167,4 @@ watch( } } ); - \ No newline at end of file + diff --git a/src/views/test/configs/Run.vue b/src/views/test/configs/Run.vue index 3040369..8d59d03 100644 --- a/src/views/test/configs/Run.vue +++ b/src/views/test/configs/Run.vue @@ -7,8 +7,8 @@ :rules="rules" label-width="auto" > - - + + { const data = formatTableData(item.config) runParams[item.itemId] = data }) - const res = await taskExecute({ runParams, taskId: props.currentRow.id, terminalId: formData.value.terminalId }) + const res = await taskExecute({ runParams, taskId: props.currentRow.id, robotId: formData.value.robotId }) if (res.code === 200) { ruleFormRef.value.resetFields(); ElMessage.success('执行成功') @@ -145,12 +146,12 @@ const terminalIdOptions = ref([]); /** 获取设备终端列表 */ const handleGetTerminalGroup = async (row) => { - const res = await listTerminal({ pageNum: 1, pageSize: 1000 }); + const res = await getRobotList({ pageNum: 1, pageSize: 1000, connectStatus: '1' }); if (res.code === 200) { terminalIdOptions.value = res.rows?.map((item) => { return { - label: `${item.name}(${item.host}:${item.port})`, - value: item.id, + label: `${item.robotName || item.robotId} (${item.robotId})`, + value: item.robotId, }; }); } diff --git a/src/views/vi/testProject/evaluate/Run.vue b/src/views/vi/testProject/evaluate/Run.vue index e4fa6aa..90ac19a 100644 --- a/src/views/vi/testProject/evaluate/Run.vue +++ b/src/views/vi/testProject/evaluate/Run.vue @@ -7,8 +7,8 @@ :rules="rules" label-width="auto" > - - + + { - const res = await listTerminal({ pageNum: 1, pageSize: 1000 }); + const res = await getRobotList({ pageNum: 1, pageSize: 1000, connectStatus: '1' }); if (res.code === 200) { terminalIdOptions.value = res.rows?.map((item) => { return { - label: `${item.name}(${item.host}:${item.port})`, - value: item.id, + label: `${item.robotName || item.robotId} (${item.robotId})`, + value: item.robotId, }; }); } @@ -137,7 +137,7 @@ const confirm = () => { const data = formatTableData(item.config) runParams[item.itemId] = data }) - const res = await projectExecute({ runParams, projectId: props.projectId, terminalId: formData.value.terminalId }) + const res = await projectExecute({ runParams, projectId: props.projectId, robotId: formData.value.robotId }) if (res.code === 200) { ruleFormRef.value.resetFields(); ElMessage.success('执行成功') @@ -164,4 +164,4 @@ watch( } } ); - \ No newline at end of file + From 45b190ff028bb0df5015e73c1f264a71ed2c0ab9 Mon Sep 17 00:00:00 2001 From: lixiaolong <702156524@qq.com> Date: Wed, 5 Aug 2026 09:27:55 +0800 Subject: [PATCH 13/96] =?UTF-8?q?feat(flow):=20=E4=BC=98=E5=8C=96=E6=9C=BA?= =?UTF-8?q?=E5=99=A8=E4=BA=BA=E8=AE=BE=E5=A4=87=E7=BB=91=E5=AE=9A=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将terminalId替换为robotId以统一机器人标识 - 实现机器人设备动态加载和选择功能 - 添加设备类型过滤和状态显示 - 重构设备注册页面为机器人设备管理界面 - 优化API调用参数传递方式 - 添加设备自动匹配和锁定机制 --- src/api/device/camera.js | 4 +- src/api/inspection/robot.js | 12 + src/store/modules/flow.js | 11 +- .../register/components/Camera/index.vue | 31 +- .../components/DexHand/LeftBottomHand.vue | 12 +- .../components/DexHand/LeftTopHand.vue | 14 +- .../register/components/DexHand/index.vue | 38 +- .../device/register/components/Head/index.vue | 10 +- .../register/components/Microphone/index.vue | 30 +- .../register/components/Speaker/index.vue | 10 +- src/views/device/register/index.vue | 538 +++++------------- src/views/device/robotManger/index.vue | 6 +- src/views/flow/components/TestRun.vue | 133 ++++- .../components/params/BasicNodeParams.vue | 119 +++- src/views/flow/config.js | 43 +- src/views/flow/index.vue | 83 ++- .../ManualTakeover/DirectionControl.vue | 14 +- .../components/ManualTakeover/RoboticArm.vue | 20 +- .../ManualTakeover/VoiceConversation.vue | 35 +- .../components/ManualTakeover/index.vue | 68 ++- src/views/is/project/evaluate/Run.vue | 6 +- src/views/test/configs/Run.vue | 6 +- src/views/test/configs/index.vue | 2 +- src/views/vi/testProject/evaluate/Run.vue | 6 +- 24 files changed, 675 insertions(+), 576 deletions(-) diff --git a/src/api/device/camera.js b/src/api/device/camera.js index 71781d4..c10a7be 100644 --- a/src/api/device/camera.js +++ b/src/api/device/camera.js @@ -1,9 +1,9 @@ import request from '@/utils/request' -export function getRgbStreamUrl(terminalId, deviceId) { +export function getRgbStreamUrl(robotId, deviceId) { return request({ url: '/api/edge/camera/rgb-stream-url', method: 'get', - params: { terminalId, deviceId } + params: { robotId, deviceId } }) } diff --git a/src/api/inspection/robot.js b/src/api/inspection/robot.js index a0a2e21..a1ee25b 100644 --- a/src/api/inspection/robot.js +++ b/src/api/inspection/robot.js @@ -38,6 +38,18 @@ export function getRobotDevices(id) { }) } +export function getRobotDevicesByRobotId(robotId, params = {}) { + return request({ + url: '/inspection/robot/devices', + method: 'get', + params: { + robotId, + onlineOnly: true, + ...params + } + }) +} + /** * 获取机器人地图 * @param {*} robotId diff --git a/src/store/modules/flow.js b/src/store/modules/flow.js index 287ae45..a78c228 100644 --- a/src/store/modules/flow.js +++ b/src/store/modules/flow.js @@ -2,20 +2,11 @@ import { defineStore } from 'pinia' export const useFlowStore = defineStore('flow', { state: () => ({ - disableForm: false, - deviceList: [] + disableForm: false }), actions: { updateDisableForm(value) { this.disableForm = value - }, - - async getDeviceList() { - const data = await new Promise((resolve, reject) => { - resolve( ['cam1', 'cam2', 'cam3', 'cam4']) - }) - this.deviceList = data - return data } } } diff --git a/src/views/device/register/components/Camera/index.vue b/src/views/device/register/components/Camera/index.vue index a7becbe..8e820bf 100644 --- a/src/views/device/register/components/Camera/index.vue +++ b/src/views/device/register/components/Camera/index.vue @@ -47,7 +47,7 @@ const props = defineProps({ type: String, default: null, }, - initialTerminalId: { + initialRobotId: { type: String, default: null, }, @@ -60,7 +60,7 @@ const data = reactive({ stereoModule: false, rgbCamera: false, }, - terminalId: null, + robotId: null, cameraId: null, type: 'camera', callbacks: {}, @@ -95,7 +95,7 @@ function getVideoRef(method) { } async function startDirectRgbStream() { - if (!data.terminalId || !data.cameraId || !colorVideo.value) return; + if (!data.robotId || !data.cameraId || !colorVideo.value) return; rgbStreamAbortController?.abort(); rgbStreamAbortController = null; const generation = ++rgbStreamGeneration; @@ -103,7 +103,7 @@ async function startDirectRgbStream() { rgbStreamRetryTimer = null; streamStatus.getRGBImageStream = 'connecting'; try { - const response = await getRgbStreamUrl(data.terminalId, data.cameraId); + const response = await getRgbStreamUrl(data.robotId, data.cameraId); if (generation !== rgbStreamGeneration || !data.form.rgbCamera) return; const relativeUrl = response.data; const baseUrl = import.meta.env.VITE_APP_BASE_API.replace(/\/$/, ''); @@ -342,11 +342,11 @@ function destroyMediaPlayer(method) { } function channelFor(method) { - return `edgeCameraServiceImpl/${method}/${data.terminalId}/${data.cameraId}`; + return `edgeCameraServiceImpl/${method}/${data.robotId}/${data.cameraId}`; } async function setSubscription(enabled, method, force = false) { - if (!socket || !data.terminalId || !data.cameraId) return; + if (!socket || !data.robotId || !data.cameraId) return; await nextTick(); const channel = channelFor(method); @@ -401,7 +401,7 @@ function scheduleRestart(method) { } function handleSocketOpen() { - if (data.form.stereoModule && data.terminalId && data.cameraId) { + if (data.form.stereoModule && data.robotId && data.cameraId) { destroyMediaPlayer('getDepthImageStream'); setSubscription(true, 'getDepthImageStream', true); } @@ -418,12 +418,12 @@ socket?.on('open', handleSocketOpen); socket?.on('close', handleSocketClose); watch(() => data.form.stereoModule, async (newVal) => { - if (!data.terminalId || !data.cameraId) return; + if (!data.robotId || !data.cameraId) return; await setSubscription(newVal, 'getDepthImageStream'); }); watch(() => data.form.rgbCamera, async (newVal) => { - if (!data.terminalId || !data.cameraId) return; + if (!data.robotId || !data.cameraId) return; if (newVal) { await nextTick(); await startDirectRgbStream(); @@ -436,10 +436,19 @@ watch( () => props.initialDeviceId || route.path.split('/')[3], async (deviceId) => { if (!deviceId) return; + const queryRobotId = props.initialRobotId || route.query.robotId; + const queryDeviceId = props.initialDeviceId || route.query.deviceId; + if (queryRobotId && queryDeviceId) { + data.cameraId = String(queryDeviceId); + data.robotId = String(queryRobotId); + if (data.form.stereoModule) await setSubscription(true, 'getDepthImageStream'); + if (data.form.rgbCamera) await startDirectRgbStream(); + return; + } try { const response = await getRegister(deviceId); data.cameraId = response.data.deviceCode; - data.terminalId = props.initialTerminalId || response.data.idDeDeviceTerminalConfig; + data.robotId = props.initialRobotId || response.data.idDeDeviceTerminalConfig; if (data.form.stereoModule) await setSubscription(true, 'getDepthImageStream'); if (data.form.rgbCamera) await startDirectRgbStream(); } catch (error) { @@ -455,7 +464,7 @@ onUnmounted(() => { stopDirectRgbStream(); ['getDepthImageStream'].forEach(method => { clearTimeout(retryTimers.get(method)); - if (data.terminalId && data.cameraId) { + if (data.robotId && data.cameraId) { const channel = channelFor(method); socket?.send({ type: 'channel_subscription', action: 'unsubscribe', channel }); if (data.callbacks[channel]) socket.off(channel, data.callbacks[channel]); diff --git a/src/views/device/register/components/DexHand/LeftBottomHand.vue b/src/views/device/register/components/DexHand/LeftBottomHand.vue index 27d3d37..302711f 100644 --- a/src/views/device/register/components/DexHand/LeftBottomHand.vue +++ b/src/views/device/register/components/DexHand/LeftBottomHand.vue @@ -17,7 +17,7 @@ import { inject } from 'vue'; const emit = defineEmits(['update:bottomSeriesData']); const socket = inject('ws'); const handCanvas = ref(null); -const terminalId = ref(''); // 待您修改 +const robotId = ref(''); // 待您修改 const deviceId = ref(''); // 待您修改 const frameData = ref({ count: 0, lastTime: 0 }); const isHandSeries = ref(false); @@ -29,7 +29,7 @@ const props = defineProps({ type: String, default: null, }, - initialTerminalId: { // 示例 prop,用于从弹窗接收 terminalId + initialRobotId: { // 示例 prop,用于从弹窗接收 robotId type: String, default: null, }, @@ -37,7 +37,7 @@ const props = defineProps({ // 2. 使用 watchEffect 来响应 props 和路由的变化 watchEffect(() => { - terminalId.value = props.initialTerminalId; + robotId.value = props.initialRobotId; deviceId.value = props.initialDeviceId; }); @@ -225,11 +225,11 @@ const sensorMap = [ // WebSocket 订阅 const subscribeSensorData = async (sub) => { - if (!socket || !terminalId.value || !deviceId.value) { - console.warn(`订阅失败: socket=${!!socket}, terminalId=${terminalId.value}, deviceId=${deviceId.value}`); + if (!socket || !robotId.value || !deviceId.value) { + console.warn(`订阅失败: socket=${!!socket}, robotId=${robotId.value}, deviceId=${deviceId.value}`); return; } - const channel = `edgeDexHandServiceImpl/getSensorDataStream/${terminalId.value}/${deviceId.value}`; + const channel = `edgeDexHandServiceImpl/getSensorDataStream/${robotId.value}/${deviceId.value}`; console.log(sub ? '订阅' : '取消订阅', channel); socket.send({ type: 'channel_subscription', diff --git a/src/views/device/register/components/DexHand/LeftTopHand.vue b/src/views/device/register/components/DexHand/LeftTopHand.vue index 5261e51..faeae7c 100644 --- a/src/views/device/register/components/DexHand/LeftTopHand.vue +++ b/src/views/device/register/components/DexHand/LeftTopHand.vue @@ -35,7 +35,7 @@ const handImage = ref(null); const containerRef = ref(null); // 新增:引用父容器 const defaultHandImageInfo = { width: 891, height: 981 }; const imageAspectRatio = defaultHandImageInfo.width / defaultHandImageInfo.height; -const terminalId = ref(''); +const robotId = ref(''); const deviceId = ref(''); // 1. 定义 props const props = defineProps({ @@ -43,7 +43,7 @@ const props = defineProps({ type: String, default: null, }, - initialTerminalId: { // 示例 prop,用于从弹窗接收 terminalId + initialRobotId: { // 示例 prop,用于从弹窗接收 robotId type: String, default: null, }, @@ -51,9 +51,9 @@ const props = defineProps({ // 2. 使用 watchEffect 来响应 props 和路由的变化 watchEffect(() => { - terminalId.value = props.initialTerminalId; + robotId.value = props.initialRobotId; deviceId.value = props.initialDeviceId; - console.log(deviceId.value, terminalId.value,props) + console.log(deviceId.value, robotId.value,props) }); const sliders = ref([ @@ -66,9 +66,9 @@ const sliders = ref([ ]); function updateSeriesData(value, i) { - console.log(deviceId.value, terminalId.value) + console.log(deviceId.value, robotId.value) loading.value = true; - setDexHandAngle({ deviceId: deviceId.value, terminalId: terminalId.value, value: value / 100, id: i }).then(() => { + setDexHandAngle({ deviceId: deviceId.value, robotId: robotId.value, value: value / 100, id: i }).then(() => { loading.value = false; updateToSeriesData(); }).catch(() => { @@ -131,7 +131,7 @@ const updateSliderPositions = () => { }; const updateToSeriesData = () => { - status({ deviceId: deviceId.value, terminalId: terminalId.value }).then((res) => { + status({ deviceId: deviceId.value, robotId: robotId.value }).then((res) => { const newTopSeriesData = res.data.handsList.map(item => item.force); emit('update:topSeriesData', newTopSeriesData); }); diff --git a/src/views/device/register/components/DexHand/index.vue b/src/views/device/register/components/DexHand/index.vue index fd33381..892ad7a 100644 --- a/src/views/device/register/components/DexHand/index.vue +++ b/src/views/device/register/components/DexHand/index.vue @@ -4,11 +4,11 @@
- +
- - + +
@@ -34,7 +34,7 @@ const props = defineProps({ type: String, default: null, }, - initialTerminalId: { + initialRobotId: { type: String, default: null, }, @@ -62,7 +62,7 @@ const avgData = ref({ // 声明组件内部使用的响应式变量 const deviceId = ref(""); -const terminalId = ref(""); +const robotId = ref(""); const handleUpdateTopSeriesData = (newData) => { topSeriesData.value = newData; @@ -77,19 +77,24 @@ const handleUpdateBottomSeriesData = ({ maxData: newMaxData, avgData: newAvgData const route = useRoute(); // 统一处理获取 register 数据的方法 const fetchRegisterData = (currentDeviceId) => { + if (route.query.robotId && route.query.deviceId) { + robotId.value = String(route.query.robotId); + deviceId.value = String(route.query.deviceId); + return; + } if (currentDeviceId) { getRegister(currentDeviceId).then(res => { deviceId.value = res.data.deviceCode; // 再次更新,确保与 API 返回一致 - terminalId.value = res.data.idDeDeviceTerminalConfig; + robotId.value = res.data.idDeDeviceTerminalConfig; }).catch(error => { console.error("Error fetching register:", error); - // 可以根据需要重置 deviceId 和 terminalId + // 可以根据需要重置 deviceId 和 robotId deviceId.value = ""; - terminalId.value = ""; + robotId.value = ""; }); } else { - // 如果 deviceId 为空,可以考虑重置 terminalId - terminalId.value = ""; + // 如果 deviceId 为空,可以考虑重置 robotId + robotId.value = ""; } }; @@ -107,6 +112,11 @@ watch(() => props.initialDeviceId, (newVal) => { // 注意:这里假设 route.path.split("/")[3] 是cameraId // 如果你的路由结构不同,请调整这里的逻辑 watch(() => route.path, (newPath) => { + if (route.query.robotId && route.query.deviceId) { + robotId.value = String(route.query.robotId); + deviceId.value = String(route.query.deviceId); + return; + } const routeDeviceId = newPath.split("/")[3]; if (!deviceId.value && routeDeviceId) { // 只有当 deviceId 尚未从 props 或其他地方设置时才从路由获取 deviceId.value = routeDeviceId; @@ -115,10 +125,10 @@ watch(() => route.path, (newPath) => { }, { immediate: true }); // immediate: true 确保在组件挂载时也检查路由 -// 如果需要 initialTerminalId prop 也影响 terminalId ref,可以添加一个 watch -watch(() => props.initialTerminalId, (newVal) => { +// 如果需要 initialRobotId prop 也影响 robotId ref,可以添加一个 watch +watch(() => props.initialRobotId, (newVal) => { if (newVal) { - terminalId.value = newVal; + robotId.value = newVal; } }, { immediate: true }); @@ -155,4 +165,4 @@ watch(() => props.initialTerminalId, (newVal) => { width: 100%; overflow: hidden; } - \ No newline at end of file + diff --git a/src/views/device/register/components/Head/index.vue b/src/views/device/register/components/Head/index.vue index 3142033..ae48490 100644 --- a/src/views/device/register/components/Head/index.vue +++ b/src/views/device/register/components/Head/index.vue @@ -80,10 +80,12 @@ + + diff --git a/src/views/device/robotManger/index.vue b/src/views/device/robotManger/index.vue index 058ce86..7eb5638 100644 --- a/src/views/device/robotManger/index.vue +++ b/src/views/device/robotManger/index.vue @@ -61,7 +61,7 @@ ×
- +
@@ -398,7 +398,7 @@ const openPopup = (area) => { originalTop: `${area.y}px`, originalLeft: `${area.x + leftOffset}px`, isMaximized: false, - props: { terminalId: 'your-terminal-id', cameraId: 'your-camera-id' } // 示例props + props: { robotId: area.robotId || '', cameraId: area.deviceId || '' } }; popups.value.push(popup); isResizing.value.push(false); @@ -871,4 +871,4 @@ onMounted(() => { flex: 1; overflow: auto; } - \ No newline at end of file + diff --git a/src/views/flow/components/TestRun.vue b/src/views/flow/components/TestRun.vue index bdd1b69..241f387 100644 --- a/src/views/flow/components/TestRun.vue +++ b/src/views/flow/components/TestRun.vue @@ -11,6 +11,18 @@ :rules="rules" label-width="auto" > +
+
+ + 机器人设备自动匹配 +
+
正在读取机器人在线设备...
+
+ 已匹配 {{ bindingSummary.matched }} 个设备节点 + ,{{ bindingSummary.missing }} 个节点暂无匹配设备 +
+
选择机器人后,将按节点类型自动填充该机器人上报的在线设备。
+
@@ -66,7 +78,8 @@ import { getStartNodeFormData, formatTableData } from '@/utils/flow' import { flowExecuteTrial } from '@/api/flow/flow' import { emitter } from '@/utils/eventBus'; import { ElMessage } from 'element-plus'; -import { getRobotList } from '@/api/inspection/robot' +import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot' +import { getDeviceKindForAction, toDeviceOptions } from '@/utils/robotDevice' const props = defineProps({ drawer: Boolean, @@ -76,6 +89,8 @@ const props = defineProps({ const ruleFormRef = ref() const rules = ref({}); const tableData = ref([]) +const bindingLoading = ref(false) +const bindingSummary = ref({ robotId: '', matched: 0, missing: 0 }) const emits = defineEmits(['close', 'changeState']) @@ -89,7 +104,8 @@ watch(() => props.drawer, if (newVal) { const data = getStartNodeFormData() tableData.value = data - handleGetTerminalGroup() + bindingSummary.value = { robotId: '', matched: 0, missing: 0 } + loadRobotOptions() } } ) @@ -104,11 +120,11 @@ const confirm = () => { }) const flowData = JSON.stringify(lf.getGraphData()) - const { terminalId, robotId, ...runParams } = formData + const { robotId, ...runParams } = formData const res = await flowExecuteTrial({ flowData, itemId: props.flowId, - robotId: robotId || terminalId, + robotId, runParams }) if (res.code === 200) { @@ -123,13 +139,69 @@ const confirm = () => { .catch(() => {}) } -const terminalIdOptions = ref([]) +const robotIdOptions = ref([]) -/** 获取设备终端列表 */ -const handleGetTerminalGroup = async (row) => { +const handleRobotChange = async (robotId) => { + bindingSummary.value = { robotId: robotId || '', matched: 0, missing: 0 } + if (!robotId) return + bindingLoading.value = true + try { + const response = await getRobotDevicesByRobotId(robotId) + const devices = Array.isArray(response.data) ? response.data : [] + let matched = 0 + let missing = 0 + const missingNodes = [] + const { nodes } = lf.getGraphData() + + nodes.forEach((node) => { + if (node.properties?.nodeType !== 'EDGE') return + const nodeParams = JSON.parse(JSON.stringify(node.properties.nodeParams || [])) + const robotParam = nodeParams.find(param => param.name === 'robotId') + if (robotParam) robotParam.input = robotId + + const deviceParam = nodeParams.find(param => param.name === 'deviceId') + if (!deviceParam) { + if (robotParam) lf.setProperties(node.id, { ...node.properties, nodeParams }) + return + } + + const kind = getDeviceKindForAction(node.properties.action) + const candidates = kind + ? devices.filter(device => Number(device.deviceKind) === kind) + : devices + deviceParam.input = candidates[0]?.deviceId || '' + deviceParam.componentType = 'select' + deviceParam.selectOptions = toDeviceOptions(candidates) + deviceParam.deviceLocked = candidates.length === 1 + lf.setProperties(node.id, { ...node.properties, nodeParams }) + + if (candidates.length > 0) matched += 1 + else { + missing += 1 + missingNodes.push(node.properties.name || node.id) + } + }) + + bindingSummary.value = { robotId, matched, missing } + if (missingNodes.length) { + ElMessage.warning(`以下节点没有匹配的在线设备:${missingNodes.join('、')}`) + } else if (matched > 0) { + ElMessage.success(`已自动匹配 ${matched} 个设备节点`) + } + } catch (error) { + bindingSummary.value = { robotId, matched: 0, missing: 0 } + console.error('自动匹配机器人设备失败:', error) + ElMessage.error('读取机器人设备失败') + } finally { + bindingLoading.value = false + } +} + +/** 获取在线机器人列表 */ +const loadRobotOptions = async () => { const res = await getRobotList({ pageNum: 1, pageSize: 1000, connectStatus: '1' }) if (res.code === 200) { - terminalIdOptions.value = res.rows?.map((item) => { + robotIdOptions.value = res.rows?.map((item) => { return { label: `${item.robotName || item.robotId} (${item.robotId})`, value: item.robotId @@ -139,3 +211,40 @@ const handleGetTerminalGroup = async (row) => { } + + diff --git a/src/views/flow/components/params/BasicNodeParams.vue b/src/views/flow/components/params/BasicNodeParams.vue index 040539d..59fdd4d 100644 --- a/src/views/flow/components/params/BasicNodeParams.vue +++ b/src/views/flow/components/params/BasicNodeParams.vue @@ -11,7 +11,7 @@
- {{ robotActionButtonText }} + {{ robotActionButtonText }}
@@ -82,15 +82,25 @@ - - - +
设备来自机器人最近一次在线上报,单个匹配设备会自动锁定。
+ + + + + - - + + + + +
已自动匹配并锁定
+
+
+
执行设备
+
从机器人当前上报的全部在线设备中选择
+
+ + 选择机器人设备 + +
+ + +
通用指令不限制设备类型,设备列表来自机器人最近一次 QUIC 在线上报。
+ + + + + + + + + + +
仅有一个在线设备,已自动选择并锁定
+
+
+ +
\ No newline at end of file diff --git a/src/views/inspection/cockpit/components/ManualTakeover/RoboticArm.vue b/src/views/inspection/cockpit/components/ManualTakeover/RoboticArm.vue index 2ccbe7e..8ba9ba0 100644 --- a/src/views/inspection/cockpit/components/ManualTakeover/RoboticArm.vue +++ b/src/views/inspection/cockpit/components/ManualTakeover/RoboticArm.vue @@ -282,6 +282,12 @@ watch(() => props.robot, (newRobot) => { }, 100) } }, { immediate: true }) + +onUnmounted(() => { + if (timer.value) { + clearInterval(timer.value) + } +}) \ No newline at end of file + diff --git a/src/views/inspection/cockpit/components/MapOverview.vue b/src/views/inspection/cockpit/components/MapOverview.vue index 40d97b4..2114a44 100644 --- a/src/views/inspection/cockpit/components/MapOverview.vue +++ b/src/views/inspection/cockpit/components/MapOverview.vue @@ -7,7 +7,7 @@
{{ item.robotName }}
-
{{ robotStatus[item.status] }}
+
{{ agvRuntimeStatusLabel(item.status) }}
@@ -86,16 +86,9 @@ import MapCanvas from "./MapCanvas.vue"; import { onMounted, onUnmounted } from "vue"; import { getRobotList } from '@/api/inspection/robot' import { getRunTaskList } from '@/api/inspection/runTask' - -console.log('sss', import.meta.env.VITE_INSPECTION_TYPE) +import { agvRuntimeStatusClass, agvRuntimeStatusLabel } from '@/utils/agvRuntimeStatus' const robotList = ref([]) -const robotStatus = { - "0": "在线", - "1": "离线", - "2": "充电中", - "3": "巡检中" -} const getRobot = async () => { let robotType if (import.meta.env.VITE_INSPECTION_TYPE === 'inspection') { @@ -125,7 +118,7 @@ const taskStatus = { } const getRunningTask = async () => { let taskType - if (import.meta.env.VITE_INSPECTION_TYPE = 'inspection') { + if (import.meta.env.VITE_INSPECTION_TYPE === 'inspection') { taskType = '1' } else { taskType = '2' @@ -161,9 +154,18 @@ const centerCanvasView = () => { } } +let robotRefreshTimer + onMounted(() => { getRobot() getRunningTask() + robotRefreshTimer = window.setInterval(() => { + if (!document.hidden) getRobot() + }, 5000) +}) + +onUnmounted(() => { + window.clearInterval(robotRefreshTimer) }) @@ -224,6 +226,31 @@ onMounted(() => { background: #0f83; color: #0f8; } + + .unknown, + .offline, + .stopped { + background: rgba(148, 163, 184, 0.2); + color: #cbd5e1; + } + + .manual, + .running { + background: rgba(0, 212, 255, 0.18); + color: #00d4ff; + } + + .charging, + .paused { + background: rgba(255, 179, 0, 0.2); + color: #ffb300; + } + + .fault, + .emergency { + background: rgba(255, 77, 79, 0.2); + color: #ff6b6d; + } } .info-box { @@ -403,4 +430,4 @@ onMounted(() => { } } } - \ No newline at end of file + diff --git a/src/views/inspection/robot/index.vue b/src/views/inspection/robot/index.vue index 414f917..f48a64c 100644 --- a/src/views/inspection/robot/index.vue +++ b/src/views/inspection/robot/index.vue @@ -185,6 +185,7 @@ import { computed, getCurrentInstance, onBeforeUnmount, onMounted, reactive, ref import { ElMessage, ElMessageBox } from 'element-plus' import { addRobot, deleteRobot, getRobotDevices, getRobotList, getRobotStatus, updateRobot } from '@/api/inspection/robot' import { getMapList } from '@/api/inspection/map' +import { agvRuntimeStatusLabel, agvRuntimeStatusType } from '@/utils/agvRuntimeStatus' const { proxy } = getCurrentInstance() const { robot_type } = proxy.useDict('robot_type') @@ -256,8 +257,8 @@ async function loadDevices(showLoading = true) { try { const res = await getRobotDevices(activeRobot.value.id); devices.value = res.data || [] } finally { deviceLoading.value = false } } function endpointText(row) { return row.ipAddress && row.port ? `${row.ipAddress}:${row.port}` : '等待连接后自动获取' } -function runtimeStatusLabel(value) { return ({ 0: '在线', 1: '离线', 2: '充电中', 3: '巡检中' })[value] || '未知' } -function runtimeStatusType(value) { return ({ 0: 'success', 1: 'info', 2: 'warning', 3: 'primary' })[value] || 'info' } +const runtimeStatusLabel = agvRuntimeStatusLabel +const runtimeStatusType = agvRuntimeStatusType function hasBattery(row) { return row.batteryLevel !== null && row.batteryLevel !== undefined && row.batteryLevel !== '' } function normalizeBattery(value) { return Math.max(0, Math.min(100, Math.round(Number(value) || 0))) } function batteryColor(value) { From b332a5461e231bcc1a8e01f0db168dc3bfcdaaec Mon Sep 17 00:00:00 2001 From: lixiaolong <702156524@qq.com> Date: Thu, 6 Aug 2026 11:24:50 +0800 Subject: [PATCH 29/96] =?UTF-8?q?feat(utils):=20=E6=B7=BB=E5=8A=A0AGV?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E7=8A=B6=E6=80=81=E5=B7=A5=E5=85=B7=E5=87=BD?= =?UTF-8?q?=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 定义AGV运行状态常量映射,包含未知、离线、在线、手动模式等状态 - 实现状态解析函数resolveStatus,支持数字类型转换 - 提供状态标签、类型和CSS类名的导出函数 - 支持11种不同的AGV运行状态及其对应的显示样式 - 添加默认未知状态兜底处理机制 --- src/utils/agvRuntimeStatus.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/utils/agvRuntimeStatus.js diff --git a/src/utils/agvRuntimeStatus.js b/src/utils/agvRuntimeStatus.js new file mode 100644 index 0000000..5d34334 --- /dev/null +++ b/src/utils/agvRuntimeStatus.js @@ -0,0 +1,18 @@ +const AGV_RUNTIME_STATUS = { + 0: { label: '未知', type: 'info', className: 'unknown' }, + 1: { label: '已断开', type: 'info', className: 'offline' }, + 2: { label: '空闲', type: 'success', className: 'online' }, + 3: { label: '手动模式', type: 'primary', className: 'manual' }, + 4: { label: '自动运行', type: 'primary', className: 'running' }, + 5: { label: '充电中', type: 'warning', className: 'charging' }, + 6: { label: '已暂停', type: 'warning', className: 'paused' }, + 7: { label: '已停止', type: 'info', className: 'stopped' }, + 8: { label: '故障', type: 'danger', className: 'fault' }, + 9: { label: '急停', type: 'danger', className: 'emergency' }, +} + +const resolveStatus = value => AGV_RUNTIME_STATUS[Number(value)] || AGV_RUNTIME_STATUS[0] + +export const agvRuntimeStatusLabel = value => resolveStatus(value).label +export const agvRuntimeStatusType = value => resolveStatus(value).type +export const agvRuntimeStatusClass = value => resolveStatus(value).className From 2159b7f22814098a919670d4ddc6d7060a2cdf49 Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Thu, 6 Aug 2026 11:52:30 +0800 Subject: [PATCH 30/96] =?UTF-8?q?feat:=20=E5=8A=A0=E8=BD=BD=E5=9C=B0?= =?UTF-8?q?=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ManualTakeover/index.vue | 38 +++++++++++++++---- .../cockpit/components/MapCanvas.vue | 10 +++++ .../inspection/cockpit/components/Monitor.vue | 31 +++++++++++++-- 3 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/views/inspection/cockpit/components/ManualTakeover/index.vue b/src/views/inspection/cockpit/components/ManualTakeover/index.vue index 4cbb293..d82338a 100644 --- a/src/views/inspection/cockpit/components/ManualTakeover/index.vue +++ b/src/views/inspection/cockpit/components/ManualTakeover/index.vue @@ -11,7 +11,7 @@
{{ item.robotName }}
-
{{ agvRuntimeStatusLabel(item.status) }}
+
{{ agvRuntimeStatusLabel(item.status) }}
@@ -100,7 +100,7 @@
- +
@@ -134,7 +134,7 @@ import { getJointStateApi, moveJApi, torqueOnApi, speedJApi, stopMotionApi, clea import { Remove, CirclePlus } from '@element-plus/icons-vue' import VoiceConversation from "./VoiceConversation.vue"; import IPlayer from "@/components/IPlayer/index.vue"; -import { agvRuntimeStatusLabel } from '@/utils/agvRuntimeStatus' +import { agvRuntimeStatusClass, agvRuntimeStatusLabel } from '@/utils/agvRuntimeStatus' const robotList = ref([]) const robotDevices = ref([]) @@ -310,7 +310,7 @@ onMounted(() => { fetchRobotList() robotRefreshTimer = window.setInterval(() => { if (!document.hidden) fetchRobotList(false) - }, 5000) + }, 500) }) onUnmounted(() => { @@ -353,10 +353,10 @@ onUnmounted(() => { background: #0F1E38; height: 56px; border-radius: 8px; - padding: 16px; + padding: 12px; display: flex; align-items: center; - gap: 20px; + gap: 12px; margin-bottom: 16px; cursor: pointer; border: 1px solid transparent; @@ -367,10 +367,34 @@ onUnmounted(() => { } .bot-local { - color: #00d4ff; font-size: 11px; margin-top: 4px; } + + .online { + color: #0f8; + } + + .unknown, + .offline, + .stopped { + color: #cbd5e1; + } + + .manual, + .running { + color: #00d4ff; + } + + .charging, + .paused { + color: #ffb300; + } + + .fault, + .emergency { + color: #ff6b6d; + } } .active { diff --git a/src/views/inspection/cockpit/components/MapCanvas.vue b/src/views/inspection/cockpit/components/MapCanvas.vue index b33de48..7d630e4 100644 --- a/src/views/inspection/cockpit/components/MapCanvas.vue +++ b/src/views/inspection/cockpit/components/MapCanvas.vue @@ -28,11 +28,16 @@ import { import { getMapJson } from '@/api/inspection/robot' import { useInspectionStore } from "@/store/modules/inspection"; import { ElMessage } from 'element-plus'; +import { clear } from 'ace-builds/src-noconflict/ext-code_lens'; const props = defineProps({ robotList: { type: Array, default: () => [] + }, + clearMap: { + type: Boolean, + default: false } }) @@ -419,6 +424,11 @@ const colorList = ['#00D4FF', '#FFB300', '#00FF88', '#ff5100', '#fff', '#15ff00' watch(() => props.robotList, (newVal) => { if (newVal.length > 0) { + if (props.clearMap) { + applicationState.robots = [] + } + loadSourceMap() + const ids = applicationState.robots.map(r => r.id); props.robotList.forEach(item => { const position = parseRobotPosition(item.currentPosition) diff --git a/src/views/inspection/cockpit/components/Monitor.vue b/src/views/inspection/cockpit/components/Monitor.vue index ee1ab54..345aa9b 100644 --- a/src/views/inspection/cockpit/components/Monitor.vue +++ b/src/views/inspection/cockpit/components/Monitor.vue @@ -13,8 +13,8 @@
-
机器人#0{{ index + 1}}
-
厂区A-在线
+
{{ item.robotName }}
+
{{ agvRuntimeStatusLabel(item.status) }}
@@ -42,6 +42,7 @@ import SvgIcon from "@/components/SvgIcon"; import IPlayer from "@/components/IPlayer/index.vue"; import { getRobotList } from '@/api/inspection/robot' +import { agvRuntimeStatusClass, agvRuntimeStatusLabel } from '@/utils/agvRuntimeStatus' const videoUrl = ref('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8') @@ -112,10 +113,34 @@ onMounted(() => { } .bot-local { - color: #00d4ff; font-size: 11px; margin-top: 4px; } + + .online { + color: #0f8; + } + + .unknown, + .offline, + .stopped { + color: #cbd5e1; + } + + .manual, + .running { + color: #00d4ff; + } + + .charging, + .paused { + color: #ffb300; + } + + .fault, + .emergency { + color: #ff6b6d; + } } .active { From f5b1b70274af91b493264c2152e571ba939dd1f9 Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Thu, 6 Aug 2026 13:46:08 +0800 Subject: [PATCH 31/96] =?UTF-8?q?feat:=20=E5=A4=84=E7=90=86=E8=BD=AE?= =?UTF-8?q?=E8=AF=A2=E6=97=B6=E5=9C=B0=E5=9B=BE=E5=88=B7=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ManualTakeover/index.vue | 2 +- .../cockpit/components/MapCanvas.vue | 131 +++++++++--------- 2 files changed, 64 insertions(+), 69 deletions(-) diff --git a/src/views/inspection/cockpit/components/ManualTakeover/index.vue b/src/views/inspection/cockpit/components/ManualTakeover/index.vue index d82338a..d78e55e 100644 --- a/src/views/inspection/cockpit/components/ManualTakeover/index.vue +++ b/src/views/inspection/cockpit/components/ManualTakeover/index.vue @@ -100,7 +100,7 @@
- +
diff --git a/src/views/inspection/cockpit/components/MapCanvas.vue b/src/views/inspection/cockpit/components/MapCanvas.vue index 7d630e4..fc29cc2 100644 --- a/src/views/inspection/cockpit/components/MapCanvas.vue +++ b/src/views/inspection/cockpit/components/MapCanvas.vue @@ -28,16 +28,11 @@ import { import { getMapJson } from '@/api/inspection/robot' import { useInspectionStore } from "@/store/modules/inspection"; import { ElMessage } from 'element-plus'; -import { clear } from 'ace-builds/src-noconflict/ext-code_lens'; const props = defineProps({ robotList: { type: Array, default: () => [] - }, - clearMap: { - type: Boolean, - default: false } }) @@ -78,6 +73,9 @@ const applicationState = { let mapCanvas; // 主渲染画布 DOM 元素 let canvasCtx; // 2D 渲染上下文 +let animationFrameId; +let mapLoadVersion = 0; + const init = () => { mapCanvas = document.getElementById('map-canvas'); canvasCtx = mapCanvas.getContext('2d'); @@ -191,7 +189,7 @@ function renderFrame(timestamp) { // 如果地图未加载,跳过绘制 if (!applicationState.isMapLoaded) { - requestAnimationFrame(renderFrame); // 继续请求下一帧 + animationFrameId = requestAnimationFrame(renderFrame); // 继续请求下一帧 return; } @@ -241,7 +239,7 @@ function renderFrame(timestamp) { drawMapBoundary(canvasCtx, map); // 请求下一帧渲染 - requestAnimationFrame(renderFrame); + animationFrameId = requestAnimationFrame(renderFrame); } const drawAllRobots = (ctx) => { @@ -354,14 +352,25 @@ const centerCanvasView = () => { inspectionStore.camera.centerY = (map.originY + map.maxY) / 2; } -const loadSourceMap = async () => { - const connectedRobot = props.robotList.find(item => item.connectStatus == 1); +const getConnectedRobot = (robotList = props.robotList) => { + return robotList.find(item => item?.connectStatus == 1) +} + +const getMapIdentity = (robotList = props.robotList) => { + const robot = getConnectedRobot(robotList) + return robot ? `${robot.id ?? ''}:${robot.robotMapName ?? ''}` : '' +} + +const loadSourceMap = async (connectedRobot = getConnectedRobot()) => { if (!connectedRobot) return; + const currentLoadVersion = ++mapLoadVersion; + const res = await getMapJson({ robotId: connectedRobot.id, mapName: connectedRobot.robotMapName }); + if (currentLoadVersion !== mapLoadVersion) return; if (res.code === 200) { try { const rawJsonObject = JSON.parse(res.data); // 解析 JSON @@ -373,20 +382,6 @@ const loadSourceMap = async () => { } }; -/** - * - * @param id 机器人编号 - * @param name 机器人名称 - * @param angleRad 角度 - * @param color 颜色 - */ -const addRobot = (id, name,x, y, angleRad, color) => { - const map = applicationState.parsedMap; - // 放置于地图中央附近 - const robot = new MobileRobot(id, name, x, y, angleRad, color); - applicationState.robots.push(robot); -} - const parseRobotPosition = (currentPosition) => { if (!currentPosition) return null const values = String(currentPosition).split(',').map(Number) @@ -394,60 +389,58 @@ const parseRobotPosition = (currentPosition) => { return values } -const moveRobot = (robot, dx, dy, dtheta = 0) => { - robot.x += dx; - robot.y += dy; - robot.angle += dtheta; +const syncRobots = (robotList) => { + const previousRobots = new Map(applicationState.robots.map(robot => [robot.id, robot])); + const nextRobots = []; + + robotList.forEach(item => { + const position = parseRobotPosition(item.currentPosition) + if (!position) return + + const [x, y, angle] = position + const robot = previousRobots.get(item.id) + if (robot) { + robot.name = item.robotName + robot.x = x + robot.y = y + robot.angle = angle + nextRobots.push(robot) + return + } + + nextRobots.push(new MobileRobot( + item.id, + item.robotName, + x, + y, + angle, + colorList[nextRobots.length % colorList.length] + )) + }) + + applicationState.robots = nextRobots } onMounted(() => { init() - if (props.robotList.length > 0) { - loadSourceMap() - requestAnimationFrame(renderFrame); - window.addEventListener('resize', resizeCanvasToContainer); // 监听窗口大小变化 - resizeCanvasToContainer(); // 初始调用一次 - setTimeout(() => { - bindEvent() - props.robotList.forEach(item => { - const position = parseRobotPosition(item.currentPosition) - if (!position) return - const [x, y, angle] = position - addRobot(item.id, item.robotName, x, y, angle, colorList[applicationState.robots.length % colorList.length]) - }) - }, 300) - } + window.addEventListener('resize', resizeCanvasToContainer); + resizeCanvasToContainer(); + bindEvent() + syncRobots(props.robotList) + loadSourceMap() + animationFrameId = requestAnimationFrame(renderFrame); }) const colorList = ['#00D4FF', '#FFB300', '#00FF88', '#ff5100', '#fff', '#15ff00', '#ff00d4', '#00d4ff'] -watch(() => props.robotList, - (newVal) => { - if (newVal.length > 0) { - if (props.clearMap) { - applicationState.robots = [] - } - loadSourceMap() +// 轮询只同步机器人实例,不重新请求、解析或重置地图。 +watch(() => props.robotList, syncRobots) - const ids = applicationState.robots.map(r => r.id); - props.robotList.forEach(item => { - const position = parseRobotPosition(item.currentPosition) - if (!position) return - const [x, y, angle] = position - if (!ids.includes(item.id)) { - addRobot(item.id, item.robotName, x, y, angle, colorList[applicationState.robots.length % colorList.length]) - } else { - applicationState.robots.forEach(robot => { - if (robot.id === item.id) { - // moveRobot(robot, parseFloat(x) - robot.x, parseFloat(y) - robot.y, parseFloat(angle) - robot.angle) - robot.x = x - robot.y = y - robot.angle = angle - } - }) - } - }) - } +// 只有用于加载地图的机器人或地图名称改变时,才重新加载底图。 +watch( + () => getMapIdentity(props.robotList), + (newIdentity, oldIdentity) => { + if (newIdentity && newIdentity !== oldIdentity) loadSourceMap() } ) @@ -458,6 +451,8 @@ defineExpose({ onUnmounted(() => { window.removeEventListener('resize', resizeCanvasToContainer) + cancelAnimationFrame(animationFrameId) + mapLoadVersion += 1 canvasCtx = null }) From 544711cd73e041fe2726908fe0acfeb0d2b48d6e Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Thu, 6 Aug 2026 14:48:28 +0800 Subject: [PATCH 32/96] =?UTF-8?q?feat:=20=E8=AF=95=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E4=B8=8D=E9=9C=80=E8=A6=81=E8=A6=81=E6=9C=89=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E4=B8=BA=E8=BF=9E=E6=8E=A5=E7=9A=84=EF=BC=8C?= =?UTF-8?q?=E5=9B=A0=E4=B8=BA=E5=8F=AF=E4=BB=A5=E9=80=89=E6=8B=A9=E6=9C=AA?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E7=9A=84=E6=9C=BA=E5=99=A8=E4=BA=BA=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E8=AF=95=E8=BF=90=E8=A1=8C=E6=9D=A5=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/flow/components/TestRun.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/views/flow/components/TestRun.vue b/src/views/flow/components/TestRun.vue index dc5b567..47daa2c 100644 --- a/src/views/flow/components/TestRun.vue +++ b/src/views/flow/components/TestRun.vue @@ -209,7 +209,9 @@ const handleRobotChange = async (robotId) => { /** 获取在线机器人列表 */ const loadRobotOptions = async () => { - const res = await getRobotList({ pageNum: 1, pageSize: 1000, connectStatus: '1' }) + // 试运行不需要要有连接状态为连接的,因为可以选择未连接的机器人进行试运行来保存流程数据 + // , connectStatus: '1' + const res = await getRobotList({ pageNum: 1, pageSize: 1000 }) if (res.code === 200) { robotIdOptions.value = res.rows?.map((item) => { return { From 5fb3bf4e2007a07315c0011b8b7e5372c8f0c395 Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Thu, 6 Aug 2026 15:10:47 +0800 Subject: [PATCH 33/96] =?UTF-8?q?feat:=20=E5=85=8B=E9=9A=86=E8=8A=82?= =?UTF-8?q?=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/test/configs/index.vue | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/views/test/configs/index.vue b/src/views/test/configs/index.vue index ad0264b..769e606 100644 --- a/src/views/test/configs/index.vue +++ b/src/views/test/configs/index.vue @@ -227,13 +227,15 @@
待检测项
+ diff --git a/src/views/inspection/runTask/index.vue b/src/views/inspection/runTask/index.vue index 55f7ece..61651ea 100644 --- a/src/views/inspection/runTask/index.vue +++ b/src/views/inspection/runTask/index.vue @@ -20,7 +20,7 @@ @click="removeTask(task)" />
-
机器人:{{ task.robotName }}
+
主要机器人:{{ task.robotName || '执行时安排' }}
@@ -44,7 +44,7 @@
ID: {{ runTask.taskId }}
-
机器人:{{ runTask.robotName }}
+
主要机器人:{{ runTask.robotName || '按流程角色安排' }}
开始时间: {{ formatDate(runTask.startTime) }}
@@ -84,7 +84,9 @@ - + + +