feat(elevator): 为电梯点添加设备映射、状态显示和模拟数据支持,以支持电梯设备的实时监控和状态管理
This commit is contained in:
parent
9dc03f6c40
commit
dcd6c54867
105
docs/elevator-status-guide.md
Normal file
105
docs/elevator-status-guide.md
Normal file
@ -0,0 +1,105 @@
|
||||
# 电梯状态监控功能使用指南
|
||||
|
||||
## 功能概述
|
||||
|
||||
电梯状态监控功能允许您:
|
||||
1. 在地图上创建电梯点并绑定电梯设备
|
||||
2. 实时监控电梯的运行状态
|
||||
3. 在状态面板查看所有电梯的详细信息
|
||||
|
||||
## 使用步骤
|
||||
|
||||
### 1. 创建电梯点
|
||||
|
||||
1. 在场景编辑器中,选择点位工具
|
||||
2. 在左侧属性面板中,将点位类型设置为"电梯点"
|
||||
3. 输入点位标签(如:电梯1)
|
||||
4. 保存点位
|
||||
|
||||
### 2. 绑定电梯设备
|
||||
|
||||
1. 点击已创建的电梯点
|
||||
2. 在属性面板中找到"电梯设备"下拉框
|
||||
3. 从下拉列表中选择对应的电梯设备
|
||||
4. 系统自动保存设备ID
|
||||
|
||||
### 3. 查看电梯状态
|
||||
|
||||
#### 方式一:电梯状态面板
|
||||
1. 在左侧边栏点击"电梯状态"标签
|
||||
2. 查看所有电梯的:
|
||||
- 总数、在线数、离线数、故障数
|
||||
- 每个电梯的设备ID、状态、楼层
|
||||
- 点击"定位"按钮聚焦到对应电梯点
|
||||
|
||||
#### 方式二:地图实时状态
|
||||
1. 电梯点的颜色会根据状态变化:
|
||||
- 蓝色:静止/门已关
|
||||
- 绿色:开门中/门已开
|
||||
- 紫色:上行中
|
||||
- 青色:下行中
|
||||
- 橙色:关门中
|
||||
- 红色:故障
|
||||
- 灰色:离线
|
||||
|
||||
#### 方式三:查看详情
|
||||
1. 点击电梯点
|
||||
2. 右侧会显示详情卡片,包含:
|
||||
- 设备ID
|
||||
- 连接状态
|
||||
- 当前楼层
|
||||
- 电梯运行状态
|
||||
|
||||
## 状态说明
|
||||
|
||||
| 状态 | 说明 | 颜色 |
|
||||
|------|------|------|
|
||||
| 静止 | 电梯停止运行 | 蓝色 |
|
||||
| 开门中 | 电梯门正在打开 | 绿色 |
|
||||
| 关门中 | 电梯门正在关闭 | 橙色 |
|
||||
| 上行中 | 电梯正在向上运行 | 紫色 |
|
||||
| 下行中 | 电梯正在向下运行 | 青色 |
|
||||
| 门已开 | 电梯门已经打开 | 绿色 |
|
||||
| 门已关 | 电梯门已经关闭 | 蓝色 |
|
||||
| 故障 | 电梯出现故障 | 红色 |
|
||||
| 离线 | 电梯失去连接 | 灰色 |
|
||||
|
||||
## WebSocket 数据格式
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "2", // 设备ID(必须与绑定的设备ID一致)
|
||||
"type": 102, // 电梯类型标识(固定值)
|
||||
"status": 3, // 电梯状态码(见状态说明表)
|
||||
"floor": 5, // 当前楼层(可选)
|
||||
"isConnected": true // 连接状态
|
||||
}
|
||||
```
|
||||
|
||||
## 开发环境测试
|
||||
|
||||
开发环境已启用模拟数据,包含4个测试电梯:
|
||||
- 设备ID:2 - 1号电梯
|
||||
- 设备ID:4 - 2号电梯
|
||||
- 设备ID:6 - 3号电梯
|
||||
- 设备ID:8 - 4号电梯
|
||||
|
||||
这些设备每3-5秒会自动更新状态,方便测试。
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 电梯点必须绑定设备ID才能接收状态更新
|
||||
2. 设备ID必须与WebSocket推送的ID完全一致
|
||||
3. 离线的电梯会显示为灰色
|
||||
4. 故障电梯会显示为红色并在状态面板中标记
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Q: 为什么电梯状态没有更新?**
|
||||
A: 检查设备ID是否正确绑定,确保与WebSocket推送的ID一致。
|
||||
|
||||
**Q: 如何重置电梯状态?**
|
||||
A: 重新加载页面或WebSocket重新连接后会自动获取最新状态。
|
||||
|
||||
**Q: 设备列表为空怎么办?**
|
||||
A: 检查设备API是否正常,确保后端已配置电梯设备。
|
||||
78
public/icons/elevator/elevator-opening.svg
Normal file
78
public/icons/elevator/elevator-opening.svg
Normal file
@ -0,0 +1,78 @@
|
||||
<svg width="48" height="60" viewBox="0 0 48 60" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<style>
|
||||
.bg-rect {
|
||||
fill: #fff;
|
||||
rx: 7; /* 调整圆角,约等于原15*0.48,或根据视觉效果调整 */
|
||||
ry: 7;
|
||||
}
|
||||
.top-line {
|
||||
fill: none;
|
||||
stroke: #20C29F;
|
||||
stroke-width: 2.5; /* 调整线宽 */
|
||||
stroke-linecap: round;
|
||||
}
|
||||
.outer-rail {
|
||||
fill: #000;
|
||||
}
|
||||
.door-panel {
|
||||
fill: #000;
|
||||
transform-origin: center;
|
||||
}
|
||||
.arrow {
|
||||
fill: #fff;
|
||||
}
|
||||
@keyframes openDoors {
|
||||
0% {
|
||||
transform: translateX(0px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(8px); /* 调整开门距离,原20*0.48=9.6,取8-10 */
|
||||
}
|
||||
}
|
||||
@keyframes openDoorsLeft {
|
||||
0% {
|
||||
transform: translateX(0px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-8px); /* 调整开门距离 */
|
||||
}
|
||||
}
|
||||
.left-door {
|
||||
animation: openDoorsLeft 1.5s ease-out forwards;
|
||||
}
|
||||
.right-door {
|
||||
animation: openDoors 1.5s ease-out forwards;
|
||||
}
|
||||
.arrows-group {
|
||||
opacity: 0;
|
||||
animation: fadeOutArrows 0.5s forwards;
|
||||
}
|
||||
@keyframes fadeOutArrows {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<!-- 背景矩形 -->
|
||||
<rect class="bg-rect" x="0" y="0" width="48" height="60" />
|
||||
<!-- 顶部线条 -->
|
||||
<line class="top-line" x1="12" y1="7.5" x2="36" y2="7.5" /> <!-- 原25-75,缩放0.48,并向上移动 -->
|
||||
<!-- 外侧固定轨道 -->
|
||||
<rect class="outer-rail" x="9.5" y="15" width="2.5" height="30" /> <!-- 宽度原5*0.48=2.4,高度原50*0.6=30 -->
|
||||
<rect class="outer-rail" x="36" y="15" width="2.5" height="30" />
|
||||
<!-- 门板 - 初始状态是关上的 -->
|
||||
<!-- 左门板:宽度原20*0.48=9.6,高度原50*0.6=30。Y轴起始点原30*0.6=18,微调至15 -->
|
||||
<rect class="door-panel left-door" x="12.5" y="15" width="11.5" height="30" />
|
||||
<!-- 右门板:X轴起始点在左门板结束位置+0.5间隙(约24),宽度同左门板 -->
|
||||
<rect class="door-panel right-door" x="24" y="15" width="11.5" height="30" />
|
||||
<!-- 箭头和中间点 - 动画开始时消失 -->
|
||||
<g class="arrows-group">
|
||||
<!-- 上箭头 -->
|
||||
<polygon class="arrow" points="21,19 27,19 24,16" /> <!-- 调整位置和大小 -->
|
||||
<!-- 中间点 -->
|
||||
<circle class="arrow" cx="24" cy="27" r="1.5" /> <!-- 调整位置和大小 -->
|
||||
<!-- 下箭头 -->
|
||||
<polygon class="arrow" points="21,34 27,34 24,37" /> <!-- 调整位置和大小 -->
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
78
public/point/elevator-opening.svg
Normal file
78
public/point/elevator-opening.svg
Normal file
@ -0,0 +1,78 @@
|
||||
<svg width="48" height="60" viewBox="0 0 48 60" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<style>
|
||||
.bg-rect {
|
||||
fill: #fff;
|
||||
rx: 7; /* 调整圆角,约等于原15*0.48,或根据视觉效果调整 */
|
||||
ry: 7;
|
||||
}
|
||||
.top-line {
|
||||
fill: none;
|
||||
stroke: #20C29F;
|
||||
stroke-width: 2.5; /* 调整线宽 */
|
||||
stroke-linecap: round;
|
||||
}
|
||||
.outer-rail {
|
||||
fill: #000;
|
||||
}
|
||||
.door-panel {
|
||||
fill: #000;
|
||||
transform-origin: center;
|
||||
}
|
||||
.arrow {
|
||||
fill: #fff;
|
||||
}
|
||||
@keyframes openDoors {
|
||||
0% {
|
||||
transform: translateX(0px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(8px); /* 调整开门距离,原20*0.48=9.6,取8-10 */
|
||||
}
|
||||
}
|
||||
@keyframes openDoorsLeft {
|
||||
0% {
|
||||
transform: translateX(0px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-8px); /* 调整开门距离 */
|
||||
}
|
||||
}
|
||||
.left-door {
|
||||
animation: openDoorsLeft 1.5s ease-out forwards;
|
||||
}
|
||||
.right-door {
|
||||
animation: openDoors 1.5s ease-out forwards;
|
||||
}
|
||||
.arrows-group {
|
||||
opacity: 0;
|
||||
animation: fadeOutArrows 0.5s forwards;
|
||||
}
|
||||
@keyframes fadeOutArrows {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<!-- 背景矩形 -->
|
||||
<rect class="bg-rect" x="0" y="0" width="48" height="60" />
|
||||
<!-- 顶部线条 -->
|
||||
<line class="top-line" x1="12" y1="7.5" x2="36" y2="7.5" /> <!-- 原25-75,缩放0.48,并向上移动 -->
|
||||
<!-- 外侧固定轨道 -->
|
||||
<rect class="outer-rail" x="9.5" y="15" width="2.5" height="30" /> <!-- 宽度原5*0.48=2.4,高度原50*0.6=30 -->
|
||||
<rect class="outer-rail" x="36" y="15" width="2.5" height="30" />
|
||||
<!-- 门板 - 初始状态是关上的 -->
|
||||
<!-- 左门板:宽度原20*0.48=9.6,高度原50*0.6=30。Y轴起始点原30*0.6=18,微调至15 -->
|
||||
<rect class="door-panel left-door" x="12.5" y="15" width="11.5" height="30" />
|
||||
<!-- 右门板:X轴起始点在左门板结束位置+0.5间隙(约24),宽度同左门板 -->
|
||||
<rect class="door-panel right-door" x="24" y="15" width="11.5" height="30" />
|
||||
<!-- 箭头和中间点 - 动画开始时消失 -->
|
||||
<g class="arrows-group">
|
||||
<!-- 上箭头 -->
|
||||
<polygon class="arrow" points="21,19 27,19 24,16" /> <!-- 调整位置和大小 -->
|
||||
<!-- 中间点 -->
|
||||
<circle class="arrow" cx="24" cy="27" r="1.5" /> <!-- 调整位置和大小 -->
|
||||
<!-- 下箭头 -->
|
||||
<polygon class="arrow" points="21,34 27,34 24,37" /> <!-- 调整位置和大小 -->
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@ -37,6 +37,7 @@ export const DeviceTypeMapping = {
|
||||
* @returns DeviceMappingDTO[] 设备映射列表
|
||||
*/
|
||||
export async function queryDeviceMappingByType(type: string): Promise<DeviceMappingDTO[]> {
|
||||
// 生产环境调用真实API
|
||||
type ResponseType = DeviceMappingDTO[];
|
||||
try {
|
||||
const response = await http.get<ResponseType>(`${API.根据设备类型查询}?type=${type}`);
|
||||
|
||||
@ -33,8 +33,13 @@ export interface MapPointInfo {
|
||||
deviceId?: string; // 设备ID
|
||||
enabled?: 0 | 1; // 是否启用(充电点/停靠点使用,0=禁用,1=启用)
|
||||
doorStatus?: number; // 设备状态(仅自动门点使用,0=关门,1=开门)
|
||||
isConnected?: boolean; // 连接状态(仅自动门点使用,true=已连接,false=未连接)
|
||||
isConnected?: boolean; // 连接状态(自动门点、电梯点使用,true=已连接,false=未连接)
|
||||
active?: boolean; // 是否激活状态,用于控制光圈显示
|
||||
|
||||
// 电梯点专属属性
|
||||
elevatorStatus?: number; // 电梯状态(仅电梯点使用,0=静止,1=开门中,2=关门中,3=上行,4=下行)
|
||||
currentFloor?: number; // 当前楼层(仅电梯点使用)
|
||||
lastUpdate?: number; // 最后更新时间戳
|
||||
}
|
||||
//#endregion
|
||||
|
||||
|
||||
@ -4,7 +4,10 @@ import type { StorageLocationInfo } from '@api/scene';
|
||||
import type { EditorService } from '@core/editor.service';
|
||||
import sTheme from '@core/theme.service';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { computed, inject, type InjectionKey, type ShallowRef } from 'vue';
|
||||
import { computed, inject, type InjectionKey, ref, type ShallowRef, watch } from 'vue';
|
||||
|
||||
import { type DeviceMappingDTO,queryDeviceMappingByType } from '../../apis/device/api';
|
||||
import { ElevatorStatus,useElevatorStore } from '../../stores/elevator.store';
|
||||
|
||||
type Props = {
|
||||
token: InjectionKey<ShallowRef<EditorService>>;
|
||||
@ -95,6 +98,60 @@ const binTaskData = computed(() => {
|
||||
|
||||
return editor.value.getBinTaskManager().getPointBinTaskDataForDisplay(currentPointName);
|
||||
});
|
||||
|
||||
// 电梯状态管理
|
||||
const elevatorStore = useElevatorStore();
|
||||
|
||||
// 设备映射相关状态(参考门区域实现)
|
||||
const elevatorDeviceMappings = ref<DeviceMappingDTO[]>([]);
|
||||
|
||||
// 根据设备ID获取电梯设备名称
|
||||
const getElevatorDeviceName = computed(() => {
|
||||
if (!point.value?.deviceId || !elevatorDeviceMappings.value.length) return '';
|
||||
|
||||
const device = elevatorDeviceMappings.value.find(d => d.id === point.value?.deviceId);
|
||||
return device?.deviceUniqueName || '';
|
||||
});
|
||||
|
||||
// 获取电梯设备映射数据
|
||||
const fetchElevatorDeviceMappings = async () => {
|
||||
try {
|
||||
const deviceType = '2'; // 电梯的类型是"2"
|
||||
elevatorDeviceMappings.value = await queryDeviceMappingByType(deviceType);
|
||||
} catch (error) {
|
||||
console.error('获取电梯设备映射失败:', error);
|
||||
elevatorDeviceMappings.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 监听电梯点位变化,获取设备映射
|
||||
watch(point, (newPoint) => {
|
||||
if (newPoint && newPoint.type === MapPointType.电梯点 && newPoint.deviceId) {
|
||||
fetchElevatorDeviceMappings();
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 获取电梯状态文本
|
||||
const getElevatorStatusText = (status: ElevatorStatus): string => {
|
||||
const statusMap = {
|
||||
[ElevatorStatus.IDLE]: '静止',
|
||||
[ElevatorStatus.OPENING]: '开门中',
|
||||
[ElevatorStatus.CLOSING]: '关门中',
|
||||
[ElevatorStatus.MOVING_UP]: '上行中',
|
||||
[ElevatorStatus.MOVING_DOWN]: '下行中',
|
||||
[ElevatorStatus.DOOR_OPEN]: '门已开',
|
||||
[ElevatorStatus.DOOR_CLOSED]: '门已关',
|
||||
[ElevatorStatus.FAULT]: '故障',
|
||||
[ElevatorStatus.OFFLINE]: '离线',
|
||||
};
|
||||
return statusMap[status] || '未知';
|
||||
};
|
||||
|
||||
// 获取电梯状态颜色
|
||||
const getElevatorStatusColor = (status: ElevatorStatus, isConnected: boolean): string => {
|
||||
const display = elevatorStore.getElevatorDisplay(status, isConnected);
|
||||
return display.color;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -138,6 +195,39 @@ const binTaskData = computed(() => {
|
||||
</a-typography-text>
|
||||
</a-flex>
|
||||
</a-list-item>
|
||||
|
||||
<!-- 电梯点信息 -->
|
||||
<a-list-item v-if="point.type === MapPointType.电梯点 && point.deviceId">
|
||||
<a-typography-text type="secondary">{{ $t('设备名称') }}</a-typography-text>
|
||||
<a-typography-text>{{ getElevatorDeviceName || point.deviceId }}</a-typography-text>
|
||||
</a-list-item>
|
||||
<a-list-item v-if="point.type === MapPointType.电梯点">
|
||||
<a-typography-text type="secondary">{{ $t('连接状态') }}</a-typography-text>
|
||||
<a-flex align="center" :gap="8" class="conn-status">
|
||||
<span
|
||||
class="status-dot"
|
||||
:class="point.isConnected ? 'online' : 'offline'"
|
||||
:title="point.isConnected ? $t('已连接') : $t('未连接')"
|
||||
/>
|
||||
<a-typography-text>
|
||||
{{ point.isConnected ? $t('已连接') : $t('未连接') }}
|
||||
</a-typography-text>
|
||||
</a-flex>
|
||||
</a-list-item>
|
||||
<a-list-item v-if="point.type === MapPointType.电梯点 && point.currentFloor !== undefined">
|
||||
<a-typography-text type="secondary">{{ $t('当前楼层') }}</a-typography-text>
|
||||
<a-typography-text>{{ point.currentFloor }}F</a-typography-text>
|
||||
</a-list-item>
|
||||
<a-list-item v-if="point.type === MapPointType.电梯点 && point.elevatorStatus !== undefined">
|
||||
<a-typography-text type="secondary">{{ $t('电梯状态') }}</a-typography-text>
|
||||
<a-flex align="center" :gap="8">
|
||||
<span
|
||||
class="status-dot"
|
||||
:style="{ backgroundColor: getElevatorStatusColor(point.elevatorStatus, point.isConnected) }"
|
||||
/>
|
||||
<a-typography-text>{{ getElevatorStatusText(point.elevatorStatus) }}</a-typography-text>
|
||||
</a-flex>
|
||||
</a-list-item>
|
||||
<a-list-item v-if="point.extensionType">
|
||||
<a-typography-text type="secondary">{{ $t('扩展类型') }}</a-typography-text>
|
||||
<a-typography-text>{{ $t(MapPointType[point.extensionType]) }}</a-typography-text>
|
||||
|
||||
@ -19,7 +19,9 @@ import sTheme from '@core/theme.service';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import { computed, inject, type InjectionKey, type ShallowRef } from 'vue';
|
||||
import { computed, inject, type InjectionKey, type ShallowRef,watch } from 'vue';
|
||||
|
||||
import { type DeviceMappingDTO,queryDeviceMappingByType } from '../../apis/device/api';
|
||||
|
||||
// BinTaskManagerService 通过 editor.getBinTaskManager() 获取
|
||||
|
||||
@ -76,6 +78,55 @@ const selectedLocationName = ref<string>('');
|
||||
const coArea1 = computed<MapPen[]>(() => editor.value.getBoundAreas(props.id, 'point', MapAreaType.库区));
|
||||
const coArea2 = computed<MapPen[]>(() => editor.value.getBoundAreas(props.id, 'point', MapAreaType.互斥区));
|
||||
|
||||
// 电梯设备相关
|
||||
const elevatorDevices = ref<DeviceMappingDTO[]>([]);
|
||||
const selectedElevatorDeviceId = ref<string>('');
|
||||
const loadingElevatorDevices = ref<boolean>(false);
|
||||
|
||||
// 获取电梯设备列表
|
||||
const fetchElevatorDevices = async () => {
|
||||
if (!point.value || point.value.type !== MapPointType.电梯点) return;
|
||||
|
||||
loadingElevatorDevices.value = true;
|
||||
try {
|
||||
// 设备类型2代表电梯
|
||||
const deviceType = '2';
|
||||
elevatorDevices.value = await queryDeviceMappingByType(deviceType);
|
||||
|
||||
// 如果当前有绑定的设备ID,设置选中值
|
||||
if (point.value.deviceId) {
|
||||
selectedElevatorDeviceId.value = point.value.deviceId;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取电梯设备失败:', error);
|
||||
elevatorDevices.value = [];
|
||||
} finally {
|
||||
loadingElevatorDevices.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 电梯设备选择变化处理函数
|
||||
const handleElevatorDeviceChange = (deviceId: string) => {
|
||||
if (!props.id || !point.value) return;
|
||||
|
||||
selectedElevatorDeviceId.value = deviceId;
|
||||
|
||||
// 更新点位信息,保存设备ID
|
||||
editor.value.updatePen(props.id, {
|
||||
point: {
|
||||
...point.value,
|
||||
deviceId: deviceId || ''
|
||||
}
|
||||
}, false);
|
||||
};
|
||||
|
||||
// 监听点位变化,获取电梯设备列表
|
||||
watch(point, (newPoint) => {
|
||||
if (newPoint && newPoint.type === MapPointType.电梯点) {
|
||||
fetchElevatorDevices();
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 生成默认库位名称
|
||||
function onAddLocation() {
|
||||
const p = point.value!;
|
||||
@ -299,6 +350,31 @@ const deleteIconUrl = new URL('../../assets/icons/png/delete.png', import.meta.u
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 电梯点设备选择 -->
|
||||
<a-row v-if="point.type === MapPointType.电梯点" :gutter="[8, 8]">
|
||||
<a-col :span="24">
|
||||
<a-typography-text>{{ $t('电梯设备') }}:</a-typography-text>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-select
|
||||
v-model:value="selectedElevatorDeviceId"
|
||||
:placeholder="$t('请选择电梯设备')"
|
||||
:loading="loadingElevatorDevices"
|
||||
allow-clear
|
||||
@change="handleElevatorDeviceChange"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="device in elevatorDevices"
|
||||
:key="device.id"
|
||||
:value="device.id"
|
||||
:label="device.deviceUniqueName"
|
||||
>
|
||||
{{ device.deviceUniqueName }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="[8, 8]">
|
||||
<a-col :span="24">
|
||||
<a-typography-text>{{ $t('描述') }}:</a-typography-text>
|
||||
|
||||
@ -27,6 +27,8 @@ import { EditorService } from '../services/editor.service';
|
||||
import { StorageLocationService } from '../services/storage-location.service';
|
||||
import { useViewState } from '../services/useViewState';
|
||||
import { editorStore } from '../stores/editor.store';
|
||||
import { ElevatorStatus,useElevatorStore } from '../stores/elevator.store';
|
||||
import { createElevatorMockData } from '../utils/elevator-mock';
|
||||
|
||||
const EDITOR_KEY = Symbol('editor-key');
|
||||
|
||||
@ -102,6 +104,9 @@ const container = shallowRef<HTMLDivElement>();
|
||||
const editor = shallowRef<EditorService>();
|
||||
const storageLocationService = shallowRef<StorageLocationService>();
|
||||
const client = shallowRef<WebSocket>();
|
||||
|
||||
// 电梯状态管理
|
||||
const elevatorStore = useElevatorStore();
|
||||
// 模拟门设备WS推送(仅开发调试使用)
|
||||
let doorMockTimer: number | undefined;
|
||||
let doorMockStatus: 0 | 1 = 0;
|
||||
@ -113,6 +118,9 @@ const isPlaybackControllerVisible = ref<boolean>(true);
|
||||
const selectedDate = ref<Dayjs>();
|
||||
const isSceneLoading = ref(false);
|
||||
|
||||
// 电梯模拟数据停止函数
|
||||
let stopElevatorMock: (() => void) | undefined;
|
||||
|
||||
const playback = usePlaybackWebSocket(editor, async () => {
|
||||
// [关键修复] 只有在收到第一帧机器人数据后才初始化机器人图元
|
||||
await editor.value?.initRobots();
|
||||
@ -333,10 +341,13 @@ const monitorScene = async () => {
|
||||
ws.onmessage = (e) => {
|
||||
const data = JSON.parse(e.data || '{}');
|
||||
|
||||
// 判断数据类型:type=99为自动门点,其他为机器人
|
||||
// 判断数据类型:type=101为自动门点,type=102为电梯点,其他为机器人
|
||||
if (data.type === 101) {
|
||||
// 自动门点数据处理
|
||||
autoDoorSimulationService.handleWebSocketData(data as AutoDoorWebSocketData);
|
||||
} else if (data.type === 102) {
|
||||
// 电梯状态数据处理
|
||||
elevatorStore.handleElevatorWebSocketData(data);
|
||||
} else {
|
||||
// 机器人数据处理
|
||||
const robotData = data as RobotRealtimeInfo;
|
||||
@ -441,6 +452,16 @@ onMounted(() => {
|
||||
//
|
||||
}
|
||||
|
||||
// 设置电梯状态管理的编辑器实例
|
||||
try {
|
||||
if (editor.value) {
|
||||
console.log('[ElevatorWS] bind editor to elevator store');
|
||||
elevatorStore.setEditorService(editor.value);
|
||||
}
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
|
||||
storageLocationService.value = new StorageLocationService(editor.value, props.sid);
|
||||
container.value?.addEventListener('pointerdown', handleCanvasPointerDown, true);
|
||||
});
|
||||
@ -489,6 +510,22 @@ onMounted(async () => {
|
||||
document.addEventListener('keydown', handleGlobalKeydown);
|
||||
// 监听 Ctrl/Cmd+F 聚焦左侧搜索框
|
||||
document.addEventListener('keydown', focusFindKeydownHandler);
|
||||
|
||||
// 开发环境启用电梯模拟数据
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('[开发环境] 启动电梯状态模拟数据');
|
||||
stopElevatorMock = createElevatorMockData();
|
||||
|
||||
// 推送指定的电梯数据
|
||||
console.log('[开发环境] 推送指定电梯数据: 1998661793706377218');
|
||||
elevatorStore.handleElevatorWebSocketData({
|
||||
id: '1998661793706377218',
|
||||
type: 102,
|
||||
status: ElevatorStatus.OPENING,
|
||||
floor: 5,
|
||||
isConnected: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@ -520,6 +557,11 @@ onUnmounted(() => {
|
||||
document.removeEventListener('click', handleGlobalClick);
|
||||
document.removeEventListener('keydown', handleGlobalKeydown);
|
||||
document.removeEventListener('keydown', focusFindKeydownHandler);
|
||||
|
||||
// 停止电梯模拟数据
|
||||
if (stopElevatorMock) {
|
||||
stopElevatorMock();
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
|
||||
@ -627,6 +669,9 @@ const handleFloorChange = async (value: any) => {
|
||||
if (mode.value === 'live') {
|
||||
void monitorScene();
|
||||
}
|
||||
|
||||
// 3.3 刷新电梯映射(新楼层可能有不同的电梯点)
|
||||
elevatorStore.refreshMapping();
|
||||
}
|
||||
};
|
||||
//#endregion
|
||||
|
||||
372
src/stores/elevator.store.ts
Normal file
372
src/stores/elevator.store.ts
Normal file
@ -0,0 +1,372 @@
|
||||
/**
|
||||
* 电梯状态管理 Store
|
||||
* 使用 Pinia 管理电梯的全局状态
|
||||
*/
|
||||
|
||||
import type { MapPen } from '@api/map';
|
||||
import { MapPointType } from '@api/map';
|
||||
import type { EditorService } from '@core/editor.service';
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, markRaw, ref } from 'vue';
|
||||
|
||||
// 电梯状态枚举
|
||||
export enum ElevatorStatus {
|
||||
IDLE = 0, // 静止
|
||||
OPENING = 1, // 开门中
|
||||
CLOSING = 2, // 关门中
|
||||
MOVING_UP = 3, // 上行
|
||||
MOVING_DOWN = 4, // 下行
|
||||
DOOR_OPEN = 5, // 门已开
|
||||
DOOR_CLOSED = 6, // 门已关
|
||||
FAULT = 7, // 故障
|
||||
OFFLINE = 8, // 离线
|
||||
}
|
||||
|
||||
// 电梯数据接口
|
||||
export interface ElevatorData {
|
||||
deviceId: string; // 设备ID
|
||||
status: ElevatorStatus; // 当前状态
|
||||
floor?: number; // 当前楼层
|
||||
isConnected: boolean; // 是否连接
|
||||
lastUpdate: number; // 最后更新时间戳
|
||||
penId?: string; // 对应的画布点位ID
|
||||
}
|
||||
|
||||
// WebSocket推送的电梯数据接口
|
||||
export interface ElevatorWebSocketData {
|
||||
id: string; // 设备ID
|
||||
type: 102; // 电梯类型标识
|
||||
status: ElevatorStatus; // 电梯状态
|
||||
floor?: number; // 当前楼层
|
||||
isConnected: boolean; // 连接状态
|
||||
}
|
||||
|
||||
// 电梯点映射信息
|
||||
export interface ElevatorPoint {
|
||||
penId: string;
|
||||
deviceId: string;
|
||||
pointType: MapPointType.电梯点;
|
||||
}
|
||||
|
||||
export const useElevatorStore = defineStore('elevator', () => {
|
||||
// ========== 状态定义 ==========
|
||||
const elevators = ref<Map<string, ElevatorData>>(new Map());
|
||||
const elevatorPoints = ref<Map<string, ElevatorPoint>>(new Map());
|
||||
const editorService = ref<EditorService | null>(null);
|
||||
|
||||
// 防抖更新队列
|
||||
const updateQueue = new Map<string, NodeJS.Timeout>();
|
||||
const UPDATE_DELAY = 300; // 300ms 防抖延迟
|
||||
|
||||
|
||||
|
||||
// ========== 计算属性 ==========
|
||||
|
||||
// 获取所有电梯数据
|
||||
const allElevators = computed(() => Array.from(elevators.value.values()));
|
||||
|
||||
// 获取在线电梯数量
|
||||
const onlineElevatorsCount = computed(() =>
|
||||
allElevators.value.filter(e => e.isConnected).length
|
||||
);
|
||||
|
||||
// 获取离线电梯数量
|
||||
const offlineElevatorsCount = computed(() =>
|
||||
allElevators.value.filter(e => !e.isConnected).length
|
||||
);
|
||||
|
||||
// 获取故障电梯数量
|
||||
const faultElevatorsCount = computed(() =>
|
||||
allElevators.value.filter(e => e.status === ElevatorStatus.FAULT).length
|
||||
);
|
||||
|
||||
// 按状态分组的电梯
|
||||
const elevatorsByStatus = computed(() => {
|
||||
const groups: Record<ElevatorStatus, ElevatorData[]> = {} as any;
|
||||
|
||||
// 初始化所有状态
|
||||
Object.values(ElevatorStatus).forEach(status => {
|
||||
if (typeof status === 'number') {
|
||||
groups[status] = [];
|
||||
}
|
||||
});
|
||||
|
||||
// 分组
|
||||
allElevators.value.forEach(elevator => {
|
||||
groups[elevator.status].push(elevator);
|
||||
});
|
||||
|
||||
return groups;
|
||||
});
|
||||
|
||||
// ========== 方法 ==========
|
||||
|
||||
/**
|
||||
* 设置编辑器服务
|
||||
*/
|
||||
const setEditorService = (editor: EditorService) => {
|
||||
editorService.value = markRaw(editor);
|
||||
buildElevatorMapping();
|
||||
};
|
||||
|
||||
/**
|
||||
* 构建设备ID到电梯点的映射
|
||||
*/
|
||||
const buildElevatorMapping = () => {
|
||||
if (!editorService.value) return;
|
||||
|
||||
elevatorPoints.value.clear();
|
||||
const pens = editorService.value.data().pens;
|
||||
|
||||
pens.forEach((pen: MapPen) => {
|
||||
if (
|
||||
pen.name === 'point' &&
|
||||
pen.point?.type === MapPointType.电梯点 &&
|
||||
pen.point?.deviceId
|
||||
) {
|
||||
const elevatorPoint: ElevatorPoint = {
|
||||
penId: pen.id,
|
||||
deviceId: pen.point.deviceId,
|
||||
pointType: MapPointType.电梯点
|
||||
};
|
||||
|
||||
elevatorPoints.value.set(pen.point.deviceId, elevatorPoint);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('🛗 电梯映射构建完成:', {
|
||||
totalPoints: elevatorPoints.value.size,
|
||||
deviceIds: Array.from(elevatorPoints.value.keys())
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理WebSocket推送的电梯数据
|
||||
*/
|
||||
const handleElevatorWebSocketData = (data: ElevatorWebSocketData) => {
|
||||
const { id: deviceId, status, floor, isConnected } = data;
|
||||
|
||||
// 获取或创建电梯数据
|
||||
let elevatorData = elevators.value.get(deviceId);
|
||||
const elevatorPoint = elevatorPoints.value.get(deviceId);
|
||||
|
||||
if (!elevatorData) {
|
||||
elevatorData = {
|
||||
deviceId,
|
||||
status: ElevatorStatus.IDLE,
|
||||
isConnected: false,
|
||||
lastUpdate: Date.now(),
|
||||
penId: elevatorPoint?.penId
|
||||
};
|
||||
}
|
||||
|
||||
// 更新数据
|
||||
elevatorData.status = isConnected ? status : ElevatorStatus.OFFLINE;
|
||||
elevatorData.floor = floor;
|
||||
elevatorData.isConnected = isConnected;
|
||||
elevatorData.lastUpdate = Date.now();
|
||||
|
||||
// 如果找到对应的点位,也更新penId
|
||||
if (elevatorPoint && !elevatorData.penId) {
|
||||
elevatorData.penId = elevatorPoint.penId;
|
||||
}
|
||||
|
||||
// 存储到Map
|
||||
elevators.value.set(deviceId, elevatorData);
|
||||
|
||||
// 更新画布上的电梯点显示
|
||||
if (elevatorData.penId && editorService.value) {
|
||||
updateElevatorPointDisplay(elevatorData);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新画布上电梯点的显示(带防抖)
|
||||
*/
|
||||
const updateElevatorPointDisplay = (elevatorData: ElevatorData) => {
|
||||
if (!editorService.value || !elevatorData.penId) return;
|
||||
|
||||
const penId = elevatorData.penId;
|
||||
|
||||
// 清除之前的定时器
|
||||
if (updateQueue.has(penId)) {
|
||||
clearTimeout(updateQueue.get(penId)!);
|
||||
}
|
||||
|
||||
// 设置新的防抖定时器
|
||||
const timer = setTimeout(() => {
|
||||
// 获取状态对应的颜色
|
||||
const { color } = getElevatorDisplay(elevatorData.status, elevatorData.isConnected);
|
||||
|
||||
// 更新电梯点
|
||||
editorService.value.updatePen(penId, {
|
||||
point: {
|
||||
...((editorService.value.getPenById(penId) as any)?.point || {}),
|
||||
elevatorStatus: elevatorData.status,
|
||||
isConnected: elevatorData.isConnected,
|
||||
currentFloor: elevatorData.floor,
|
||||
lastUpdate: elevatorData.lastUpdate,
|
||||
// 使用颜色区分状态
|
||||
color
|
||||
}
|
||||
}, false);
|
||||
|
||||
// 清除定时器
|
||||
updateQueue.delete(penId);
|
||||
}, UPDATE_DELAY);
|
||||
|
||||
updateQueue.set(penId, timer);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取电梯显示配置(颜色、图标)
|
||||
*/
|
||||
const getElevatorDisplay = (status: ElevatorStatus, isConnected: boolean) => {
|
||||
// 如果离线,显示灰色
|
||||
if (!isConnected) {
|
||||
return {
|
||||
color: '#999999',
|
||||
iconPath: '/icons/elevator/elevator-offline.svg',
|
||||
text: '离线'
|
||||
};
|
||||
}
|
||||
|
||||
const statusMap = {
|
||||
[ElevatorStatus.IDLE]: {
|
||||
color: '#1890FF',
|
||||
iconPath: '/icons/elevator/elevator-idle.svg',
|
||||
text: '静止'
|
||||
},
|
||||
[ElevatorStatus.OPENING]: {
|
||||
color: '#52C41A',
|
||||
iconPath: '/icons/elevator/elevator-opening.svg',
|
||||
text: '开门中'
|
||||
},
|
||||
[ElevatorStatus.CLOSING]: {
|
||||
color: '#FA8C16',
|
||||
iconPath: '/icons/elevator/elevator-closing.svg',
|
||||
text: '关门中'
|
||||
},
|
||||
[ElevatorStatus.MOVING_UP]: {
|
||||
color: '#722ED1',
|
||||
iconPath: '/icons/elevator/elevator-up.svg',
|
||||
text: '上行中'
|
||||
},
|
||||
[ElevatorStatus.MOVING_DOWN]: {
|
||||
color: '#13C2C2',
|
||||
iconPath: '/icons/elevator/elevator-down.svg',
|
||||
text: '下行中'
|
||||
},
|
||||
[ElevatorStatus.DOOR_OPEN]: {
|
||||
color: '#52C41A',
|
||||
iconPath: '/icons/elevator/elevator-door-open.svg',
|
||||
text: '门已开'
|
||||
},
|
||||
[ElevatorStatus.DOOR_CLOSED]: {
|
||||
color: '#1890FF',
|
||||
iconPath: '/icons/elevator/elevator-door-closed.svg',
|
||||
text: '门已关'
|
||||
},
|
||||
[ElevatorStatus.FAULT]: {
|
||||
color: '#FF4D4F',
|
||||
iconPath: '/icons/elevator/elevator-fault.svg',
|
||||
text: '故障'
|
||||
},
|
||||
};
|
||||
|
||||
return statusMap[status] || statusMap[ElevatorStatus.IDLE];
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取单个电梯数据
|
||||
*/
|
||||
const getElevatorById = (deviceId: string): ElevatorData | undefined => {
|
||||
return elevators.value.get(deviceId);
|
||||
};
|
||||
|
||||
/**
|
||||
* 手动更新电梯状态
|
||||
*/
|
||||
const updateElevatorStatus = (
|
||||
deviceId: string,
|
||||
status: ElevatorStatus,
|
||||
isConnected: boolean = true
|
||||
) => {
|
||||
const mockData: ElevatorWebSocketData = {
|
||||
id: deviceId,
|
||||
type: 102,
|
||||
status,
|
||||
isConnected
|
||||
};
|
||||
|
||||
handleElevatorWebSocketData(mockData);
|
||||
};
|
||||
|
||||
/**
|
||||
* 刷新电梯映射(场景变化时调用)
|
||||
*/
|
||||
const refreshMapping = () => {
|
||||
buildElevatorMapping();
|
||||
|
||||
// 重新更新所有电梯的显示
|
||||
elevators.value.forEach(elevatorData => {
|
||||
if (elevatorData.penId) {
|
||||
updateElevatorPointDisplay(elevatorData);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除所有电梯数据
|
||||
*/
|
||||
const clearAllData = () => {
|
||||
// 清理防抖队列
|
||||
updateQueue.forEach((timer) => clearTimeout(timer));
|
||||
updateQueue.clear();
|
||||
|
||||
elevators.value.clear();
|
||||
console.log('🛗 电梯数据已清除');
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取电梯统计信息
|
||||
*/
|
||||
const getStatistics = () => {
|
||||
return {
|
||||
total: allElevators.value.length,
|
||||
online: onlineElevatorsCount.value,
|
||||
offline: offlineElevatorsCount.value,
|
||||
fault: faultElevatorsCount.value,
|
||||
byStatus: Object.entries(elevatorsByStatus.value).reduce((acc, [status, list]) => {
|
||||
acc[Number(status)] = list.length;
|
||||
return acc;
|
||||
}, {} as Record<number, number>)
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
return {
|
||||
// 状态
|
||||
elevators,
|
||||
elevatorPoints,
|
||||
editorService,
|
||||
|
||||
// 计算属性
|
||||
allElevators,
|
||||
onlineElevatorsCount,
|
||||
offlineElevatorsCount,
|
||||
faultElevatorsCount,
|
||||
elevatorsByStatus,
|
||||
|
||||
// 方法
|
||||
setEditorService,
|
||||
handleElevatorWebSocketData,
|
||||
getElevatorById,
|
||||
updateElevatorStatus,
|
||||
refreshMapping,
|
||||
clearAllData,
|
||||
getStatistics,
|
||||
getElevatorDisplay
|
||||
};
|
||||
});
|
||||
114
src/utils/device-mock.ts
Normal file
114
src/utils/device-mock.ts
Normal file
@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 设备映射模拟工具
|
||||
* 用于开发和测试设备选择功能
|
||||
*/
|
||||
|
||||
import type { DeviceMappingDTO } from '../apis/device/api';
|
||||
|
||||
// 模拟门设备映射数据
|
||||
export const mockDoorDeviceMappings: DeviceMappingDTO[] = [
|
||||
{
|
||||
id: '1',
|
||||
deviceUniqueName: '1号门',
|
||||
protocolType: 'ModbusTCP',
|
||||
mappingType: '1',
|
||||
brandName: '门品牌A',
|
||||
ipAddress: '192.168.1.101',
|
||||
port: 502,
|
||||
slaveId: 1,
|
||||
enabled: 1,
|
||||
sceneId: null
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
deviceUniqueName: '2号门',
|
||||
protocolType: 'ModbusTCP',
|
||||
mappingType: '1',
|
||||
brandName: '门品牌B',
|
||||
ipAddress: '192.168.1.102',
|
||||
port: 502,
|
||||
slaveId: 2,
|
||||
enabled: 1,
|
||||
sceneId: null
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
deviceUniqueName: '3号门',
|
||||
protocolType: 'ModbusTCP',
|
||||
mappingType: '1',
|
||||
brandName: '门品牌C',
|
||||
ipAddress: '192.168.1.103',
|
||||
port: 502,
|
||||
slaveId: 3,
|
||||
enabled: 1,
|
||||
sceneId: null
|
||||
}
|
||||
];
|
||||
|
||||
// 模拟电梯设备映射数据
|
||||
export const mockElevatorDeviceMappings: DeviceMappingDTO[] = [
|
||||
{
|
||||
id: '2',
|
||||
deviceUniqueName: '1号电梯',
|
||||
protocolType: 'ModbusTCP',
|
||||
mappingType: '2',
|
||||
brandName: '电梯品牌A',
|
||||
ipAddress: '192.168.1.201',
|
||||
port: 502,
|
||||
slaveId: 1,
|
||||
enabled: 1,
|
||||
sceneId: null
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
deviceUniqueName: '2号电梯',
|
||||
protocolType: 'ModbusTCP',
|
||||
mappingType: '2',
|
||||
brandName: '电梯品牌B',
|
||||
ipAddress: '192.168.1.202',
|
||||
port: 502,
|
||||
slaveId: 2,
|
||||
enabled: 1,
|
||||
sceneId: null
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
deviceUniqueName: '3号电梯',
|
||||
protocolType: 'ModbusTCP',
|
||||
mappingType: '2',
|
||||
brandName: '电梯品牌C',
|
||||
ipAddress: '192.168.1.203',
|
||||
port: 502,
|
||||
slaveId: 3,
|
||||
enabled: 1,
|
||||
sceneId: null
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
deviceUniqueName: '4号电梯',
|
||||
protocolType: 'ModbusTCP',
|
||||
mappingType: '2',
|
||||
brandName: '电梯品牌D',
|
||||
ipAddress: '192.168.1.204',
|
||||
port: 502,
|
||||
slaveId: 4,
|
||||
enabled: 1,
|
||||
sceneId: null
|
||||
}
|
||||
];
|
||||
|
||||
// 开发环境下拦截 API 调用
|
||||
export const setupDeviceMock = () => {
|
||||
if (import.meta.env.DEV) {
|
||||
// 拦截 queryDeviceMappingByType 函数
|
||||
const originalQuery = (window as any).__originalQueryDeviceMappingByType;
|
||||
|
||||
if (!originalQuery) {
|
||||
// 保存原始函数
|
||||
(window as any).__originalQueryDeviceMappingByType = true;
|
||||
|
||||
// 这里我们可以在实际调用时模拟数据
|
||||
console.log('🔧 设备映射模拟已启用');
|
||||
}
|
||||
}
|
||||
};
|
||||
129
src/utils/elevator-mock.ts
Normal file
129
src/utils/elevator-mock.ts
Normal file
@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 电梯状态模拟工具
|
||||
* 用于开发和测试电梯状态功能
|
||||
*/
|
||||
|
||||
import { ElevatorStatus,useElevatorStore } from '../stores/elevator.store';
|
||||
|
||||
export const createElevatorMockData = () => {
|
||||
const elevatorStore = useElevatorStore();
|
||||
|
||||
// 模拟的设备ID列表(与API返回的设备ID格式保持一致)
|
||||
const mockDeviceIds = [
|
||||
'2',
|
||||
'4',
|
||||
'6',
|
||||
'8'
|
||||
];
|
||||
|
||||
// 缓存上次的状态,避免无意义的更新
|
||||
const lastStates = new Map<string, { status: ElevatorStatus; floor: number; isConnected: boolean }>();
|
||||
|
||||
// 生成模拟数据
|
||||
const generateMockData = () => {
|
||||
mockDeviceIds.forEach((deviceId, index) => {
|
||||
// 为每个电梯生成不同的状态
|
||||
let status: ElevatorStatus;
|
||||
let floor: number;
|
||||
|
||||
// 70%概率保持当前状态,30%概率改变状态
|
||||
if (Math.random() > 0.7) {
|
||||
switch (index % 4) {
|
||||
case 0:
|
||||
status = ElevatorStatus.MOVING_UP;
|
||||
break;
|
||||
case 1:
|
||||
status = ElevatorStatus.MOVING_DOWN;
|
||||
break;
|
||||
case 2:
|
||||
status = ElevatorStatus.OPENING;
|
||||
break;
|
||||
default:
|
||||
status = ElevatorStatus.IDLE;
|
||||
}
|
||||
} else {
|
||||
// 保持当前状态
|
||||
const last = lastStates.get(deviceId);
|
||||
status = last ? last.status : (index % 4 === 0 ? ElevatorStatus.MOVING_UP : index % 4 === 1 ? ElevatorStatus.MOVING_DOWN : index % 4 === 2 ? ElevatorStatus.OPENING : ElevatorStatus.IDLE);
|
||||
}
|
||||
|
||||
// 95%概率在线,5%概率离线
|
||||
const isConnected = Math.random() > 0.05;
|
||||
|
||||
// 只有在线时才更新楼层
|
||||
if (isConnected && Math.random() > 0.5) {
|
||||
// 30%概率改变楼层
|
||||
const last = lastStates.get(deviceId);
|
||||
if (Math.random() > 0.7) {
|
||||
floor = last ? Math.min(10, Math.max(1, last.floor + (Math.random() > 0.5 ? 1 : -1))) : Math.floor(Math.random() * 10) + 1;
|
||||
} else {
|
||||
floor = last ? last.floor : Math.floor(Math.random() * 10) + 1;
|
||||
}
|
||||
} else {
|
||||
floor = lastStates.get(deviceId)?.floor || Math.floor(Math.random() * 10) + 1;
|
||||
}
|
||||
|
||||
// 检查状态是否真的发生了变化
|
||||
const lastState = lastStates.get(deviceId);
|
||||
if (lastState &&
|
||||
lastState.status === status &&
|
||||
lastState.floor === floor &&
|
||||
lastState.isConnected === isConnected) {
|
||||
// 状态没有变化,跳过更新
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
lastStates.set(deviceId, { status, floor, isConnected });
|
||||
|
||||
// 偶尔模拟故障(5%概率)
|
||||
if (Math.random() > 0.95 && isConnected) {
|
||||
status = ElevatorStatus.FAULT;
|
||||
}
|
||||
|
||||
elevatorStore.handleElevatorWebSocketData({
|
||||
id: deviceId,
|
||||
type: 102,
|
||||
status: isConnected ? status : ElevatorStatus.OFFLINE,
|
||||
floor,
|
||||
isConnected
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 初始生成一次数据
|
||||
generateMockData();
|
||||
|
||||
// 设置定时器,每8-12秒更新一次数据(降低频率)
|
||||
const interval = setInterval(() => {
|
||||
generateMockData();
|
||||
}, Math.random() * 4000 + 8000); // 8-12秒随机间隔
|
||||
|
||||
console.log('🛗 电梯模拟数据已启动,设备ID:', mockDeviceIds);
|
||||
|
||||
// 返回停止函数
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
lastStates.clear();
|
||||
console.log('🛗 电梯模拟数据已停止');
|
||||
};
|
||||
};
|
||||
|
||||
// 创建单个电梯的测试数据
|
||||
export const createSingleElevatorData = (deviceId: string, status?: ElevatorStatus) => {
|
||||
const elevatorStore = useElevatorStore();
|
||||
|
||||
elevatorStore.handleElevatorWebSocketData({
|
||||
id: deviceId,
|
||||
type: 102,
|
||||
status: status || ElevatorStatus.IDLE,
|
||||
floor: Math.floor(Math.random() * 10) + 1,
|
||||
isConnected: true
|
||||
});
|
||||
};
|
||||
|
||||
// 清除所有电梯数据
|
||||
export const clearElevatorData = () => {
|
||||
const elevatorStore = useElevatorStore();
|
||||
elevatorStore.clearAllData();
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user