feat: 扩展机器人状态信息,新增充电、载货和不接单状态,更新相关组件以显示状态图标,优化机器人信息同步逻辑

This commit is contained in:
xudan 2025-09-15 17:30:01 +08:00
parent 911a9fbe2f
commit c5064b189c
8 changed files with 199 additions and 3 deletions

View File

@ -9,6 +9,19 @@ export interface RobotGroup {
robots?: Array<string>; // 机器人列表
}
export type RobotPen = Pen & {
robot: {
type: RobotType;
active?: boolean;
isWaring?: boolean;
isFault?: boolean;
isCharging?: boolean; // 是否充电中
isCarrying?: boolean; // 是否载货
isNotAcceptingOrders?: boolean; // 是否不接单
path?: { x: number; y: number }[];
angle?: number;
};
};
export interface RobotInfo {
gid?: string; // 机器人组id
id: string; // 机器人id
@ -24,6 +37,9 @@ export interface RobotInfo {
canControl?: boolean; // 控制状态
targetPoint?: string; // 目标点位(名称)
isLoading?: 0 | 1; // 载货状态1载货0空载实时数据透传
isCharging?: boolean; // 是否充电中
isCarrying?: boolean; // 是否载货
isNotAcceptingOrders?: boolean; // 是否不接单
}
export interface RobotDetail extends RobotInfo {
@ -45,6 +61,9 @@ export interface RobotRealtimeInfo extends RobotInfo {
path?: Array<Point>; // 规划路径
isWaring?: boolean; // 是否告警
isFault?: boolean; // 是否故障
isCharging?: boolean; // 是否充电中
isCarrying?: boolean; // 是否载货
isNotAcceptingOrders?: boolean; // 是否不接单
}
// AMR Redis状态接口 - 更新为实际返回的数据结构

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

View File

@ -16,6 +16,11 @@
<div class="robot-status" :style="{ color: getRobotStatusColor((robotInfo.state as any) || 'offline') }">
{{ getRobotStatusText((robotInfo.state as any) || 'offline') }}
</div>
<div class="robot-extra-statuses">
<span v-if="robotInfo.isCarrying" class="extra-status-tag">载货中</span>
<span v-if="robotInfo.isNotAcceptingOrders" class="extra-status-tag">不接单</span>
<span v-if="robotInfo.isCharging" class="extra-status-tag">充电中</span>
</div>
</div>
<div class="robot-details">
@ -326,6 +331,23 @@ onUnmounted(() => {
margin-bottom: 12px;
}
.robot-extra-statuses {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
}
.extra-status-tag {
font-size: 10px;
font-weight: 500;
padding: 2px 6px;
border-radius: 8px;
background-color: #e9ecef;
color: #495057;
white-space: nowrap;
}
.robot-name {
font-size: 14px;
font-weight: 600;

View File

@ -100,10 +100,28 @@ const monitorScene = async () => {
//
updates.forEach(({ id, data }) => {
const { x, y, active, angle, path: points, isWaring, isFault, ...rest } = data;
const {
x,
y,
active,
angle,
path: points,
isWaring,
isFault,
isCharging = 1,
isCarrying = 1,
isNotAcceptingOrders = 1,
...rest
} = data;
//
editor.value?.updateRobot(id, rest);
// //便
editor.value?.updateRobot(id, {
...rest,
isCharging: isCharging as any as boolean,
isCarrying: isCarrying as any as boolean,
isNotAcceptingOrders: isNotAcceptingOrders as any as boolean,
});
// refreshRobot
let processedPath: Array<{ x: number; y: number }> | undefined;
@ -115,7 +133,16 @@ const monitorScene = async () => {
}
//
const robotState = { active, isWaring, isFault, path: processedPath, angle };
const robotState = {
active,
isWaring,
isFault,
path: processedPath,
angle,
isCharging,
isCarrying,
isNotAcceptingOrders,
};
if (Object.values(robotState).some((v) => v !== undefined)) {
editor.value?.setValue({ id, robot: robotState }, { render: false, history: false, doEvent: false });
}
@ -140,6 +167,11 @@ const monitorScene = async () => {
editor.value?.setValue(update, { render: false, history: false, doEvent: false });
});
// > >
updates.forEach(({ id }) => {
editor.value?.updateRobotStatusOverlay?.(id, false);
});
//
editor.value?.render();
};

View File

@ -27,6 +27,10 @@ import { clone, get, isEmpty, isNil, isString, nth, pick, remove, some } from 'l
import { BehaviorSubject, debounceTime, filter, map, Subject, switchMap } from 'rxjs';
import { reactive, watch } from 'vue';
// 机器人状态覆盖图标
import cargoIcon from '../assets/icons/png/cargo.png';
import chargingIcon from '../assets/icons/png/charging.png';
import notAcceptingOrdersIcon from '../assets/icons/png/notAcceptingOrders.png';
import { AreaOperationService } from './area-operation.service';
import { BinTaskManagerService } from './bintask-manager.service';
import colorConfig from './color/color-config.service';
@ -1086,6 +1090,8 @@ export class EditorService extends Meta2d {
locked: LockState.Disable,
};
await this.addPen(pen, false, true, true);
// 初始化时创建/同步一次状态覆盖图标(默认隐藏或根据当前状态显示)
this.updateRobotStatusOverlay(id, false);
}),
);
@ -1147,6 +1153,8 @@ export class EditorService extends Meta2d {
},
{ render: true, history: false, doEvent: false },
);
// 同步状态图标尺寸/位置
this.updateRobotStatusOverlay(id, true);
}
});
}
@ -1215,6 +1223,114 @@ export class EditorService extends Meta2d {
}
//#endregion
/**
* > >
*/
#getRobotStatusIcon(pen?: MapPen): string | null {
if (!pen) return null;
// 兼容:状态可能存放在 pen.robot 或 机器人信息表中
const r1: any = pen.robot ?? {};
const r2 = this.getRobotById(pen.id || '');
const toBool = (v: any) => v === true || v === 1 || v === '1';
const isCarrying = toBool(r1?.isCarrying ?? r2?.isCarrying);
const isNotAcceptingOrders = toBool(r1?.isNotAcceptingOrders ?? r2?.isNotAcceptingOrders);
const isCharging = toBool(r1?.isCharging ?? r2?.isCharging);
if (isCarrying) return cargoIcon;
if (isNotAcceptingOrders) return notAcceptingOrdersIcon;
if (isCharging) return chargingIcon;
return null;
}
/**
* //
* @param id ID
* @param render
*/
public updateRobotStatusOverlay(id: string, render = false): void {
const pen = this.getPenById(id);
if (!pen) return;
const icon = this.#getRobotStatusIcon(pen);
const robotVisible = pen.visible !== false;
// 计算覆盖图标的尺寸(为机器人图片的一半)
const baseW = (pen as any).iconWidth ?? 42;
const baseH = (pen as any).iconHeight ?? 76;
const oW = Math.max(8, Math.floor(baseW * 0.5));
const oH = Math.max(8, Math.floor(baseH * 0.5));
// 以机器人中心为基准,做一个轻微向下的偏移,并随机器人一起旋转
const rect = this.getPenRect(pen);
const wr: any = (pen as any).calculative?.worldRect;
const deg: number = (wr?.rotate ?? 0) as number;
const theta = (deg * Math.PI) / 180;
const cx = rect.x + rect.width / 2;
const cy = rect.y + rect.height / 2;
const iconTop = (pen as any).iconTop ?? 0;
// 在本地坐标中的偏移以机器人中心为原点y向下为正
const localDx = 0;
const localDy = iconTop +16;
// 旋转后的偏移
const rotDx = localDx * Math.cos(theta) - localDy * Math.sin(theta);
const rotDy = localDx * Math.sin(theta) + localDy * Math.cos(theta);
// 覆盖图标中心点
const icx = cx + rotDx;
const icy = cy + rotDy;
const ox = icx - oW / 2;
const oy = icy - oH / 2;
const overlayId = `robot-status-${id}`;
const exist = this.getPenById(overlayId);
if (!icon || !robotVisible) {
if (exist) {
this.setValue({ id: overlayId, visible: false }, { render, history: false, doEvent: false });
}
return;
}
// 如果已存在覆盖图标,更新其属性;否则创建新的 image 图元
if (exist) {
this.setValue(
{
id: overlayId,
image: icon,
x: ox,
y: oy,
width: oW,
height: oH,
rotate: deg,
visible: true,
locked: LockState.Disable,
},
{ render: false, history: false, doEvent: false },
);
// 置顶,保证覆盖在机器人之上
this.top([exist]);
if (render) this.render();
return;
}
const overlayPen: MapPen = {
id: overlayId,
name: 'image',
tags: ['robot-status'],
x: ox,
y: oy,
width: oW,
height: oH,
image: icon,
rotate: deg,
canvasLayer: CanvasLayer.CanvasImage,
locked: LockState.Disable,
visible: true,
} as any;
this.addPen(overlayPen, false, false, true);
this.top([overlayPen]);
if (render) this.render();
}
//#region 点位
/** 画布上所有点位对象列表,响应式更新 */
public readonly points = useObservable<MapPen[], MapPen[]>(

View File

@ -26,6 +26,7 @@ export class LayerManagerService {
const routes = this.editor.find('route');
const points = this.editor.find('point');
const robots = this.editor.find('robot');
const images = this.editor.find('image');
const storageBackgrounds = this.editor.find('storage-background');
const storageLocations = this.editor.find('storage-location,storage-more');
@ -58,6 +59,12 @@ export class LayerManagerService {
if (robots.length > 0) {
this.editor.top(robots);
}
// 将机器人状态覆盖图标置于最顶层(在机器人之上)
const robotStatusOverlays = images.filter((p: any) => p?.tags?.includes('robot-status'));
if (robotStatusOverlays.length > 0) {
this.editor.top(robotStatusOverlays);
}
}
/**