feat: 添加AMR Redis状态接口及相关功能,支持在机器人详细卡片中查看AMR状态信息

This commit is contained in:
xudan 2025-08-14 17:55:22 +08:00
parent bdaa6b2b21
commit f69f699f4a
3 changed files with 392 additions and 3 deletions

View File

@ -1,6 +1,6 @@
import http from '@core/http';
import type { RobotDetail, RobotGroup, RobotInfo } from './type';
import type { AmrRedisState, RobotDetail, RobotGroup, RobotInfo } from './type';
const enum API {
= '/robot/getAll',
@ -9,6 +9,7 @@ const enum API {
= '/robot/seizeByIds',
= '/robot/syncByGroupId',
AMR状态 = '/amr/redis',
}
export async function getAllRobots(): Promise<Array<RobotInfo>> {
@ -62,3 +63,14 @@ export async function syncGroupRobotsById(id: RobotGroup['id'], sid: RobotGroup[
return false;
}
}
export async function getAmrRedisState(id: string): Promise<AmrRedisState | null> {
type D = AmrRedisState;
try {
const data = await http.get<D>(`${API.AMR状态}/${id}`);
return data ?? null;
} catch (error) {
console.debug(error);
return null;
}
}

View File

@ -46,3 +46,91 @@ export interface RobotRealtimeInfo extends RobotInfo {
isWaring?: boolean; // 是否告警
isFault?: boolean; // 是否故障
}
// AMR Redis状态接口
export interface AmrRedisState {
headerid: number;
timestamp: string;
version: string;
manufacturer: string;
serialnumber: string;
orderid: string;
orderupdateid: number;
lastnodeid: string;
lastnodesequenceid: number;
driving: boolean;
waitingforinteractionzonerelease: boolean;
paused: boolean;
newbaserequest: boolean;
distancesincelastnode: number;
operatingmode: string;
nodestates: any[];
edgestates: any[];
agvposition: {
x: number;
y: number;
theta: number;
mapid: string;
mapdescription: string;
positioninitialized: boolean;
deviationrange: number;
localizationscore: number;
};
velocity: {
vx: number;
vy: number;
omega: number;
};
loads: Array<{
loadid: string;
loadtype: string;
loadposition: string;
boundingboxreference: {
x: number;
y: number;
z: number;
theta: number;
};
loaddimensions: {
length: number;
width: number;
height: number;
};
}>;
actionstates: Array<{
actionid: string;
actiontype: string;
actiondescription: string;
actionstatus: string;
resultdescription: string;
}>;
batterystate: {
batterycharge: number;
batteryvoltage: number;
batteryhealth: number;
charging: boolean;
reach: number;
};
errors: Array<{
errortype: string;
errorreferences: Array<{
referencekey: string;
referencevalue: string;
}>;
errordescription: string;
errorlevel: string;
}>;
information: Array<{
infotype: string;
inforeferences: Array<{
referencekey: string;
referencevalue: string;
}>;
infodescription: string;
infolevel: string;
}>;
safetystate: {
estop: string;
fieldviolation: boolean;
};
}

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import { type RobotInfo, RobotState, RobotType } from '@api/robot';
import { type AmrRedisState, getAmrRedisState, type RobotInfo, RobotState, RobotType } from '@api/robot';
import type { EditorService } from '@core/editor.service';
import sTheme from '@core/theme.service';
import { computed, inject, type InjectionKey, type ShallowRef } from 'vue';
import { computed, inject, type InjectionKey, ref, type ShallowRef } from 'vue';
type Props = {
token: InjectionKey<ShallowRef<EditorService>>;
@ -19,6 +19,49 @@ const robot = computed<RobotInfo | null>(() => {
const batteryIcon = computed<string>(() => (robot.value?.state === RobotState.充电中 ? 'battery_charge' : 'battery'));
const stateDot = computed<string>(() => `state-${robot.value?.state}`);
// AMR
const amrDetailVisible = ref(false);
const amrDetailData = ref<AmrRedisState | null>(null);
const lastUpdateTime = ref<string>('');
// AMR
const fetchAmrDetail = async () => {
if (!robot.value?.id) return;
//
amrDetailVisible.value = true;
try {
//
const data = await getAmrRedisState(robot.value.id);
console.log(data + 'AMR全量redis接口返回数据');
amrDetailData.value = data;
lastUpdateTime.value = new Date().toLocaleString('zh-CN');
} catch (error) {
console.error('获取AMR详情失败:', error);
//
amrDetailData.value = { error: true, message: '接口调用失败' } as AmrRedisState & {
error: boolean;
message: string;
};
lastUpdateTime.value = new Date().toLocaleString('zh-CN');
}
};
// JSON
const formatJson = (obj: unknown) => {
try {
return JSON.stringify(obj, null, 2);
} catch {
return String(obj);
}
};
//
const handleModalClose = () => {
amrDetailVisible.value = false;
amrDetailData.value = null;
lastUpdateTime.value = '';
};
</script>
<template>
@ -83,8 +126,198 @@ const stateDot = computed<string>(() => `state-${robot.value?.state}`);
<a-typography-text :content="$t(robot.targetPoint ?? '-')" ellipsis />
</a-list-item>
</a-list>
<!-- 查看AMR详情按钮 -->
<div class="mt-16 text-center">
<a-button type="primary" @click="fetchAmrDetail">
{{ $t('查看AMR详情') }}
</a-button>
</div>
</template>
<a-empty v-else :image="sTheme.empty" />
<!-- AMR详情弹窗 -->
<a-modal
v-model:open="amrDetailVisible"
:title="$t('AMR Redis状态详情')"
width="80%"
:footer="null"
:destroy-on-close="true"
@cancel="handleModalClose"
>
<template #title>
<div class="flex items-center justify-between">
<div>
<div>{{ $t('AMR Redis状态详情') }}</div>
<div v-if="robot?.id" class="text-xs text-gray-500 mt-1">{{ $t('机器人ID') }}: {{ robot.id }}</div>
<div v-if="lastUpdateTime" class="text-xs text-gray-500 mt-1">
{{ $t('最后更新') }}: {{ lastUpdateTime }}
</div>
</div>
<a-button type="text" size="small" @click="fetchAmrDetail">
<i class="icon redo mr-4" />
{{ $t('刷新') }}
</a-button>
</div>
</template>
<div v-if="amrDetailData" class="amr-detail-content">
<!-- 如果数据解析失败只显示原始数据 -->
<div v-if="(amrDetailData as any).error" class="error-content">
<a-alert
:message="$t('数据解析失败')"
:description="(amrDetailData as any).message"
type="warning"
show-icon
class="mb-16"
/>
<a-typography-title :level="5">{{ $t('原始数据') }}</a-typography-title>
<a-textarea :value="formatJson(amrDetailData)" :rows="25" readonly class="font-mono" />
</div>
<!-- 如果数据正常显示结构化内容 -->
<a-tabs v-else>
<a-tab-pane key="basic" :tab="$t('基本信息')">
<a-descriptions :column="2" bordered>
<a-descriptions-item :label="$t('序列号')">{{ amrDetailData.serialnumber || '-' }}</a-descriptions-item>
<a-descriptions-item :label="$t('制造商')">{{ amrDetailData.manufacturer || '-' }}</a-descriptions-item>
<a-descriptions-item :label="$t('版本')">{{ amrDetailData.version || '-' }}</a-descriptions-item>
<a-descriptions-item :label="$t('订单ID')">{{ amrDetailData.orderid || '-' }}</a-descriptions-item>
<a-descriptions-item :label="$t('最后节点')">{{ amrDetailData.lastnodeid || '-' }}</a-descriptions-item>
<a-descriptions-item :label="$t('操作模式')">{{
amrDetailData.operatingmode || '-'
}}</a-descriptions-item>
<a-descriptions-item :label="$t('是否行驶中')">{{
amrDetailData.driving ? $t('是') : $t('否')
}}</a-descriptions-item>
<a-descriptions-item :label="$t('是否暂停')">{{
amrDetailData.paused ? $t('是') : $t('否')
}}</a-descriptions-item>
<a-descriptions-item :label="$t('时间戳')" :span="2">{{
amrDetailData.timestamp || '-'
}}</a-descriptions-item>
<a-descriptions-item :label="$t('距离上次节点')"
>{{ (amrDetailData.distancesincelastnode || 0).toFixed(2) }}m</a-descriptions-item
>
<a-descriptions-item :label="$t('等待交互区释放')">{{
amrDetailData.waitingforinteractionzonerelease ? $t('是') : $t('否')
}}</a-descriptions-item>
</a-descriptions>
</a-tab-pane>
<a-tab-pane key="position" :tab="$t('位置信息')">
<a-descriptions :column="2" bordered>
<a-descriptions-item :label="$t('X坐标')">{{
amrDetailData.agvposition?.x?.toFixed(4) || '-'
}}</a-descriptions-item>
<a-descriptions-item :label="$t('Y坐标')">{{
amrDetailData.agvposition?.y?.toFixed(4) || '-'
}}</a-descriptions-item>
<a-descriptions-item :label="$t('角度')">{{
amrDetailData.agvposition?.theta?.toFixed(4) || '-'
}}</a-descriptions-item>
<a-descriptions-item :label="$t('地图ID')">{{
amrDetailData.agvposition?.mapid || '-'
}}</a-descriptions-item>
<a-descriptions-item :label="$t('定位分数')">{{
amrDetailData.agvposition?.localizationscore?.toFixed(4) || '-'
}}</a-descriptions-item>
<a-descriptions-item :label="$t('偏差范围')">{{
amrDetailData.agvposition?.deviationrange?.toFixed(4) || '-'
}}</a-descriptions-item>
</a-descriptions>
</a-tab-pane>
<a-tab-pane key="battery" :tab="$t('电池状态')">
<a-descriptions :column="2" bordered>
<a-descriptions-item :label="$t('电池电量')"
>{{ amrDetailData.batterystate?.batterycharge?.toFixed(1) || '-' }}%</a-descriptions-item
>
<a-descriptions-item :label="$t('电池电压')"
>{{ amrDetailData.batterystate?.batteryvoltage?.toFixed(3) || '-' }}V</a-descriptions-item
>
<a-descriptions-item :label="$t('电池健康度')"
>{{ amrDetailData.batterystate?.batteryhealth || '-' }}%</a-descriptions-item
>
<a-descriptions-item :label="$t('是否充电中')">{{
amrDetailData.batterystate?.charging ? $t('是') : $t('否')
}}</a-descriptions-item>
<a-descriptions-item :label="$t('续航里程')"
>{{ amrDetailData.batterystate?.reach?.toFixed(1) || '-' }}km</a-descriptions-item
>
</a-descriptions>
</a-tab-pane>
<a-tab-pane key="velocity" :tab="$t('速度信息')">
<a-descriptions :column="2" bordered>
<a-descriptions-item :label="$t('X方向速度')"
>{{ amrDetailData.velocity?.vx?.toFixed(3) || '-' }}m/s</a-descriptions-item
>
<a-descriptions-item :label="$t('Y方向速度')"
>{{ amrDetailData.velocity?.vy?.toFixed(3) || '-' }}m/s</a-descriptions-item
>
<a-descriptions-item :label="$t('角速度')"
>{{ amrDetailData.velocity?.omega?.toFixed(3) || '-' }}rad/s</a-descriptions-item
>
</a-descriptions>
</a-tab-pane>
<a-tab-pane key="loads" :tab="$t('载货信息')">
<a-table :data-source="amrDetailData.loads || []" :pagination="false" size="small">
<a-table-column key="loadid" :title="$t('载货ID')" data-index="loadid" />
<a-table-column key="loadtype" :title="$t('载货类型')" data-index="loadtype" />
<a-table-column key="loadposition" :title="$t('载货位置')" data-index="loadposition" />
<a-table-column key="dimensions" :title="$t('尺寸')">
<template #default="{ record }">
{{ record.loaddimensions?.length || '-' }}×{{ record.loaddimensions?.width || '-' }}×{{
record.loaddimensions?.height || '-'
}}
</template>
</a-table-column>
</a-table>
</a-tab-pane>
<a-tab-pane key="actions" :tab="$t('动作状态')">
<a-table :data-source="amrDetailData.actionstates || []" :pagination="false" size="small">
<a-table-column key="actionid" :title="$t('动作ID')" data-index="actionid" />
<a-table-column key="actiontype" :title="$t('动作类型')" data-index="actiontype" />
<a-table-column key="actionstatus" :title="$t('动作状态')" data-index="actionstatus" />
<a-table-column key="resultdescription" :title="$t('结果描述')" data-index="resultdescription" />
</a-table>
</a-tab-pane>
<a-tab-pane key="errors" :tab="$t('错误信息')">
<a-table :data-source="amrDetailData.errors || []" :pagination="false" size="small">
<a-table-column key="errortype" :title="$t('错误类型')" data-index="errortype" />
<a-table-column key="errorlevel" :title="$t('错误级别')" data-index="errorlevel" />
<a-table-column key="errordescription" :title="$t('错误描述')" data-index="errordescription" />
</a-table>
</a-tab-pane>
<a-tab-pane key="safety" :tab="$t('安全状态')">
<a-descriptions :column="2" bordered>
<a-descriptions-item :label="$t('急停状态')">
<a-tag :color="amrDetailData.safetystate?.estop === 'NONE' ? 'green' : 'red'">
{{ amrDetailData.safetystate?.estop || '-' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item :label="$t('场地违规')">
<a-tag :color="amrDetailData.safetystate?.fieldviolation ? 'red' : 'green'">
{{ amrDetailData.safetystate?.fieldviolation ? $t('是') : $t('否') }}
</a-tag>
</a-descriptions-item>
</a-descriptions>
</a-tab-pane>
<a-tab-pane key="raw" :tab="$t('原始数据')">
<a-textarea :value="formatJson(amrDetailData)" :rows="20" readonly class="font-mono" />
</a-tab-pane>
</a-tabs>
</div>
<div v-else>
<a-empty :image="sTheme.empty" />
</div>
</a-modal>
</a-card>
</template>
@ -112,4 +345,60 @@ const stateDot = computed<string>(() => `state-${robot.value?.state}`);
}
}
}
.amr-detail-content {
.ant-tabs-content {
max-height: 600px;
overflow-y: auto;
}
.ant-descriptions {
margin-bottom: 16px;
}
.ant-table {
margin-bottom: 16px;
}
.font-mono {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
line-height: 1.5;
}
.mt-16 {
margin-top: 16px;
}
.icon {
margin-right: 4px;
}
}
.error-content {
.mb-16 {
margin-bottom: 16px;
}
}
//
:deep(.ant-modal-header) {
.flex {
display: flex;
align-items: center;
justify-content: space-between;
}
.text-xs {
font-size: 12px;
}
.text-gray-500 {
color: #6b7280;
}
.mt-1 {
margin-top: 4px;
}
}
</style>