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 3/6] =?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 4/6] =?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 @@
待检测项
+
@@ -256,7 +258,7 @@
:list="taskRightList"
class="draggable-group"
ghostClass="dragClass"
- group="people"
+ :group="{ name: 'people', pull: false, put: true }"
itemKey="name"
>
From 1b8e2df14509b155d85cfab9ecb3556de0f7e1f6 Mon Sep 17 00:00:00 2001
From: zhanghao <774378400@qq.com>
Date: Thu, 6 Aug 2026 15:40:32 +0800
Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=E6=A3=80=E6=B5=8B=E9=A1=B9?=
=?UTF-8?q?=E6=8B=96=E5=8A=A8=E8=B0=83=E6=95=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/views/inspection/task/index.vue | 10 ++++++++--
src/views/test/configs/index.vue | 11 ++++++++---
2 files changed, 16 insertions(+), 5 deletions(-)
diff --git a/src/views/inspection/task/index.vue b/src/views/inspection/task/index.vue
index b70b0e9..d77b916 100644
--- a/src/views/inspection/task/index.vue
+++ b/src/views/inspection/task/index.vue
@@ -122,7 +122,7 @@
待选择点位
+ ghostClass="dragClass" :group="{ name: 'people', pull: 'clone', put: false}" itemKey="name">
@@ -138,10 +138,11 @@
点位排序
+ ghostClass="dragClass" :group="{ name: 'people', pull: false, put: true }" itemKey="name">
{{ index + 1 }} 、{{ element.waypointName }}
+
@@ -377,6 +378,11 @@ const cancelPoint = () => {
dialogVisible.value = false;
};
+const removeFromB = (id) => {
+ const index = taskRightList.value.findIndex(item => item.id === id)
+ if (index !== -1) taskRightList.value.splice(index, 1)
+}
+
onMounted(() => {
getList();
getMapArr();
diff --git a/src/views/test/configs/index.vue b/src/views/test/configs/index.vue
index 769e606..589e5f1 100644
--- a/src/views/test/configs/index.vue
+++ b/src/views/test/configs/index.vue
@@ -234,8 +234,7 @@
:list="taskLeftList"
class="draggable-group"
ghostClass="dragClass"
-
- :group="{ name: 'people', pull: 'clone', put: true }"
+ :group="{ name: 'people', pull: 'clone', put: false}"
itemKey="name"
>
@@ -264,6 +263,7 @@
{{ index + 1 }} 、{{ element.name }}
+
@@ -349,7 +349,7 @@ import { flowExecute } from "@/api/flow/flow";
import TableSearch from "@/components/TableSearch/index.vue";
import draggable from "vuedraggable";
import { listDetect } from "@/api/test/detect.js";
-import { Tools } from "@element-plus/icons-vue";
+import { Tools, Delete } from "@element-plus/icons-vue";
import { listGroup } from "@/api/system/group.js";
import { listTerminal } from "@/api/device/terminal.js";
import Run from "./Run.vue";
@@ -679,6 +679,11 @@ function handleGetTerminalGroup(row) {
});
}
+const removeFromB = (id) => {
+ const index = taskRightList.value.findIndex(item => item.id === id)
+ if (index !== -1) taskRightList.value.splice(index, 1)
+}
+
onMounted(() => {
getList();
handleGetSystemGroup();
From 80583a526d883d8c9e59c7854ba44b64088eb241 Mon Sep 17 00:00:00 2001
From: zhanghao <774378400@qq.com>
Date: Thu, 6 Aug 2026 16:50:12 +0800
Subject: [PATCH 6/6] =?UTF-8?q?doc:=20=E6=B3=A8=E9=87=8A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../cockpit/components/MapCanvas.vue | 33 +++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/src/views/inspection/cockpit/components/MapCanvas.vue b/src/views/inspection/cockpit/components/MapCanvas.vue
index fc29cc2..0d943d6 100644
--- a/src/views/inspection/cockpit/components/MapCanvas.vue
+++ b/src/views/inspection/cockpit/components/MapCanvas.vue
@@ -73,7 +73,9 @@ const applicationState = {
let mapCanvas; // 主渲染画布 DOM 元素
let canvasCtx; // 2D 渲染上下文
+// 保存当前动画帧 ID,组件卸载时用于停止 Canvas 渲染循环。
let animationFrameId;
+// 地图请求版本号:当多个地图请求并发时,只允许最后一次请求更新画布。
let mapLoadVersion = 0;
const init = () => {
@@ -352,24 +354,39 @@ const centerCanvasView = () => {
inspectionStore.camera.centerY = (map.originY + map.maxY) / 2;
}
+/**
+ * 从机器人列表中选出地图数据来源。
+ * 当前沿用原有规则:使用第一台已连接的机器人获取地图。
+ */
const getConnectedRobot = (robotList = props.robotList) => {
return robotList.find(item => item?.connectStatus == 1)
}
+/**
+ * 生成地图来源的稳定标识。
+ * 机器人轮询时位置等字段会不断变化,但机器人 ID 和地图名称不变,
+ * 因此监听该标识可以避免位置更新触发底图重新加载。
+ */
const getMapIdentity = (robotList = props.robotList) => {
const robot = getConnectedRobot(robotList)
return robot ? `${robot.id ?? ''}:${robot.robotMapName ?? ''}` : ''
}
+/**
+ * 请求并加载机器人当前使用的地图。
+ * @param {Object} connectedRobot 用于提供地图的已连接机器人
+ */
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 {
@@ -382,6 +399,10 @@ const loadSourceMap = async (connectedRobot = getConnectedRobot()) => {
}
};
+/**
+ * 将接口返回的 "x,y,angle" 字符串转换为数值坐标。
+ * 无位置或包含非法数值时返回 null,避免无效机器人进入绘制列表。
+ */
const parseRobotPosition = (currentPosition) => {
if (!currentPosition) return null
const values = String(currentPosition).split(',').map(Number)
@@ -389,7 +410,15 @@ const parseRobotPosition = (currentPosition) => {
return values
}
+/**
+ * 将最新机器人列表同步到 Canvas 使用的机器人实例中。
+ *
+ * 已存在的机器人会复用原 MobileRobot 实例,仅修改位置和名称;新增机器人
+ * 创建实例,接口中已移除或没有有效位置的机器人不会进入 nextRobots。
+ * 该函数不会加载地图或重置相机,因此可安全用于高频轮询。
+ */
const syncRobots = (robotList) => {
+ // 用 ID 建立索引,避免为每台机器人遍历整个旧列表。
const previousRobots = new Map(applicationState.robots.map(robot => [robot.id, robot]));
const nextRobots = [];
@@ -400,6 +429,7 @@ const syncRobots = (robotList) => {
const [x, y, angle] = position
const robot = previousRobots.get(item.id)
if (robot) {
+ // 保留原实例和颜色,只更新轮询产生的动态信息。
robot.name = item.robotName
robot.x = x
robot.y = y
@@ -408,6 +438,7 @@ const syncRobots = (robotList) => {
return
}
+ // 列表中首次出现的机器人需要创建绘制实例并分配颜色。
nextRobots.push(new MobileRobot(
item.id,
item.robotName,
@@ -418,6 +449,7 @@ const syncRobots = (robotList) => {
))
})
+ // 整体替换绘制列表,同时自然移除本轮接口中不存在的机器人。
applicationState.robots = nextRobots
}
@@ -451,6 +483,7 @@ defineExpose({
onUnmounted(() => {
window.removeEventListener('resize', resizeCanvasToContainer)
+ // 停止渲染循环,并使仍在进行中的地图请求全部失效。
cancelAnimationFrame(animationFrameId)
mapLoadVersion += 1
canvasCtx = null