feat: 扩展自动门点功能,新增光圈绘制与状态更新机制,优化性能并引入映射缓存以提升数据处理效率

This commit is contained in:
xudan 2025-08-20 10:31:27 +08:00
parent 94dccba404
commit 26af765dc8
7 changed files with 657 additions and 6 deletions

View File

@ -630,3 +630,149 @@ export class EditorService extends Meta2d {
5. **状态验证不足**: 缺乏对接收数据的有效性验证
通过实施全局状态管理、WebSocket连接复用、状态缓存机制和坐标转换优化等解决方案可以有效解决这些问题确保多页面间机器人位置的一致性。
## 8. 自动门点光圈功能扩展
### 8.1 功能概述
在机器人光圈绘制的基础上新增了自动门点的光圈绘制功能。当WebSocket推送自动门点状态数据时系统会根据设备状态自动绘制相应颜色的光圈。
### 8.2 数据结构
WebSocket推送的自动门点数据格式
```typescript
{
"gid": "",
"id": "172.31.57.55-502-17", // 设备ID用于匹配地图中的自动门点
"label": "AutoD01",
"type": 99, // 标识为自动门点
"deviceStatus": 0, // 设备状态0=关门1=开门
"active": true,
// ... 其他字段
}
```
### 8.3 实现细节
#### 主题颜色配置
`editor-dark.json``editor-light.json` 中添加自动门点颜色配置:
```json
"autoDoor": {
"stroke-closed": "#FF4D4F99", // 关门状态边框(红色)
"fill-closed": "#FF4D4F33", // 关门状态填充(红色)
"stroke-open": "#1890FF99", // 开门状态边框(蓝色)
"fill-open": "#1890FF33" // 开门状态填充(蓝色)
}
```
#### 数据模型扩展
`MapPointInfo` 接口中新增字段:
```typescript
interface MapPointInfo {
// ... 现有字段
deviceStatus?: number; // 设备状态0=关门1=开门)
active?: boolean; // 是否激活状态,控制光圈显示
}
```
#### 绘制函数修改
`drawPoint` 函数中添加自动门点光圈绘制逻辑:
```typescript
// 为自动门点绘制光圈
if (type === MapPointType.自动门点 && pointActive && deviceStatus !== undefined) {
const ox = x + w / 2;
const oy = y + h / 2;
const haloRadius = Math.max(w, h) / 2 + 10;
ctx.ellipse(ox, oy, haloRadius, haloRadius, 0, 0, Math.PI * 2);
// 根据设备状态选择颜色
if (deviceStatus === 0) {
// 关门状态 - 红色
ctx.fillStyle = get(theme, 'autoDoor.fill-closed') ?? '#FF4D4F33';
ctx.strokeStyle = get(theme, 'autoDoor.stroke-closed') ?? '#FF4D4F99';
} else {
// 开门状态 - 蓝色
ctx.fillStyle = get(theme, 'autoDoor.fill-open') ?? '#1890FF33';
ctx.strokeStyle = get(theme, 'autoDoor.stroke-open') ?? '#1890FF99';
}
ctx.fill();
ctx.stroke();
}
```
#### 状态更新方法
新增 `updateAutoDoorByDeviceId` 方法:
```typescript
public updateAutoDoorByDeviceId(deviceId: string, deviceStatus: number, active = true): void {
// 查找匹配的自动门点
const autoDoorPoint = this.data().pens.find(
(pen) => pen.name === 'point' &&
(pen as MapPen).point?.type === MapPointType.自动门点 &&
(pen as MapPen).point?.deviceId === deviceId
) as MapPen | undefined;
if (!autoDoorPoint?.id || !autoDoorPoint.point) return;
// 更新自动门点状态
this.updatePen(autoDoorPoint.id, {
point: {
...autoDoorPoint.point,
deviceStatus,
active,
},
}, false);
}
```
#### WebSocket数据处理
修改运动监控组件的WebSocket处理逻辑
```typescript
ws.onmessage = (e) => {
const data = JSON.parse(e.data || '{}');
// 判断数据类型type=99为自动门点其他为机器人
if (data.type === 99) {
// 自动门点数据处理
const { id: deviceId, deviceStatus, active = true } = data;
if (deviceId && deviceStatus !== undefined) {
latestAutoDoorData.set(deviceId, { deviceId, deviceStatus, active });
}
} else {
// 机器人数据处理
const robotData = data as RobotRealtimeInfo;
latestRobotData.set(robotData.id, robotData);
}
};
```
### 8.4 使用方式
1. **地图编辑**在场景编辑器中创建自动门点并设置对应的设备ID
2. **状态监控**系统自动接收WebSocket推送的自动门点状态数据
3. **视觉反馈**
- 关门状态deviceStatus=0显示红色光圈
- 开门状态deviceStatus=1显示蓝色光圈
- 无状态数据时:不显示光圈
### 8.5 技术优势
1. **复用机器人架构**:充分利用现有的渲染和状态管理机制
2. **高性能处理**:采用相同的时间分片和数据缓冲策略
3. **类型安全**完整的TypeScript类型支持
4. **主题适配**:支持深色和浅色主题
5. **实时性**:与机器人监控相同的实时更新能力
这种设计展现了系统架构的可扩展性,为未来支持更多设备类型(如电梯、传感器等)奠定了良好的基础。

View File

@ -31,6 +31,8 @@ export interface MapPointInfo {
associatedStorageLocations?: string[]; // 库位名称
deviceId?: string; // 设备ID
enabled?: 0 | 1; // 是否启用仅停靠点使用0=禁用1=启用)
deviceStatus?: number; // 设备状态仅自动门点使用0=关门1=开门)
active?: boolean; // 是否激活状态,用于控制光圈显示
}
//#endregion

View File

@ -51,5 +51,11 @@
"fill-warning": "#FF851B33",
"stroke-fault": "#FF4D4F99",
"fill-fault": "#FF4D4F33"
},
"autoDoor": {
"stroke-closed": "#FF4D4F99",
"fill-closed": "#FF4D4F33",
"stroke-open": "#1890FF99",
"fill-open": "#1890FF33"
}
}

