Compare commits

...

3 Commits

Author SHA1 Message Date
b332a5461e feat(utils): 添加AGV运行状态工具函数
- 定义AGV运行状态常量映射,包含未知、离线、在线、手动模式等状态
- 实现状态解析函数resolveStatus,支持数字类型转换
- 提供状态标签、类型和CSS类名的导出函数
- 支持11种不同的AGV运行状态及其对应的显示样式
- 添加默认未知状态兜底处理机制
2026-08-06 11:24:50 +08:00
02706a39a1 Merge remote-tracking branch 'origin/lxl-dev' into lxl-dev
# Conflicts:
#	src/views/inspection/cockpit/components/MapOverview.vue
2026-08-06 11:24:14 +08:00
e18c4b88be refactor(inspection): 统一AGV运行状态管理并优化机器人列表刷新逻辑
- 将多个组件中的硬编码机器人状态映射替换为统一的 agvRuntimeStatus 工具函数
- 在手动接管和地图概览组件中实现5秒定时刷新机器人列表功能
- 添加 onUnmounted 钩子清理定时器避免内存泄漏
- 重构 MapCanvas 中的机器人位置解析逻辑并添加数据验证
- 为地图概览组件的状态显示添加动态CSS类名支持
2026-08-06 11:23:19 +08:00
5 changed files with 94 additions and 91 deletions

View File

