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 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 {

View File

@ -11,7 +11,7 @@
</div>
<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>
@ -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 {

View File

@ -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
})
</script>

View File

@ -13,8 +13,8 @@
<SvgIcon name="bot" :color="colorList[index]" />
</div>
<div>
<div class="bot-name">机器人#0{{ index + 1}}</div>
<div class="bot-local">厂区A-在线</div>
<div class="bot-name">{{ item.robotName }}</div>
<div class="bot-local" :class="agvRuntimeStatusClass(item.status)">{{ agvRuntimeStatusLabel(item.status) }}</div>
</div>
</div>
</div>
@ -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 {

View File

@ -122,7 +122,7 @@
<div class="task-title">待选择点位</div>
<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 }">
<div class="list-group-item">
<el-icon>
@ -138,10 +138,11 @@
<div style="height: 55vh">
<div class="task-title">点位排序</div>
<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 }">
<div class="list-group-item">
{{ index + 1 }} {{ element.waypointName }}
<el-icon color="red" @click="removeFromB(element.id)" style="margin-left: 10px;"><Delete /></el-icon>
</div>
</template>
</draggable>
@ -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();

View File

@ -227,13 +227,14 @@
<el-col :span="12">
<div style="height: 55vh">
<div class="task-title">待检测项</div>
<!-- group="people" -->
<draggable
:animation="340"
:forceFallback="true"
:list="taskLeftList"
class="draggable-group"
ghostClass="dragClass"
group="people"
:group="{ name: 'people', pull: 'clone', put: false}"
itemKey="name"
>
<template #item="{ element, index }">
@ -256,12 +257,13 @@
:list="taskRightList"
class="draggable-group"
ghostClass="dragClass"
group="people"
:group="{ name: 'people', pull: false, put: true }"
itemKey="name"
>
<template #item="{ element, index }">
<div class="list-group-item">
{{ index + 1 }} {{ element.name }}
<el-icon color="red" @click="removeFromB(element.id)" style="margin-left: 10px;"><Delete /></el-icon>
</div>
</template>
</draggable>
@ -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();