feat(auto-door): 新增自动门设备状态模拟功能,优化门区域与路段的状态同步逻辑

This commit is contained in:
xudan 2025-10-21 10:30:09 +08:00
parent a0e33b2e86
commit 11cdde454c
4 changed files with 195 additions and 79 deletions

View File

@ -12,7 +12,18 @@ type Props = {
const props = defineProps<Props>();
const editor = inject(props.token)!;
const pen = computed<MapPen | undefined>(() => editor.value.getPenById(props.current));
// /
const areasTick = computed<string>(() =>
editor.value.areas.value
.map((v: any) => `${v.id}:${v?.area?.deviceStatus ?? ''}:${v?.area?.isConnected ?? ''}`)
.join('|'),
);
const pen = computed<MapPen | undefined>(() => {
// areasTick
void areasTick.value;
return editor.value.getPenById(props.current);
});
const area = computed<MapAreaInfo | null>(() => {
const v = pen.value?.area;
if (!v?.type) return null;
@ -61,6 +72,8 @@ const ruleText = computed(() => {
if (area.value?.inoutflag === 2) return '后进先出';
return '';
});
//
</script>
<template>
@ -106,6 +119,22 @@ const ruleText = computed(() => {
<a-typography-text>{{ area.doorDeviceId || $t('无') }}</a-typography-text>
</a-flex>
</a-list-item>
<a-list-item v-if="area && (area.type as any) === DOOR_AREA_TYPE">
<a-flex :gap="8" vertical>
<a-typography-text type="secondary">{{ $t('连接状态') }}</a-typography-text>
<a-typography-text>{{
area.isConnected === true ? $t('已连接') : area.isConnected === false ? $t('未连接') : $t('无')
}}</a-typography-text>
</a-flex>
</a-list-item>
<a-list-item v-if="area && (area.type as any) === DOOR_AREA_TYPE">
<a-flex :gap="8" vertical>
<a-typography-text type="secondary">{{ $t('门状态') }}</a-typography-text>
<a-typography-text>
{{ area.deviceStatus === 1 ? $t('开门') : area.deviceStatus === 0 ? $t('关门') : $t('无') }}
</a-typography-text>
</a-flex>
</a-list-item>
<a-list-item v-if="area && (area.type as any) === DOOR_AREA_TYPE">
<a-flex :gap="8" vertical>
<a-typography-text type="secondary">{{ $t('已绑定路段') }}</a-typography-text>

View File

@ -1,4 +1,5 @@
<script setup lang="ts">
import { DOOR_AREA_TYPE } from '@api/map/door-area';
import { LockState } from '@meta2d/core';
import { message } from 'ant-design-vue';
import dayjs, { type Dayjs } from 'dayjs';
@ -76,6 +77,9 @@ const container = shallowRef<HTMLDivElement>();
const editor = shallowRef<EditorService>();
const storageLocationService = shallowRef<StorageLocationService>();
const client = shallowRef<WebSocket>();
// WS使
let doorMockTimer: number | undefined;
let doorMockStatus: 0 | 1 = 0;
// Ctrl/Cmd+F
const leftSiderEl = shallowRef<HTMLElement>();
const isPlaybackControllerVisible = ref<boolean>(true);
@ -379,6 +383,59 @@ const monitorScene = async () => {
originalOnClose.call(ws, event);
}
};
// /DOOR_MOCKWS
try {
// DEV localStorage.DOOR_MOCK === '1'
const enableMock =
(typeof localStorage !== 'undefined' && localStorage.getItem('DOOR_MOCK') === '1') ||
import.meta?.env?.DEV === true;
if (enableMock) {
if (doorMockTimer) window.clearInterval(doorMockTimer);
// 使ID退 door1
let mockDeviceId = 'door1';
try {
const areas: any[] = (editor.value?.find('area') || []) as any[];
const doorAreas = areas.filter((a: any) => (a.area?.type as any) === (DOOR_AREA_TYPE as any));
const first = doorAreas[0];
const did = first?.area?.doorDeviceId;
if (did) mockDeviceId = did;
} catch {
//
}
doorMockTimer = window.setInterval(() => {
doorMockStatus = doorMockStatus === 0 ? 1 : 0;
const mock: AutoDoorWebSocketData = {
gid: 'mock-gid',
id: mockDeviceId,
label: mockDeviceId,
brand: null,
type: 99,
ip: null,
battery: 100,
isConnected: true,
state: 0,
canOrder: true,
canStop: null,
canControl: true,
targetPoint: null,
deviceStatus: doorMockStatus,
x: 0,
y: 0,
active: true,
angle: 0,
isWaring: null,
isFault: null,
isLoading: null,
path: [],
};
autoDoorSimulationService.handleWebSocketData(mock);
}, 2000);
}
} catch {
//
}
};
//#endregion
@ -389,6 +446,16 @@ onMounted(() => {
// editor store
editorStore.setEditor(editor as ShallowRef<EditorService>);
// WS//
try {
if (editor.value) {
console.log('[DoorWS] bind editor to door service');
autoDoorSimulationService.setEditorService(editor.value);
}
} catch {
//
}
storageLocationService.value = new StorageLocationService(editor.value, props.sid);
container.value?.addEventListener('pointerdown', handleCanvasPointerDown, true);
});
@ -424,7 +491,6 @@ onMounted(async () => {
contextMenuState.value = state;
});
//
document.addEventListener('click', handleGlobalClick);
document.addEventListener('keydown', handleGlobalKeydown);
@ -438,6 +504,10 @@ onUnmounted(() => {
storageLocationService.value?.destroy();
//
autoDoorSimulationService.clearBufferedData();
if (doorMockTimer) {
window.clearInterval(doorMockTimer);
doorMockTimer = undefined;
}
// EditorService
if (editor.value) {

View File

@ -100,12 +100,6 @@ type SaveScenePayload = {
png?: string;
};
type FloorViewState = {
scale: number;
x: number;
y: number;
};
const syncDoorDeviceToRoutes = () => {
const pens = editor.value?.data()?.pens || [];
pens.forEach((pen: any) => {
@ -126,7 +120,11 @@ const syncDoorDeviceToRoutes = () => {
};
const saveScene = async (payload?: SaveScenePayload) => {
// deviceId
try { syncDoorDeviceToRoutes(); } catch {}
try {
syncDoorDeviceToRoutes();
} catch {
//
}
const currentJson = payload?.json ?? editor.value?.save();
if (currentJson) {
try {
@ -320,26 +318,6 @@ const addFloor = () => {
});
};
const mergeSceneObjects = (scenes: any[]): any => {
if (!scenes || scenes.length === 0) {
return {};
}
return scenes.reduce((merged, currentScene) => {
const newMerged = { ...merged, ...currentScene };
if (currentScene.pens && Array.isArray(currentScene.pens)) {
const penMap = new Map(merged.pens?.map((p: any) => [p.id, p]) ?? []);
currentScene.pens.forEach((pen: any) => {
if (pen.id) {
penMap.set(pen.id, pen);
}
});
newMerged.pens = Array.from(penMap.values());
}
return newMerged;
}, {});
};
const openImportModal = () => {
importMode.value = 'normal';
importModalVisible.value = true;
@ -417,7 +395,9 @@ const handleImportConfirm = async () => {
console.log('[MapConverter] Request payload (object):', payload);
console.log('[MapConverter] Request payload (JSON):', JSON.stringify(payload, null, 2));
message.info('已在控制台打印请求参数');
} catch {}
} catch {
//
}
// 3)
const result = await postProcessJsonList(payload);

View File

@ -153,6 +153,19 @@ export interface AutoDoorWebSocketData {
*/
export class AutoDoorService {
// 日志开关:默认关闭;可通过 localStorage.DOOR_LOG='1' 或 VITE_LOG_DOOR='1' 开启
private readonly DEBUG = (() => {
try {
const ls = typeof localStorage !== 'undefined' && localStorage.getItem('DOOR_LOG') === '1';
const env = typeof import.meta !== 'undefined' && (import.meta as any)?.env?.VITE_LOG_DOOR === '1';
return !!(ls || env);
} catch {
return false;
}
})();
private debugLog = (...args: any[]) => {
if (this.DEBUG) console.log('[DoorWS]', ...args);
};
private timers = new Map<string, NodeJS.Timeout>();
private statusMap = new Map<string, 0 | 1>();
@ -182,6 +195,7 @@ export class AutoDoorService {
// 初始化设备ID到点位ID的映射关系
this.initializeDeviceMapping();
this.debugLog('setEditorService -> editor attached, mapping initialized');
}
/**
@ -224,6 +238,12 @@ export class AutoDoorService {
}
}
});
this.debugLog(
'initializeDeviceMapping -> found autoDoor points:',
autoDoorCount,
'map keys:',
Array.from(this.deviceIdToPointIdMap.keys()),
);
}
/**
@ -391,7 +411,11 @@ export class AutoDoorService {
*/
handleWebSocketData(data: AutoDoorWebSocketData): void {
const { label: deviceId, deviceStatus, active = true, isConnected = true } = data;
// 兼容不同字段:优先使用 id 作为设备ID退回 label
const deviceId = (data as any)?.id || (data as any)?.label;
const deviceStatus = (data as any)?.deviceStatus;
const active = (data as any)?.active ?? true;
const isConnected = (data as any)?.isConnected ?? true;
if (!deviceId || deviceStatus === undefined) {
console.warn('⚠️ 自动门点数据格式不正确', data);
@ -402,6 +426,13 @@ export class AutoDoorService {
// 缓存最新数据
this.latestAutoDoorData.set(deviceId, { deviceId, deviceStatus, active, isConnected });
this.debugLog('handleWebSocketData -> buffered', {
deviceId,
deviceStatus,
active,
isConnected,
bufferSize: this.latestAutoDoorData.size,
});
// console.log(
@ -431,6 +462,8 @@ export class AutoDoorService {
// 在时间预算内,智能处理自动门点数据
if (this.latestAutoDoorData.size > 0)
this.debugLog('processBufferedData -> start, buffer size:', this.latestAutoDoorData.size);
while (
performance.now() - startTime < frameBudget &&
this.latestAutoDoorData.size > 0 &&
@ -447,69 +480,73 @@ export class AutoDoorService {
// 使用映射缓存快速查找点位ID列表
const pointIds = this.deviceIdToPointIdMap.get(data.deviceId);
this.debugLog('processBufferedData -> processing', data, 'mapped pointIds:', pointIds);
if (!pointIds || pointIds.length === 0) {
console.warn(`⚠️ 未找到设备ID ${data.deviceId} 对应的自动门点,可能需要刷新映射`);
continue;
}
// 批量更新该设备对应的所有自动门点状态
// 批量更新该设备对应的所有自动门点状态(若存在自动门点)
if (this.editorService) {
// 使用批量更新减少渲染调用
const updates = pointIds.map((pid) => ({
id: pid,
deviceId: data.deviceId,
deviceStatus: data.deviceStatus,
isConnected: data.isConnected,
active: data.active,
}));
// 批量执行更新
updates.forEach((update) => {
this.editorService!.updateAutoDoorByDeviceId(
update.deviceId,
update.deviceStatus,
update.isConnected,
update.active,
update.id,
);
});
if (!pointIds || pointIds.length === 0) {
console.warn(`⚠️ 未找到设备ID ${data.deviceId} 对应的自动门点,跳过点位更新,但会同步更新路段与门区域`);
} else {
// 使用批量更新减少渲染调用
const updates = pointIds.map((pid) => ({
id: pid,
deviceId: data.deviceId,
deviceStatus: data.deviceStatus,
isConnected: data.isConnected,
active: data.active,
}));
// 批量执行更新
updates.forEach((update) => {
this.editorService!.updateAutoDoorByDeviceId(
update.deviceId,
update.deviceStatus,
update.isConnected,
update.active,
update.id,
);
this.debugLog('update point by deviceId ->', update);
});
}
// 同步更新:将门设备状态写入绑定该设备的路段与门区域
try {
const routes = this.editorService.find('route').filter((r: any) => r.route?.deviceId === data.deviceId);
this.debugLog('sync to routes -> count:', routes.length);
routes.forEach((r: any) => {
const merged = { ...(r.route || {}), deviceStatus: data.deviceStatus, isConnected: data.isConnected };
this.editorService!.setValue({ id: r.id, route: merged }, { render: true, history: false, doEvent: false });
const patch = { deviceStatus: data.deviceStatus, isConnected: data.isConnected } as any;
this.editorService!.updateRoute(r.id, patch);
this.debugLog('route updated ->', r.id, patch);
});
const areas = this.editorService
// 兼容匹配:
// 1) doorDeviceId === deviceId
// 2) 或绑定路段中存在 route.deviceId === deviceId
const allDoorAreas = this.editorService
.find('area')
.filter((a: any) => (a.area?.type as any) === (15 as any) && a.area?.doorDeviceId === data.deviceId);
areas.forEach((a: any) => {
const mergedArea = { ...(a.area || {}), deviceStatus: data.deviceStatus, isConnected: data.isConnected };
.filter((a: any) => (a.area?.type as any) === (15 as any));
this.editorService!.setValue(
{ id: a.id, area: mergedArea },
{ render: true, history: false, doEvent: false },
);
const areas = allDoorAreas.filter((a: any) => {
if (a.area?.doorDeviceId === data.deviceId) return true;
const rids: string[] = a.area?.routes || [];
for (const rid of rids) {
const rpen: any = this.editorService!.getPenById(rid);
if (rpen?.route?.deviceId === data.deviceId) return true;
}
return false;
});
} catch {}
this.debugLog('sync to door areas -> count:', areas.length, 'deviceId:', data.deviceId);
areas.forEach((a: any) => {
const patch = { deviceStatus: data.deviceStatus, isConnected: data.isConnected } as any;
this.editorService!.updateArea(a.id, patch);
this.debugLog('door area updated ->', a.id, patch);
});
} catch {
//
}
}
processedCount++;