@ -0,0 +1,18 @@
const AGV_RUNTIME_STATUS = {
0: { label: '未知', type: 'info', className: 'unknown' },
1: { label: '已断开', type: 'info', className: 'offline' },
2: { label: '空闲', type: 'success', className: 'online' },
3: { label: '手动模式', type: 'primary', className: 'manual' },
4: { label: '自动运行', type: 'primary', className: 'running' },
5: { label: '充电中', type: 'warning', className: 'charging' },
6: { label: '已暂停', type: 'warning', className: 'paused' },
7: { label: '已停止', type: 'info', className: 'stopped' },
8: { label: '故障', type: 'danger', className: 'fault' },
9: { label: '急停', type: 'danger', className: 'emergency' },
}
const resolveStatus = value => AGV_RUNTIME_STATUS[Number(value)] || AGV_RUNTIME_STATUS[0]
export const agvRuntimeStatusLabel = value => resolveStatus(value).label
export const agvRuntimeStatusType = value => resolveStatus(value).type
export const agvRuntimeStatusClass = value => resolveStatus(value).className

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">{{ robotStatus[item.status] }}</div> <div class="bot-local">{{ agvRuntimeStatusLabel(item.status) }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -122,7 +122,7 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed, onMounted, nextTick } from "vue"; import { computed, onMounted, onUnmounted, nextTick } from "vue";
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import MapCanvas from "../MapCanvas.vue"; import MapCanvas from "../MapCanvas.vue";
import SvgIcon from "@/components/SvgIcon"; import SvgIcon from "@/components/SvgIcon";
@ -134,6 +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'
const robotList = ref([]) const robotList = ref([])
const robotDevices = ref([]) const robotDevices = ref([])
@ -147,15 +148,18 @@ const loadRobotDevices = async (robotId) => {
robotDevices.value = Array.isArray(response.data) ? response.data : [] robotDevices.value = Array.isArray(response.data) ? response.data : []
} }
const fetchRobotList = async () => { const fetchRobotList = async (refreshDevices = true) => {
try { try {
const res = await getRobotList({ const res = await getRobotList({
robotType: 2, robotType: 2,
}) })
if (res.code === 200) { if (res.code === 200) {
const currentRobotId = activeBotData.value.robotId
robotList.value = res.rows robotList.value = res.rows
activeBotData.value = robotList.value[0] || {} const currentIndex = robotList.value.findIndex(item => item.robotId === currentRobotId)
await loadRobotDevices(activeBotData.value.robotId) activeBot.value = currentIndex >= 0 ? currentIndex : 0
activeBotData.value = robotList.value[activeBot.value] || {}
if (refreshDevices) await loadRobotDevices(activeBotData.value.robotId)
} else { } else {
console.error('获取机器人列表失败:', res.message) console.error('获取机器人列表失败:', res.message)
} }
@ -164,13 +168,6 @@ const fetchRobotList = async () => {
} }
} }
const robotStatus = {
0: '在线',
1: '离线',
2: '充电中',
3: '巡检中'
}
const activeBot = ref(0) const activeBot = ref(0)
const activeBotData = ref({}) const activeBotData = ref({})
const changeBot = async (index) => { const changeBot = async (index) => {
@ -307,11 +304,17 @@ const stop = async () => {
const videoUrl = ref('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8') const videoUrl = ref('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8')
let robotRefreshTimer
onMounted(() => { onMounted(() => {
fetchRobotList() fetchRobotList()
robotRefreshTimer = window.setInterval(() => {
if (!document.hidden) fetchRobotList(false)
}, 5000)
}) })
onUnmounted(() => { onUnmounted(() => {
window.clearInterval(robotRefreshTimer)
if (animationId.value) { if (animationId.value) {
clearInterval(animationId.value) clearInterval(animationId.value)
} }

View File

@ -382,6 +382,13 @@ const addRobot = (id, name,x, y, angleRad, color) => {
applicationState.robots.push(robot); applicationState.robots.push(robot);
} }
const parseRobotPosition = (currentPosition) => {
if (!currentPosition) return null
const values = String(currentPosition).split(',').map(Number)
if (values.length < 3 || values.some(value => !Number.isFinite(value))) return null
return values
}
const moveRobot = (robot, dx, dy, dtheta = 0) => { const moveRobot = (robot, dx, dy, dtheta = 0) => {
robot.x += dx; robot.x += dx;
robot.y += dy; robot.y += dy;
@ -398,7 +405,9 @@ onMounted(() => {
setTimeout(() => { setTimeout(() => {
bindEvent() bindEvent()
props.robotList.forEach(item => { props.robotList.forEach(item => {
const [x, y, angle] = item.currentPosition.split(',') 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]) addRobot(item.id, item.robotName, x, y, angle, colorList[applicationState.robots.length % colorList.length])
}) })
}, 300) }, 300)
@ -412,13 +421,14 @@ watch(() => props.robotList,
if (newVal.length > 0) { if (newVal.length > 0) {
const ids = applicationState.robots.map(r => r.id); const ids = applicationState.robots.map(r => r.id);
props.robotList.forEach(item => { props.robotList.forEach(item => {
const position = parseRobotPosition(item.currentPosition)
if (!position) return
const [x, y, angle] = position
if (!ids.includes(item.id)) { if (!ids.includes(item.id)) {
const [x, y, angle] = item.currentPosition.split(',')
addRobot(item.id, item.robotName, x, y, angle, colorList[applicationState.robots.length % colorList.length]) addRobot(item.id, item.robotName, x, y, angle, colorList[applicationState.robots.length % colorList.length])
} else { } else {
applicationState.robots.forEach(robot => { applicationState.robots.forEach(robot => {
if (robot.id === item.id) { if (robot.id === item.id) {
const [x, y, angle] = item.currentPosition.split(',')
// moveRobot(robot, parseFloat(x) - robot.x, parseFloat(y) - robot.y, parseFloat(angle) - robot.angle) // moveRobot(robot, parseFloat(x) - robot.x, parseFloat(y) - robot.y, parseFloat(angle) - robot.angle)
robot.x = x robot.x = x
robot.y = y robot.y = y

View File

@ -7,7 +7,7 @@
<div class="title-box"> <div class="title-box">
<div><SvgIcon name="bot" :color="index === 0 ? '#00D4FF' : '#FFB300'" /></div> <div><SvgIcon name="bot" :color="index === 0 ? '#00D4FF' : '#FFB300'" /></div>
<div class="title">{{ item.robotName }}</div> <div class="title">{{ item.robotName }}</div>
<div class="state online">{{ robotStatus[item.status] }}</div> <div class="state" :class="agvRuntimeStatusClass(item.status)">{{ agvRuntimeStatusLabel(item.status) }}</div>
</div> </div>
<div class="info-box"> <div class="info-box">
<div class="item-box"> <div class="item-box">
@ -71,12 +71,9 @@
<!-- <div class="split-line"></div> --> <!-- <div class="split-line"></div> -->
<div class="alarm-container"> <div class="alarm-container">
<div class="container-title">告警记录</div> <div class="container-title">告警记录</div>
<div class="alarm-box" v-for="item, index in alarmRecord"> <div class="alarm-box" v-for="item, index in 2">
<div class="alarm-title" :class="item.resultStatus"> <div class="title">#01 检测到异常噪声</div>
<div class="title">{{ item.resultName }}</div> <div class="alarm-info">14:28:33 厂区A 东侧走廊</div>
<div class="status">{{ getOptionLabel(resultStatusOptions, item.resultStatus) }}</div>
</div>
<div class="alarm-info">{{ item.createTime }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -89,15 +86,9 @@ import MapCanvas from "./MapCanvas.vue";
import { onMounted, onUnmounted } from "vue"; import { onMounted, onUnmounted } from "vue";
import { getRobotList } from '@/api/inspection/robot' import { getRobotList } from '@/api/inspection/robot'
import { getRunTaskList } from '@/api/inspection/runTask' import { getRunTaskList } from '@/api/inspection/runTask'
import { getInspectionResultList } from '@/api/inspection/result' import { agvRuntimeStatusClass, agvRuntimeStatusLabel } from '@/utils/agvRuntimeStatus'
const robotList = ref([]) const robotList = ref([])
const robotStatus = {
"0": "在线",
"1": "离线",
"2": "充电中",
"3": "巡检中"
}
const getRobot = async () => { const getRobot = async () => {
let robotType let robotType
if (import.meta.env.VITE_INSPECTION_TYPE === 'inspection') { if (import.meta.env.VITE_INSPECTION_TYPE === 'inspection') {
@ -127,7 +118,7 @@ const taskStatus = {
} }
const getRunningTask = async () => { const getRunningTask = async () => {
let taskType let taskType
if (import.meta.env.VITE_INSPECTION_TYPE = 'inspection') { if (import.meta.env.VITE_INSPECTION_TYPE === 'inspection') {
taskType = '1' taskType = '1'
} else { } else {
taskType = '2' taskType = '2'
@ -163,31 +154,18 @@ const centerCanvasView = () => {
} }
} }
const alarmRecord = ref([]) let robotRefreshTimer
const resultStatusOptions = [
{ label: '待处理', value: 'PENDING' },
{ label: '正常', value: 'NORMAL' },
{ label: '异常', value: 'ABNORMAL' },
{ label: '识别失败', value: 'RECOGNIZE_FAILED' }
]
const getAlarmRecord = async () => {
const res = await getInspectionResultList({
pageNum: 1,
pageSize: 10
})
if (res.code === 200) {
alarmRecord.value = res.rows
}
}
const getOptionLabel = (options, value) => {
return options.find(item => item.value === value)?.label || value || '-'
}
onMounted(() => { onMounted(() => {
getRobot() getRobot()
getRunningTask() getRunningTask()
getAlarmRecord() robotRefreshTimer = window.setInterval(() => {
if (!document.hidden) getRobot()
}, 5000)
})
onUnmounted(() => {
window.clearInterval(robotRefreshTimer)
}) })
</script> </script>
@ -248,6 +226,31 @@ onMounted(() => {
background: #0f83; background: #0f83;
color: #0f8; color: #0f8;
} }
.unknown,
.offline,
.stopped {
background: rgba(148, 163, 184, 0.2);
color: #cbd5e1;
}
.manual,
.running {
background: rgba(0, 212, 255, 0.18);
color: #00d4ff;
}
.charging,
.paused {
background: rgba(255, 179, 0, 0.2);
color: #ffb300;
}
.fault,
.emergency {
background: rgba(255, 77, 79, 0.2);
color: #ff6b6d;
}
} }
.info-box { .info-box {
@ -411,45 +414,13 @@ onMounted(() => {
padding: 16px; padding: 16px;
margin-bottom: 16px; margin-bottom: 16px;
.alarm-title {
display: flex;
justify-content: space-between;
.title { .title {
color: #FF6B6B;
font-size: 14px; font-size: 14px;
font-family: Inter; font-family: Inter;
font-weight: bold; font-weight: bold;
} }
.status {
font-size: 12px;
border-radius: 12px;
padding: 0 8px;
display: flex;
align-items: center;
justify-content: center;
}
}
.NORMAL {
color: #00D4FF;
}
.ABNORMAL {
color: #FF6B6B;
}
.PENDING {
color: #8899BB;
}
.RECOGNIZE_FAILED {
color: #FF6B6B;
font-size: 12px;
}
.alarm-info { .alarm-info {
margin: 10px 0; margin: 10px 0;
font-size: 12px; font-size: 12px;

View File

@ -185,6 +185,7 @@ import { computed, getCurrentInstance, onBeforeUnmount, onMounted, reactive, ref
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { addRobot, deleteRobot, getRobotDevices, getRobotList, getRobotStatus, updateRobot } from '@/api/inspection/robot' import { addRobot, deleteRobot, getRobotDevices, getRobotList, getRobotStatus, updateRobot } from '@/api/inspection/robot'
import { getMapList } from '@/api/inspection/map' import { getMapList } from '@/api/inspection/map'
import { agvRuntimeStatusLabel, agvRuntimeStatusType } from '@/utils/agvRuntimeStatus'
const { proxy } = getCurrentInstance() const { proxy } = getCurrentInstance()
const { robot_type } = proxy.useDict('robot_type') const { robot_type } = proxy.useDict('robot_type')
@ -256,8 +257,8 @@ async function loadDevices(showLoading = true) {
try { const res = await getRobotDevices(activeRobot.value.id); devices.value = res.data || [] } finally { deviceLoading.value = false } try { const res = await getRobotDevices(activeRobot.value.id); devices.value = res.data || [] } finally { deviceLoading.value = false }
} }
function endpointText(row) { return row.ipAddress && row.port ? `${row.ipAddress}:${row.port}` : '等待连接后自动获取' } function endpointText(row) { return row.ipAddress && row.port ? `${row.ipAddress}:${row.port}` : '等待连接后自动获取' }
function runtimeStatusLabel(value) { return ({ 0: '在线', 1: '离线', 2: '充电中', 3: '巡检中' })[value] || '未知' } const runtimeStatusLabel = agvRuntimeStatusLabel
function runtimeStatusType(value) { return ({ 0: 'success', 1: 'info', 2: 'warning', 3: 'primary' })[value] || 'info' } const runtimeStatusType = agvRuntimeStatusType
function hasBattery(row) { return row.batteryLevel !== null && row.batteryLevel !== undefined && row.batteryLevel !== '' } function hasBattery(row) { return row.batteryLevel !== null && row.batteryLevel !== undefined && row.batteryLevel !== '' }
function normalizeBattery(value) { return Math.max(0, Math.min(100, Math.round(Number(value) || 0))) } function normalizeBattery(value) { return Math.max(0, Math.min(100, Math.round(Number(value) || 0))) }
function batteryColor(value) { function batteryColor(value) {