View File

@ -51,5 +51,11 @@
"fill-warning": "#FF851B33",
"stroke-fault": "#FF4D4F99",
"fill-fault": "#FF4D4F33"
},
"autoDoor": {
"stroke-closed": "#FF4D4F99",
"fill-closed": "#FF4D4F33",
"stroke-open": "#1890FF99",
"fill-open": "#1890FF33"
}
}

View File

@ -9,6 +9,8 @@ import { isNil } from 'lodash-es';
import { computed, onMounted, onUnmounted, provide, ref, shallowRef, watch } from 'vue';
import { useRoute } from 'vue-router';
import { autoDoorSimulationService, type AutoDoorWebSocketData } from '../services/auto-door-simulation.service';
const EDITOR_KEY = Symbol('editor-key');
type Props = {
@ -84,7 +86,7 @@ const monitorScene = async () => {
const frameBudget = 8; // 8ms
const startTime = performance.now();
//
//
while (performance.now() - startTime < frameBudget && latestRobotData.size > 0) {
// Map
const entry = latestRobotData.entries().next().value;
@ -109,6 +111,9 @@ const monitorScene = async () => {
}
}
//
autoDoorSimulationService.processBufferedData(frameBudget, startTime);
// renderLoop
// 使
animationFrameId = requestAnimationFrame(renderLoop);
@ -117,12 +122,20 @@ const monitorScene = async () => {
/**
* WebSocket onmessage 事件处理器
* 这个函数只做一件事接收数据并将其快速存入缓冲区
* 这种数据与渲染分离的设计是避免UI阻塞的关键
* 这种"数据与渲染分离"的设计是避免UI阻塞的关键
*/
ws.onmessage = (e) => {
const data = <RobotRealtimeInfo>JSON.parse(e.data || '{}');
// MapID
latestRobotData.set(data.id, data);
const data = JSON.parse(e.data || '{}');
// type=99
if (data.type === 99) {
//
autoDoorSimulationService.handleWebSocketData(data as AutoDoorWebSocketData);
} else {
//
const robotData = data as RobotRealtimeInfo;
latestRobotData.set(robotData.id, robotData);
}
};
client.value = ws;
@ -158,11 +171,29 @@ onMounted(async () => {
storageLocationService.value?.startMonitoring({ interval: 1 });
//
await handleAutoSaveAndRestoreViewState();
//
if (editor.value) {
autoDoorSimulationService.setEditorService(editor.value);
// 使WebSocket
// autoDoorSimulationService.startSimulation({
// deviceId: 'test01',
// label: 'TestAutoDoor01',
// interval: 3000,
// initialStatus: 0,
// enableLogging: true,
// });
}
});
onUnmounted(() => {
client.value?.close();
storageLocationService.value?.destroy();
//
autoDoorSimulationService.clearBufferedData();
// WebSocket
// autoDoorSimulationService.stopAllSimulations();
});
//#endregion

View File

@ -0,0 +1,388 @@
/**
*
* WebSocket数据处理功能
* WebSocket推送数据
*/
import { type MapPen, MapPointType } from '@api/map';
import type { EditorService } from './editor.service';
/**
*
*/
export interface AutoDoorSimulationConfig {
/** 设备ID需要与地图中自动门点的deviceId匹配 */
deviceId: string;
/** 设备标签 */
label?: string;
/** 状态切换间隔毫秒默认3000ms */
interval?: number;
/** 初始状态0=关门1=开门默认0 */
initialStatus?: 0 | 1;
/** 是否启用控制台日志 */
enableLogging?: boolean;
}
/**
* WebSocket推送的数据格式
*/
interface AutoDoorStatusData {
gid: string;
id: string;
label: string;
brand: null;
type: 99; // 自动门点类型标识
ip: null;
battery: number;
isConnected: boolean;
state: number;
canOrder: boolean;
canStop: null;
canControl: boolean;
targetPoint: null;
deviceStatus: 0 | 1; // 设备状态0=关门1=开门
x: number;
y: number;
active: boolean;
angle: number;
isWaring: null;
isFault: null;
isLoading: null;
path: [];
}
/**
* WebSocket推送的自动门点数据接口
*/
export interface AutoDoorWebSocketData {
gid: string;
id: string; // 设备ID
label: string;
brand: null;
type: 99; // 自动门点类型标识
ip: null;
battery: number;
isConnected: boolean;
state: number;
canOrder: boolean;
canStop: null;
canControl: boolean;
targetPoint: null;
deviceStatus: 0 | 1; // 设备状态0=关门1=开门
x: number;
y: number;
active: boolean;
angle: number;
isWaring: null;
isFault: null;
isLoading: null;
path: [];
}
/**
*
* WebSocket数据处理使
*/
export class AutoDoorService {
private timers = new Map<string, NodeJS.Timeout>();
private statusMap = new Map<string, 0 | 1>();
private editorService: EditorService | null = null;
private latestAutoDoorData = new Map<string, { deviceId: string; deviceStatus: number; active: boolean }>();
// 设备ID到自动门点ID的映射缓存避免每次都遍历查找
private deviceIdToPointIdMap = new Map<string, string>();
/**
*
* @param editor
*/
setEditorService(editor: EditorService): void {
this.editorService = editor;
// 初始化设备ID到点位ID的映射关系
this.initializeDeviceMapping();
}
/**
* ID到自动门点ID的映射关系
*
*/
private initializeDeviceMapping(): void {
if (!this.editorService) {
console.warn('⚠️ 编辑器服务未设置,无法初始化自动门点映射');
return;
}
// 清空现有映射
this.deviceIdToPointIdMap.clear();
// 遍历所有点位,找出自动门点并建立映射
const pens = this.editorService.data().pens;
let autoDoorCount = 0;
pens.forEach((pen) => {
if (pen.name === 'point' && (pen as MapPen).point?.type === MapPointType.) {
const deviceId = (pen as MapPen).point?.deviceId;
if (deviceId && pen.id) {
this.deviceIdToPointIdMap.set(deviceId, pen.id);
autoDoorCount++;
}
}
});
console.log(`🚪 自动门点映射初始化完成: 共找到 ${autoDoorCount} 个自动门点`);
}
/**
*
*/
refreshDeviceMapping(): void {
this.initializeDeviceMapping();
}
/**
*
* 使WebSocket数据
* @param config
*/
startSimulation(config: AutoDoorSimulationConfig): void {
const { deviceId, interval = 3000, initialStatus = 0, enableLogging = true } = config;
// 如果已经存在相同设备ID的模拟先停止它
this.stopSimulation(deviceId);
if (enableLogging) {
console.log(`🚪 启动自动门点模拟: ${deviceId}`);
}
// 设置初始状态
this.statusMap.set(deviceId, initialStatus);
// 创建定时器
const timer = setInterval(() => {
const currentStatus = this.statusMap.get(deviceId) ?? 0;
const newStatus = currentStatus === 0 ? 1 : 0;
// 更新状态
this.statusMap.set(deviceId, newStatus);
// 注意:这里不需要创建完整的模拟数据对象,只需要更新状态
if (enableLogging) {
console.log(`🚪 自动门点状态更新: ${newStatus === 0 ? '关门(红色)' : '开门(蓝色)'} (deviceId: ${deviceId})`);
}
// 更新编辑器中的自动门点状态
if (this.editorService) {
this.editorService.updateAutoDoorByDeviceId(deviceId, newStatus, true);
} else {
console.warn('⚠️ 编辑器服务未设置,无法更新自动门点状态');
}
}, interval);
// 保存定时器引用
this.timers.set(deviceId, timer);
}
/**
*
* @param deviceId ID
*/
stopSimulation(deviceId: string): void {
const timer = this.timers.get(deviceId);
if (timer) {
clearInterval(timer);
this.timers.delete(deviceId);
this.statusMap.delete(deviceId);
console.log(`🚪 停止自动门点模拟: ${deviceId}`);
}
}
/**
*
*/
stopAllSimulations(): void {
for (const deviceId of this.timers.keys()) {
this.stopSimulation(deviceId);
}
console.log('🚪 停止所有自动门点模拟');
}
/**
*
*/
getActiveSimulations(): string[] {
return Array.from(this.timers.keys());
}
/**
*
* @param deviceId ID
*/
getCurrentStatus(deviceId: string): 0 | 1 | undefined {
return this.statusMap.get(deviceId);
}
/**
*
* @param deviceId ID
* @param status
*/
setDeviceStatus(deviceId: string, status: 0 | 1): void {
this.statusMap.set(deviceId, status);
if (this.editorService) {
this.editorService.updateAutoDoorByDeviceId(deviceId, status, true);
console.log(`🚪 手动设置自动门点状态: ${status === 0 ? '关门(红色)' : '开门(蓝色)'} (deviceId: ${deviceId})`);
}
}
/**
*
* @param configs
*/
startMultipleSimulations(configs: AutoDoorSimulationConfig[]): void {
configs.forEach((config) => this.startSimulation(config));
}
/**
* WebSocket推送的自动门点数据
* @param data WebSocket推送的数据
*/
handleWebSocketData(data: AutoDoorWebSocketData): void {
const { id: deviceId, deviceStatus, active = true } = data;
if (!deviceId || deviceStatus === undefined) {
console.warn('⚠️ 自动门点数据格式不正确', data);
return;
}
// 缓存最新数据
this.latestAutoDoorData.set(deviceId, { deviceId, deviceStatus, active });
console.log(
`🚪 收到自动门点WebSocket数据: ${deviceStatus === 0 ? '关门(红色)' : '开门(蓝色)'} (deviceId: ${deviceId})`,
);
}
/**
*
* @param frameBudget
* @param startTime
* @returns
*/
processBufferedData(frameBudget: number, startTime: number): boolean {
// 在时间预算内,持续处理自动门点数据
while (performance.now() - startTime < frameBudget && this.latestAutoDoorData.size > 0) {
// 获取并移除 Map 中的第一条自动门点数据
const entry = this.latestAutoDoorData.entries().next().value;
if (!entry) break;
const [deviceId, data] = entry;
this.latestAutoDoorData.delete(deviceId);
// 使用映射缓存快速查找点位ID
const pointId = this.deviceIdToPointIdMap.get(data.deviceId);
if (!pointId) {
console.warn(`⚠️ 未找到设备ID ${data.deviceId} 对应的自动门点,可能需要刷新映射`);
continue;
}
// 更新自动门点状态使用pointId直接更新避免查找
if (this.editorService) {
this.editorService.updateAutoDoorByDeviceId(data.deviceId, data.deviceStatus, data.active, pointId);
}
}
// 返回是否还有待处理的数据
return this.latestAutoDoorData.size > 0;
}
/**
*
*/
getBufferedDataCount(): number {
return this.latestAutoDoorData.size;
}
/**
*
*/
clearBufferedData(): void {
this.latestAutoDoorData.clear();
}
/**
*
*/
getMappingCount(): number {
return this.deviceIdToPointIdMap.size;
}
/**
* ID是否有对应的自动门点
*/
hasDeviceMapping(deviceId: string): boolean {
return this.deviceIdToPointIdMap.has(deviceId);
}
/**
* ID列表
*/
getMappedDeviceIds(): string[] {
return Array.from(this.deviceIdToPointIdMap.keys());
}
/**
*
* @param deviceId ID
* @param label
* @param deviceStatus
*/
private createMockData(deviceId: string, label: string, deviceStatus: 0 | 1): AutoDoorStatusData {
return {
gid: '',
id: deviceId,
label,
brand: null,
type: 99,
ip: null,
battery: 0,
isConnected: true,
state: 0,
canOrder: false,
canStop: null,
canControl: false,
targetPoint: null,
deviceStatus,
x: 0,
y: 0,
active: true,
angle: 0,
isWaring: null,
isFault: null,
isLoading: null,
path: [],
};
}
/**
*
*/
destroy(): void {
this.stopAllSimulations();
this.clearBufferedData();
this.deviceIdToPointIdMap.clear();
this.editorService = null;
console.log('🚪 自动门点服务已销毁');
}
}
/**
*
*/
export const autoDoorService = new AutoDoorService();
// 保持向后兼容
export const autoDoorSimulationService = autoDoorService;

View File

@ -839,6 +839,55 @@ export class EditorService extends Meta2d {
this.updatePen(pointId, { statusStyle: color }, false);
}
/**
* ID更新自动门点状态
* @param deviceId ID
* @param deviceStatus 0=1=
* @param active
* @param pointId ID使
*/
public updateAutoDoorByDeviceId(deviceId: string, deviceStatus: number, active = true, pointId?: string): void {
let autoDoorPointId = pointId;
// 如果没有提供pointId则通过deviceId查找
if (!autoDoorPointId) {
const autoDoorPoint = this.data().pens.find(
(pen) =>
pen.name === 'point' &&
(pen as MapPen).point?.type === MapPointType. &&
(pen as MapPen).point?.deviceId === deviceId,
) as MapPen | undefined;
if (!autoDoorPoint?.id) {
console.warn(`未找到设备ID为 ${deviceId} 的自动门点`);
return;
}
autoDoorPointId = autoDoorPoint.id;
}
// 通过pointId获取点位对象
const autoDoorPoint = this.getPenById(autoDoorPointId) as MapPen | undefined;
if (!autoDoorPoint?.point) {
console.warn(`自动门点 ${autoDoorPointId} 不存在或数据异常`);
return;
}
// 更新自动门点状态
this.updatePen(
autoDoorPointId,
{
point: {
...autoDoorPoint.point,
deviceStatus,
active,
},
},
false,
);
}
public updatePointLockIcon(pointId: string, show: boolean): void {
const pointPen = this.getPenById(pointId);
if (!pointPen || pointPen.name !== 'point') return;
@ -1301,10 +1350,33 @@ function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
const theme = sTheme.editor;
const { active, iconSize: r = 0, fontSize = 14, lineHeight = 1.5, fontFamily } = pen.calculative ?? {};
const { x = 0, y = 0, width: w = 0, height: h = 0 } = pen.calculative?.worldRect ?? {};
const { type } = pen.point ?? {};
const { type, deviceStatus, active: pointActive } = pen.point ?? {};
const { label = '', statusStyle } = pen ?? {};
ctx.save();
// 为自动门点绘制光圈
if (type === MapPointType. && pointActive && deviceStatus !== undefined) {
const ox = x + w / 2;
const oy = y + h / 2;
console.log(ox, oy);
const haloRadius = Math.max(w, h) / 2 + 6; // 光圈半径比点位本身大一些
ctx.ellipse(ox, oy, haloRadius, haloRadius, 0, 0, Math.PI * 2);
// 根据设备状态选择颜色0=关门红色1=开门(蓝色)
if (deviceStatus === 0) {
ctx.fillStyle = get(theme, 'autoDoor.fill-closed') ?? '#FF4D4F33';
ctx.strokeStyle = get(theme, 'autoDoor.stroke-closed') ?? '#FF4D4F99';
} else {
ctx.fillStyle = get(theme, 'autoDoor.fill-open') ?? '#1890FF33';
ctx.strokeStyle = get(theme, 'autoDoor.stroke-open') ?? '#1890FF99';
}
ctx.fill();
ctx.stroke();
}
switch (type) {
case MapPointType.:
case MapPointType.: