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 {
diff --git a/src/views/inspection/cockpit/components/ManualTakeover/index.vue b/src/views/inspection/cockpit/components/ManualTakeover/index.vue
index 4cbb293..d78e55e 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) }}
@@ -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..0d943d6 100644
--- a/src/views/inspection/cockpit/components/MapCanvas.vue
+++ b/src/views/inspection/cockpit/components/MapCanvas.vue
@@ -73,6 +73,11 @@ const applicationState = {
let mapCanvas; // 主渲染画布 DOM 元素
let canvasCtx; // 2D 渲染上下文
+// 保存当前动画帧 ID,组件卸载时用于停止 Canvas 渲染循环。
+let animationFrameId;
+// 地图请求版本号:当多个地图请求并发时,只允许最后一次请求更新画布。
+let mapLoadVersion = 0;
+
const init = () => {
mapCanvas = document.getElementById('map-canvas');
canvasCtx = mapCanvas.getContext('2d');
@@ -186,7 +191,7 @@ function renderFrame(timestamp) {
// 如果地图未加载,跳过绘制
if (!applicationState.isMapLoaded) {
- requestAnimationFrame(renderFrame); // 继续请求下一帧
+ animationFrameId = requestAnimationFrame(renderFrame); // 继续请求下一帧
return;
}
@@ -236,7 +241,7 @@ function renderFrame(timestamp) {
drawMapBoundary(canvasCtx, map);
// 请求下一帧渲染
- requestAnimationFrame(renderFrame);
+ animationFrameId = requestAnimationFrame(renderFrame);
}
const drawAllRobots = (ctx) => {
@@ -349,14 +354,40 @@ 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)
+}
+
+/**
+ * 生成地图来源的稳定标识。
+ * 机器人轮询时位置等字段会不断变化,但机器人 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 {
const rawJsonObject = JSON.parse(res.data); // 解析 JSON
@@ -369,19 +400,9 @@ const loadSourceMap = async () => {
};
/**
- *
- * @param id 机器人编号
- * @param name 机器人名称
- * @param angleRad 角度
- * @param color 颜色
+ * 将接口返回的 "x,y,angle" 字符串转换为数值坐标。
+ * 无位置或包含非法数值时返回 null,避免无效机器人进入绘制列表。
*/
-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)
@@ -389,55 +410,69 @@ const parseRobotPosition = (currentPosition) => {
return values
}
-const moveRobot = (robot, dx, dy, dtheta = 0) => {
- robot.x += dx;
- robot.y += dy;
- robot.angle += dtheta;
+/**
+ * 将最新机器人列表同步到 Canvas 使用的机器人实例中。
+ *
+ * 已存在的机器人会复用原 MobileRobot 实例,仅修改位置和名称;新增机器人
+ * 创建实例,接口中已移除或没有有效位置的机器人不会进入 nextRobots。
+ * 该函数不会加载地图或重置相机,因此可安全用于高频轮询。
+ */
+const syncRobots = (robotList) => {
+ // 用 ID 建立索引,避免为每台机器人遍历整个旧列表。
+ 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) {
- 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(() => props.robotList, syncRobots)
+
+// 只有用于加载地图的机器人或地图名称改变时,才重新加载底图。
+watch(
+ () => getMapIdentity(props.robotList),
+ (newIdentity, oldIdentity) => {
+ if (newIdentity && newIdentity !== oldIdentity) loadSourceMap()
}
)
@@ -448,6 +483,9 @@ defineExpose({
onUnmounted(() => {
window.removeEventListener('resize', resizeCanvasToContainer)
+ // 停止渲染循环,并使仍在进行中的地图请求全部失效。
+ cancelAnimationFrame(animationFrameId)
+ mapLoadVersion += 1
canvasCtx = null
})
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 {
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 ad0264b..589e5f1 100644
--- a/src/views/test/configs/index.vue
+++ b/src/views/test/configs/index.vue
@@ -227,13 +227,14 @@
待检测项
+
@@ -256,12 +257,13 @@
:list="taskRightList"
class="draggable-group"
ghostClass="dragClass"
- group="people"
+ :group="{ name: 'people', pull: false, put: true }"
itemKey="name"
>
{{ index + 1 }} 、{{ element.name }}
+
@@ -347,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";
@@ -677,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();