Merge remote-tracking branch 'origin/lxl-dev' into lxl-dev

This commit is contained in:
lixiaolong 2026-08-07 10:48:57 +08:00
commit e6d379f571
6 changed files with 174 additions and 72 deletions

View File

@ -209,7 +209,9 @@ const handleRobotChange = async (robotId) => {
/** 获取在线机器人列表 */ /** 获取在线机器人列表 */
const loadRobotOptions = async () => { 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) { if (res.code === 200) {
robotIdOptions.value = res.rows?.map((item) => { robotIdOptions.value = res.rows?.map((item) => {
return { return {

View File

@ -11,7 +11,7 @@
</div> </div>
<div> <div>
<div class="bot-name">{{ item.robotName }}</div> <div class="bot-name">{{ item.robotName }}</div>
<div class="bot-local">{{ agvRuntimeStatusLabel(item.status) }}</div> <div class="bot-local" :class="agvRuntimeStatusClass(item.status)">{{ agvRuntimeStatusLabel(item.status) }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -134,7 +134,7 @@ import { getJointStateApi, moveJApi, torqueOnApi, speedJApi, stopMotionApi, clea
import { Remove, CirclePlus } from '@element-plus/icons-vue' import { Remove, CirclePlus } from '@element-plus/icons-vue'
import VoiceConversation from "./VoiceConversation.vue"; import VoiceConversation from "./VoiceConversation.vue";
import IPlayer from "@/components/IPlayer/index.vue"; import IPlayer from "@/components/IPlayer/index.vue";
import { agvRuntimeStatusLabel } from '@/utils/agvRuntimeStatus' import { agvRuntimeStatusClass, agvRuntimeStatusLabel } from '@/utils/agvRuntimeStatus'
const robotList = ref([]) const robotList = ref([])
const robotDevices = ref([]) const robotDevices = ref([])
@ -310,7 +310,7 @@ onMounted(() => {
fetchRobotList() fetchRobotList()
robotRefreshTimer = window.setInterval(() => { robotRefreshTimer = window.setInterval(() => {
if (!document.hidden) fetchRobotList(false) if (!document.hidden) fetchRobotList(false)
}, 5000) }, 500)
}) })
onUnmounted(() => { onUnmounted(() => {
@ -353,10 +353,10 @@ onUnmounted(() => {
background: #0F1E38; background: #0F1E38;
height: 56px; height: 56px;
border-radius: 8px; border-radius: 8px;
padding: 16px; padding: 12px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 20px; gap: 12px;
margin-bottom: 16px; margin-bottom: 16px;
cursor: pointer; cursor: pointer;
border: 1px solid transparent; border: 1px solid transparent;
@ -367,10 +367,34 @@ onUnmounted(() => {
} }
.bot-local { .bot-local {
color: #00d4ff;
font-size: 11px; font-size: 11px;
margin-top: 4px; margin-top: 4px;
} }
.online {
color: #0f8;
}
.unknown,
.offline,
.stopped {
color: #cbd5e1;
}
.manual,
.running {
color: #00d4ff;
}
.charging,
.paused {
color: #ffb300;
}
.fault,
.emergency {
color: #ff6b6d;
}
} }
.active { .active {

View File

@ -73,6 +73,11 @@ const applicationState = {
let mapCanvas; // DOM let mapCanvas; // DOM
let canvasCtx; // 2D let canvasCtx; // 2D
// ID Canvas
let animationFrameId;
//
let mapLoadVersion = 0;
const init = () => { const init = () => {
mapCanvas = document.getElementById('map-canvas'); mapCanvas = document.getElementById('map-canvas');
canvasCtx = mapCanvas.getContext('2d'); canvasCtx = mapCanvas.getContext('2d');
@ -186,7 +191,7 @@ function renderFrame(timestamp) {
// //
if (!applicationState.isMapLoaded) { if (!applicationState.isMapLoaded) {
requestAnimationFrame(renderFrame); // animationFrameId = requestAnimationFrame(renderFrame); //
return; return;
} }
@ -236,7 +241,7 @@ function renderFrame(timestamp) {
drawMapBoundary(canvasCtx, map); drawMapBoundary(canvasCtx, map);
// //
requestAnimationFrame(renderFrame); animationFrameId = requestAnimationFrame(renderFrame);
} }
const drawAllRobots = (ctx) => { const drawAllRobots = (ctx) => {
@ -349,14 +354,40 @@ const centerCanvasView = () => {
inspectionStore.camera.centerY = (map.originY + map.maxY) / 2; 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; if (!connectedRobot) return;
//
const currentLoadVersion = ++mapLoadVersion;
const res = await getMapJson({ const res = await getMapJson({
robotId: connectedRobot.id, robotId: connectedRobot.id,
mapName: connectedRobot.robotMapName mapName: connectedRobot.robotMapName
}); });
//
if (currentLoadVersion !== mapLoadVersion) return;
if (res.code === 200) { if (res.code === 200) {
try { try {
const rawJsonObject = JSON.parse(res.data); // JSON const rawJsonObject = JSON.parse(res.data); // JSON
@ -369,19 +400,9 @@ const loadSourceMap = async () => {
}; };
/** /**
* * 将接口返回的 "x,y,angle" 字符串转换为数值坐标
* @param id 机器人编号 * 无位置或包含非法数值时返回 null避免无效机器人进入绘制列表
* @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) => { const parseRobotPosition = (currentPosition) => {
if (!currentPosition) return null if (!currentPosition) return null
const values = String(currentPosition).split(',').map(Number) const values = String(currentPosition).split(',').map(Number)
@ -389,55 +410,69 @@ const parseRobotPosition = (currentPosition) => {
return values return values
} }
const moveRobot = (robot, dx, dy, dtheta = 0) => { /**
robot.x += dx; * 将最新机器人列表同步到 Canvas 使用的机器人实例中
robot.y += dy; *
robot.angle += dtheta; * 已存在的机器人会复用原 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(() => { onMounted(() => {
init() init()
if (props.robotList.length > 0) { window.addEventListener('resize', resizeCanvasToContainer);
loadSourceMap() resizeCanvasToContainer();
requestAnimationFrame(renderFrame);
window.addEventListener('resize', resizeCanvasToContainer); //
resizeCanvasToContainer(); //
setTimeout(() => {
bindEvent() bindEvent()
props.robotList.forEach(item => { syncRobots(props.robotList)
const position = parseRobotPosition(item.currentPosition) loadSourceMap()
if (!position) return animationFrameId = requestAnimationFrame(renderFrame);
const [x, y, angle] = position
addRobot(item.id, item.robotName, x, y, angle, colorList[applicationState.robots.length % colorList.length])
})
}, 300)
}
}) })
const colorList = ['#00D4FF', '#FFB300', '#00FF88', '#ff5100', '#fff', '#15ff00', '#ff00d4', '#00d4ff'] const colorList = ['#00D4FF', '#FFB300', '#00FF88', '#ff5100', '#fff', '#15ff00', '#ff00d4', '#00d4ff']
watch(() => props.robotList, //
(newVal) => { watch(() => props.robotList, syncRobots)
if (newVal.length > 0) {
const ids = applicationState.robots.map(r => r.id); //
props.robotList.forEach(item => { watch(
const position = parseRobotPosition(item.currentPosition) () => getMapIdentity(props.robotList),
if (!position) return (newIdentity, oldIdentity) => {
const [x, y, angle] = position if (newIdentity && newIdentity !== oldIdentity) loadSourceMap()
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
}
})
}
})
}
} }
) )
@ -448,6 +483,9 @@ defineExpose({
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('resize', resizeCanvasToContainer) window.removeEventListener('resize', resizeCanvasToContainer)
// 使
cancelAnimationFrame(animationFrameId)
mapLoadVersion += 1
canvasCtx = null canvasCtx = null
}) })
</script> </script>

View File

@ -13,8 +13,8 @@
<SvgIcon name="bot" :color="colorList[index]" /> <SvgIcon name="bot" :color="colorList[index]" />
</div> </div>
<div> <div>
<div class="bot-name">机器人#0{{ index + 1}}</div> <div class="bot-name">{{ item.robotName }}</div>
<div class="bot-local">厂区A-在线</div> <div class="bot-local" :class="agvRuntimeStatusClass(item.status)">{{ agvRuntimeStatusLabel(item.status) }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -42,6 +42,7 @@
import SvgIcon from "@/components/SvgIcon"; import SvgIcon from "@/components/SvgIcon";
import IPlayer from "@/components/IPlayer/index.vue"; import IPlayer from "@/components/IPlayer/index.vue";
import { getRobotList } from '@/api/inspection/robot' import { getRobotList } from '@/api/inspection/robot'
import { agvRuntimeStatusClass, agvRuntimeStatusLabel } from '@/utils/agvRuntimeStatus'
const videoUrl = ref('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8') const videoUrl = ref('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8')
@ -112,10 +113,34 @@ onMounted(() => {
} }
.bot-local { .bot-local {
color: #00d4ff;
font-size: 11px; font-size: 11px;
margin-top: 4px; margin-top: 4px;
} }
.online {
color: #0f8;
}
.unknown,
.offline,
.stopped {
color: #cbd5e1;
}
.manual,
.running {
color: #00d4ff;
}
.charging,
.paused {
color: #ffb300;
}
.fault,
.emergency {
color: #ff6b6d;
}
} }
.active { .active {

View File

@ -122,7 +122,7 @@
<div class="task-title">待选择点位</div> <div class="task-title">待选择点位</div>
<draggable :animation="340" :forceFallback="true" :list="taskLeftList" class="draggable-group" <draggable :animation="340" :forceFallback="true" :list="taskLeftList" class="draggable-group"
ghostClass="dragClass" group="people" itemKey="name"> ghostClass="dragClass" :group="{ name: 'people', pull: 'clone', put: false}" itemKey="name">
<template #item="{ element, index }"> <template #item="{ element, index }">
<div class="list-group-item"> <div class="list-group-item">
<el-icon> <el-icon>
@ -138,10 +138,11 @@
<div style="height: 55vh"> <div style="height: 55vh">
<div class="task-title">点位排序</div> <div class="task-title">点位排序</div>
<draggable :animation="340" :forceFallback="true" :list="taskRightList" class="draggable-group" <draggable :animation="340" :forceFallback="true" :list="taskRightList" class="draggable-group"
ghostClass="dragClass" group="people" itemKey="name"> ghostClass="dragClass" :group="{ name: 'people', pull: false, put: true }" itemKey="name">
<template #item="{ element, index }"> <template #item="{ element, index }">
<div class="list-group-item"> <div class="list-group-item">
{{ index + 1 }} {{ element.waypointName }} {{ index + 1 }} {{ element.waypointName }}
<el-icon color="red" @click="removeFromB(element.id)" style="margin-left: 10px;"><Delete /></el-icon>
</div> </div>
</template> </template>
</draggable> </draggable>
@ -377,6 +378,11 @@ const cancelPoint = () => {
dialogVisible.value = false; dialogVisible.value = false;
}; };
const removeFromB = (id) => {
const index = taskRightList.value.findIndex(item => item.id === id)
if (index !== -1) taskRightList.value.splice(index, 1)
}
onMounted(() => { onMounted(() => {
getList(); getList();
getMapArr(); getMapArr();

View File

@ -227,13 +227,14 @@
<el-col :span="12"> <el-col :span="12">
<div style="height: 55vh"> <div style="height: 55vh">
<div class="task-title">待检测项</div> <div class="task-title">待检测项</div>
<!-- group="people" -->
<draggable <draggable
:animation="340" :animation="340"
:forceFallback="true" :forceFallback="true"
:list="taskLeftList" :list="taskLeftList"
class="draggable-group" class="draggable-group"
ghostClass="dragClass" ghostClass="dragClass"
group="people" :group="{ name: 'people', pull: 'clone', put: false}"
itemKey="name" itemKey="name"
> >
<template #item="{ element, index }"> <template #item="{ element, index }">
@ -256,12 +257,13 @@
:list="taskRightList" :list="taskRightList"
class="draggable-group" class="draggable-group"
ghostClass="dragClass" ghostClass="dragClass"
group="people" :group="{ name: 'people', pull: false, put: true }"
itemKey="name" itemKey="name"
> >
<template #item="{ element, index }"> <template #item="{ element, index }">
<div class="list-group-item"> <div class="list-group-item">
{{ index + 1 }} {{ element.name }} {{ index + 1 }} {{ element.name }}
<el-icon color="red" @click="removeFromB(element.id)" style="margin-left: 10px;"><Delete /></el-icon>
</div> </div>
</template> </template>
</draggable> </draggable>
@ -347,7 +349,7 @@ import { flowExecute } from "@/api/flow/flow";
import TableSearch from "@/components/TableSearch/index.vue"; import TableSearch from "@/components/TableSearch/index.vue";
import draggable from "vuedraggable"; import draggable from "vuedraggable";
import { listDetect } from "@/api/test/detect.js"; 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 { listGroup } from "@/api/system/group.js";
import { listTerminal } from "@/api/device/terminal.js"; import { listTerminal } from "@/api/device/terminal.js";
import Run from "./Run.vue"; 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(() => { onMounted(() => {
getList(); getList();
handleGetSystemGroup(); handleGetSystemGroup();