Merge remote-tracking branch 'origin/lxl-dev' into lxl-dev
This commit is contained in:
commit
e6d379f571
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user