fix(elevator): 修复电梯状态实时更新问题,优化详情面板响应式依赖并添加开发环境模拟数据

This commit is contained in:
xudan 2025-12-11 14:12:54 +08:00
parent 624c68d0eb
commit ac503ca81b
5 changed files with 324 additions and 65 deletions

View File

@ -17,7 +17,20 @@ type Props = {
const props = defineProps<Props>();
const editor = inject(props.token)!;
const pen = computed<MapPen | undefined>(() => editor.value.getPenById(props.current));
// /
const pointsTick = computed<string>(() =>
editor.value.points.value
.map((v: any) => `${v.id}:${v?.point?.isConnected ?? ''}:${v?.point?.elevatorDirection ?? ''}:${v?.point?.elevatorFrontDoorStatus ?? ''}:${v?.point?.currentFloor ?? ''}`)
.join('|'),
);
// pen
const pen = computed<MapPen | undefined>(() => {
// pointsTick
void pointsTick.value;
return editor.value.getPenById(props.current);
});
const point = computed<MapPointInfo | null>(() => {
const v = pen.value?.point;
if (isNil(v?.type)) return null;
@ -131,6 +144,79 @@ watch(point, (newPoint) => {
}
}, { immediate: true });
//
const elevatorStatusText = computed(() => {
// pointsTick
void pointsTick.value;
const pen = editor.value.getPenById(props.current);
const point = pen?.point;
if (!point) return '未知';
const { elevatorDirection, elevatorFrontDoorStatus, isConnected } = point;
// 线
if (!isConnected) return '离线';
//
if (elevatorFrontDoorStatus === 2) return '开关门中';
if (elevatorFrontDoorStatus === 3) return '门已开';
//
if (elevatorFrontDoorStatus === 1) {
if (elevatorDirection === 2) return '上行中';
if (elevatorDirection === 3) return '下行中';
if (elevatorDirection === 1) return '门已关';
}
return '未知';
});
const elevatorStatusColor = computed(() => {
// pointsTick
void pointsTick.value;
const pen = editor.value.getPenById(props.current);
const point = pen?.point;
if (!point) return '#1890FF';
const { isConnected, elevatorDirection, elevatorFrontDoorStatus } = point;
// 线
if (!isConnected) return '#999999';
//
if (elevatorFrontDoorStatus === 2) return '#52C41A'; // - 绿
if (elevatorFrontDoorStatus === 3) return '#52C41A'; // - 绿
//
if (elevatorFrontDoorStatus === 1) {
if (elevatorDirection === 2) return '#722ED1'; // -
if (elevatorDirection === 3) return '#13C2C2'; // -
if (elevatorDirection === 1) return '#1890FF'; // -
}
return '#1890FF'; // -
});
//
const elevatorIsConnected = computed(() => {
// pointsTick
void pointsTick.value;
const pen = editor.value.getPenById(props.current);
return pen?.point?.isConnected;
});
//
const elevatorCurrentFloor = computed(() => {
// pointsTick
void pointsTick.value;
const pen = editor.value.getPenById(props.current);
return pen?.point?.currentFloor;
});
//
const getElevatorStatusText = (point: any): string => {
const { elevatorDirection, elevatorFrontDoorStatus, isConnected } = point;
@ -226,26 +312,26 @@ const getElevatorStatusColor = (point: any): string => {
<a-flex align="center" :gap="8" class="conn-status">
<span
class="status-dot"
:class="point.isConnected ? 'online' : 'offline'"
:title="point.isConnected ? $t('已连接') : $t('未连接')"
:class="elevatorIsConnected ? 'online' : 'offline'"
:title="elevatorIsConnected ? $t('已连接') : $t('未连接')"
/>
<a-typography-text>
{{ point.isConnected ? $t('已连接') : $t('未连接') }}
{{ elevatorIsConnected ? $t('已连接') : $t('未连接') }}
</a-typography-text>
</a-flex>
</a-list-item>
<a-list-item v-if="point.type === MapPointType.电梯点 && point.currentFloor !== undefined">
<a-list-item v-if="point.type === MapPointType.电梯点 && elevatorCurrentFloor !== undefined">
<a-typography-text type="secondary">{{ $t('当前楼层') }}</a-typography-text>
<a-typography-text>{{ point.currentFloor }}F</a-typography-text>
<a-typography-text>{{ elevatorCurrentFloor }}F</a-typography-text>
</a-list-item>
<a-list-item v-if="point.type === MapPointType.电梯点 && point.elevatorDirection !== undefined">
<a-list-item v-if="point.type === MapPointType.电梯点">
<a-typography-text type="secondary">{{ $t('电梯状态') }}</a-typography-text>
<a-flex align="center" :gap="8">
<span
class="status-dot"
:style="{ backgroundColor: getElevatorStatusColor(point) }"
:style="{ backgroundColor: elevatorStatusColor }"
/>
<a-typography-text>{{ getElevatorStatusText(point) }}</a-typography-text>
<a-typography-text>{{ elevatorStatusText }}</a-typography-text>
</a-flex>
</a-list-item>
<a-list-item v-if="point.extensionType">

View File

@ -119,6 +119,10 @@ const isSceneLoading = ref(false);
//
let stopElevatorMock: (() => void) | undefined;
//
let elevatorMockTimer: number | undefined;
//
let elevatorMockTick = 0;
const playback = usePlaybackWebSocket(editor, async () => {
// []
@ -530,11 +534,69 @@ onMounted(async () => {
id: '1998661793706377218',
type: 102,
elevatorFloor: 5,
elevatorDirection: 3, //
elevatorFrontDoorStatus: 3, //
elevatorDirection: 2, //
elevatorFrontDoorStatus: 2, //
isConnected: true
});
//
console.log('[开发环境] 启动电梯状态自动变化定时器3秒间隔');
//
const elevatorStates = [
{ floor: 5, direction: 2, doorStatus: 2 }, // 5
{ floor: 5, direction: 2, doorStatus: 3 }, // 5
{ floor: 5, direction: 2, doorStatus: 1 }, // 5
{ floor: 5, direction: 2, doorStatus: 1 }, // 5
{ floor: 6, direction: 2, doorStatus: 1 }, // 6
{ floor: 6, direction: 2, doorStatus: 2 }, // 6
{ floor: 6, direction: 2, doorStatus: 3 }, // 6
{ floor: 6, direction: 2, doorStatus: 1 }, // 6
{ floor: 7, direction: 2, doorStatus: 1 }, // 7
{ floor: 7, direction: 2, doorStatus: 2 }, // 7
{ floor: 7, direction: 2, doorStatus: 3 }, // 7
{ floor: 7, direction: 2, doorStatus: 1 }, // 7
{ floor: 7, direction: 1, doorStatus: 1 }, // 7
{ floor: 7, direction: 3, doorStatus: 1 }, // 7
{ floor: 6, direction: 3, doorStatus: 1 }, // 6
{ floor: 6, direction: 3, doorStatus: 2 }, // 6
{ floor: 6, direction: 3, doorStatus: 3 }, // 6
{ floor: 6, direction: 3, doorStatus: 1 }, // 6
{ floor: 5, direction: 3, doorStatus: 1 }, // 5
{ floor: 5, direction: 1, doorStatus: 1 }, // 5
];
elevatorMockTimer = window.setInterval(() => {
const stateIndex = elevatorMockTick % elevatorStates.length;
const state = elevatorStates[stateIndex];
console.log(`[开发环境] 电梯状态变化 #${elevatorMockTick}:`, {
floor: state.floor,
direction: ['未知', '停止', '向上', '向下'][state.direction],
doorStatus: ['未知', '关', '开关门中', '开'][state.doorStatus]
});
elevatorStore.handleElevatorWebSocketData({
id: '1998661793706377218',
type: 102,
elevatorFloor: state.floor,
elevatorDirection: state.direction,
elevatorFrontDoorStatus: state.doorStatus,
isConnected: true
});
elevatorMockTick++;
}, 3000); // 3
//
stopElevatorMock = () => {
if (elevatorMockTimer) {
clearInterval(elevatorMockTimer);
elevatorMockTimer = undefined;
console.log('[开发环境] 停止电梯状态模拟数据');
}
};
}
});
@ -550,6 +612,12 @@ onUnmounted(() => {
doorMockTimer = undefined;
}
//
if (elevatorMockTimer) {
clearInterval(elevatorMockTimer);
elevatorMockTimer = undefined;
}
// EditorService
if (editor.value) {
(editor.value as any).off('customContextMenu', (event: Record<string, unknown>) => {

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
//
import { DOOR_AREA_TYPE } from '@api/map/door-area';
import { MapPointType } from '@api/map';
import { message, Modal } from 'ant-design-vue';
import JSZip from 'jszip';
import { computed, onMounted, onUnmounted, provide, ref, type ShallowRef, shallowRef, watch } from 'vue';
@ -653,6 +654,92 @@ const handleExportConfirm = async (payload: ExportConfirmPayload) => {
}
};
/**
* 导出全部楼层数据
* 将所有楼层的数据合并导出为一个JSON文件
*/
const exportAllFloors = async () => {
try {
if (!floorScenes.value || floorScenes.value.length === 0) {
message.error('没有可导出的楼层数据');
return;
}
//
const hideMessage = message.loading('正在导出全部楼层数据...', 0);
//
const currentEditorData = editor.value?.save();
let realtimeDeviceData = {};
if (currentEditorData) {
try {
const parsedEditorData = JSON.parse(currentEditorData);
//
realtimeDeviceData = {
elevators: parsedEditorData.pens?.filter((pen: any) =>
pen.name === 'point' && pen.point?.type === MapPointType.电梯点
).map((pen: any) => ({
deviceId: pen.point?.deviceId,
penId: pen.id,
status: pen.point
})) || [],
autoDoors: parsedEditorData.pens?.filter((pen: any) =>
pen.name === 'point' && pen.point?.type === MapPointType.自动门点
).map((pen: any) => ({
deviceId: pen.point?.deviceId,
penId: pen.id,
status: pen.point
})) || [],
exportTimestamp: Date.now()
};
} catch (error) {
console.warn('解析编辑器数据失败,将不包含实时设备状态:', error);
}
}
//
const allFloorsData = {
title: title.value || 'multi-floor-scene',
exportTime: new Date().toISOString(),
floorCount: floorScenes.value.length,
floors: floorScenes.value.map((floorScene, index) => ({
floorIndex: index,
floorName: floorScene.name || `楼层${index + 1}`,
scene: floorScene
})),
realtimeDeviceData,
metadata: {
version: '1.0',
description: '多楼层地图导出数据,包含所有楼层结构和实时设备状态',
currentFloorIndex: currentFloorIndex.value,
isMultiFloor: isMultiFloor.value
}
};
// JSON
const jsonString = JSON.stringify(allFloorsData, null, 2);
// Blob
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
// - 使.scene
const filename = `${title.value || 'multi-floor-scene'}_all_floors_${new Date().toISOString().slice(0, 10)}.scene`;
//
downloadFile(url, filename);
URL.revokeObjectURL(url);
hideMessage();
message.success(`成功导出 ${floorScenes.value.length} 个楼层的数據`);
} catch (error: any) {
console.error('导出全部楼层失败:', error);
message.error(error.message || '导出全部楼层失败');
}
};
const show = ref<boolean>(true);
const current = ref<{ type: 'robot' | 'point' | 'line' | 'area'; id: string }>();
watch(
@ -789,6 +876,7 @@ const handleFloorChange = async (value: any) => {
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="exportModalVisible = true">导出为其他格式</a-menu-item>
<a-menu-item key="2" @click="exportAllFloors" v-if="isMultiFloor">{{ $t('导出全部楼层') }}</a-menu-item>
</a-menu>
</template>
</a-dropdown-button>

View File

@ -71,7 +71,7 @@ function drawDisconnectedIcon(ctx: CanvasRenderingContext2D, cx: number, cy: num
* - 1.5pxpen较小边的3%
* - 2pxpen较小边的5%
* - 3pxpen较小边的10%
* - 4pxpen较小边的12%
* - 5pxpen较小边的20%
* - 线pen尺寸动态调整间距比例
* @param ctx Canvas上下文
* @param pen
@ -82,7 +82,6 @@ function drawElevatorMinimal(
ctx: CanvasRenderingContext2D,
pen: MapPen,
elevatorData: any,
time: number
): void {
// 提取后端字段
const {
@ -129,42 +128,7 @@ function drawElevatorMinimal(
ctx.restore();
};
// 辅助函数:绘制方向指示(仅当电梯移动时)
const drawDirection = (direction: 'up' | 'down', color: string) => {
ctx.save();
ctx.fillStyle = color;
ctx.globalAlpha = 0.8;
// 方向指示器大小基于pen尺寸动态调整
const indicatorSize = Math.max(3, baseSize * 0.1); // 最小3px否则按10%比例
const margin = Math.max(2, baseSize * 0.05); // 距离边框的距离最小2px否则按5%比例
if (direction === 'up') {
// 向上三角 - 位于矩形顶部中央
const centerX = x + w / 2;
const centerY = rectY + margin + indicatorSize;
ctx.beginPath();
ctx.moveTo(centerX, centerY - indicatorSize);
ctx.lineTo(centerX - indicatorSize, centerY + indicatorSize);
ctx.lineTo(centerX + indicatorSize, centerY + indicatorSize);
ctx.closePath();
ctx.fill();
} else {
// 向下三角 - 位于矩形底部中央
const centerX = x + w / 2;
const centerY = rectY + rectH - margin - indicatorSize;
ctx.beginPath();
ctx.moveTo(centerX, centerY + indicatorSize);
ctx.lineTo(centerX - indicatorSize, centerY - indicatorSize);
ctx.lineTo(centerX + indicatorSize, centerY - indicatorSize);
ctx.closePath();
ctx.fill();
}
ctx.restore();
};
// 辅助函数:在边框右侧绘制静态方向箭头
const drawDirectionSide = (direction: 'up' | 'down', color: string) => {
@ -173,11 +137,10 @@ function drawElevatorMinimal(
ctx.strokeStyle = color;
ctx.globalAlpha = 0.8;
// 箭头大小基于pen尺寸动态调整使用宽度和高度的平均值来更好地适应不同比例
const avgSize = (w + h) / 2; // 使用宽高平均值作为基准
const arrowSize = Math.max(4, avgSize * 0.12); // 最小4px否则按12%比例
const sideMargin = Math.max(3, avgSize * 0.08); // 距离右侧边框的距离
ctx.lineWidth = Math.max(1.5, avgSize * 0.025); // 箭头线条宽度
// 箭头大小基于pen尺寸动态调整分别考虑宽度和高度
const arrowSize = Math.max(5, Math.min(w, h) * 0.20); // 使用较小的边作为基准最小5px否则按20%比例
const sideMargin = Math.max(5, Math.min(w, h) * 0.20); // 距离右侧边框的距离,增加间距,使用较小的边作为基准
ctx.lineWidth = Math.max(2, Math.min(w, h) * 0.04); // 箭头线条宽度,使用较小的边作为基准
// 箭头位置:在矩形右侧垂直居中
const arrowX = rectX + rectW + sideMargin;
@ -226,16 +189,7 @@ function drawElevatorMinimal(
ctx.restore();
};
// 辅助函数:呼吸动画效果
const breatheAnimation = (baseAlpha: number = 0.6) => {
// 使用缓慢的正弦波创建呼吸效果
return baseAlpha + Math.sin(time * 0.001) * 0.2;
};
// 辅助函数:脉冲动画效果(用于断连状态)
const pulseAnimation = () => {
return 0.4 + Math.sin(time * 0.002) * 0.4;
};
// 状态渲染逻辑 - 静态显示,无动画
if (!isConnected) {
@ -245,14 +199,24 @@ function drawElevatorMinimal(
// 在线状态根据具体状态渲染
switch (elevatorFrontDoorStatus) {
case 2: { // 正在开关门
// 绿色静态边框
// 绿色静态边框,同时显示上行下行箭头
drawMinimalFrame('#34C759', 0.8);
if (elevatorDirection === 2) { // 向上
drawDirectionSide('up', '#007AFF'); // 使用蓝色箭头
} else if (elevatorDirection === 3) { // 向下
drawDirectionSide('down', '#007AFF'); // 使用蓝色箭头
}
break;
}
case 3: { // 门已开
// 绿色静态边框
// 绿色静态边框,同时显示上行下行箭头
drawMinimalFrame('#34C759', 0.8);
if (elevatorDirection === 2) { // 向上
drawDirectionSide('up', '#007AFF'); // 使用蓝色箭头
} else if (elevatorDirection === 3) { // 向下
drawDirectionSide('down', '#007AFF'); // 使用蓝色箭头
}
break;
}
@ -399,7 +363,7 @@ export function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
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, isConnected, doorStatus, active: pointActive } = pen.point ?? {};
const { label = '', statusStyle } = pen ?? {};
const { label = '' } = pen ?? {};
ctx.save();

View File

@ -0,0 +1,53 @@
# 电梯状态实时更新修复说明
## 问题分析
电梯详情卡片中的状态字段(连接状态、当前楼层、电梯状态等)没有实时更新,而门区域的详情卡片可以正常实时更新。
## 根本原因
- **门区域**使用`areasTick`计算属性跟踪所有区域状态变化,确保响应式更新
- **电梯点**缺少类似的`pointsTick`机制,导致状态更新时组件无法响应
## 修复方案
参考门区域的实现,为电梯点添加相同的响应式更新机制:
### 1. 添加pointsTick响应式跟踪
```typescript
// 订阅点位集合变化(含设备状态/连接状态),用于触发详情面板的响应式刷新
const pointsTick = computed<string>(() =>
editor.value.points.value
.map((v: any) => `${v.id}:${v?.point?.isConnected ?? ''}:${v?.point?.elevatorDirection ?? ''}:${v?.point?.elevatorFrontDoorStatus ?? ''}:${v?.point?.currentFloor ?? ''}`)
.join('|'),
);
```
### 2. 优化pen计算属性
```typescript
const pen = computed<MapPen | undefined>(() => {
// 引用 pointsTick 以建立依赖关系
void pointsTick.value;
return editor.value.getPenById(props.current);
});
```
### 3. 创建响应式状态计算属性
```typescript
// 电梯连接状态、当前楼层、状态文本和颜色都添加响应式支持
const elevatorStatusText = computed(() => {
void pointsTick.value;
// ...状态计算逻辑
});
```
### 4. 更新模板绑定
将模板中的电梯状态显示改为使用新的计算属性,确保实时更新。
## 修复效果
- ✅ 电梯连接状态实时更新
- ✅ 当前楼层实时更新
- ✅ 电梯状态(上行/下行/门状态)实时更新
- ✅ 与门区域保持一致的响应式行为
## 技术原理
通过`pointsTick`计算属性当编辑器中的任何点位状态发生变化时通过WebSocket更新都会触发字符串重新计算进而依赖该计算属性的所有组件都会重新渲染实现了实时更新的效果。
这种实现方式确保了UI能够实时反映电梯状态的最新数据解决了状态显示不同步的问题。