2025-05-22 18:06:30 +08:00
|
|
|
|
<script setup lang="ts">
|
2025-05-25 00:07:22 +08:00
|
|
|
|
import type { RobotRealtimeInfo } from '@api/robot';
|
2025-07-02 11:39:27 +08:00
|
|
|
|
import { getSceneByGroupId, getSceneById, monitorRealSceneById, monitorSceneById } from '@api/scene';
|
2025-05-22 18:06:30 +08:00
|
|
|
|
import { EditorService } from '@core/editor.service';
|
2025-07-18 15:52:26 +08:00
|
|
|
|
import { StorageLocationService } from '@core/storage-location.service';
|
2025-07-01 14:38:21 +08:00
|
|
|
|
import { useViewState } from '@core/useViewState';
|
|
|
|
|
import { message } from 'ant-design-vue';
|
2025-05-25 00:07:22 +08:00
|
|
|
|
import { isNil } from 'lodash-es';
|
|
|
|
|
import { computed, onMounted, onUnmounted, provide, ref, shallowRef, watch } from 'vue';
|
2025-07-02 11:39:27 +08:00
|
|
|
|
import { useRoute } from 'vue-router';
|
2025-05-22 18:06:30 +08:00
|
|
|
|
|
|
|
|
|
const EDITOR_KEY = Symbol('editor-key');
|
|
|
|
|
|
|
|
|
|
type Props = {
|
|
|
|
|
sid: string;
|
|
|
|
|
id?: string;
|
|
|
|
|
};
|
|
|
|
|
const props = defineProps<Props>();
|
|
|
|
|
|
2025-07-18 15:52:26 +08:00
|
|
|
|
//#region 响应式状态定义
|
2025-07-02 11:39:27 +08:00
|
|
|
|
// 获取路由信息以判断当前模式
|
|
|
|
|
const route = useRoute();
|
|
|
|
|
const isMonitorMode = computed(() => route.path.includes('/monitor'));
|
|
|
|
|
|
2025-07-18 15:52:26 +08:00
|
|
|
|
// 场景标题
|
|
|
|
|
const title = ref<string>('');
|
|
|
|
|
|
|
|
|
|
// 服务实例
|
|
|
|
|
const container = shallowRef<HTMLDivElement>();
|
|
|
|
|
const editor = shallowRef<EditorService>();
|
|
|
|
|
const storageLocationService = shallowRef<StorageLocationService>();
|
|
|
|
|
const client = shallowRef<WebSocket>();
|
|
|
|
|
|
|
|
|
|
// 依赖注入
|
|
|
|
|
provide(EDITOR_KEY, editor);
|
|
|
|
|
//#endregion
|
|
|
|
|
|
|
|
|
|
//#region 核心服务接口
|
|
|
|
|
/**
|
|
|
|
|
* 读取场景数据
|
|
|
|
|
*/
|
2025-05-22 18:06:30 +08:00
|
|
|
|
const readScene = async () => {
|
|
|
|
|
const res = props.id ? await getSceneByGroupId(props.id, props.sid) : await getSceneById(props.sid);
|
|
|
|
|
title.value = res?.label ?? '';
|
|
|
|
|
editor.value?.load(res?.json);
|
|
|
|
|
};
|
2025-05-25 00:07:22 +08:00
|
|
|
|
|
2025-07-18 15:52:26 +08:00
|
|
|
|
/**
|
|
|
|
|
* 监控场景中的机器人状态
|
2025-07-26 04:30:43 +08:00
|
|
|
|
* 采用 requestAnimationFrame 优化高频数据渲染,确保动画流畅且不阻塞UI线程。
|
2025-07-18 15:52:26 +08:00
|
|
|
|
*/
|
2025-05-25 00:07:22 +08:00
|
|
|
|
const monitorScene = async () => {
|
2025-07-18 15:52:26 +08:00
|
|
|
|
console.log(current.value?.id);
|
2025-05-25 00:07:22 +08:00
|
|
|
|
client.value?.close();
|
2025-07-02 11:39:27 +08:00
|
|
|
|
// 根据路由路径是否是真实场景监控决定使用哪个监控接口
|
|
|
|
|
const ws = isMonitorMode.value ? await monitorRealSceneById(props.sid) : await monitorSceneById(props.sid);
|
2025-05-25 00:07:22 +08:00
|
|
|
|
if (isNil(ws)) return;
|
2025-07-26 04:30:43 +08:00
|
|
|
|
|
|
|
|
|
// 使用 Map 来缓冲每个机器人最新的实时数据。
|
|
|
|
|
// Key 是机器人 ID,Value 是该机器人最新的完整信息。
|
|
|
|
|
// 这样做可以确保我们总是有每个机器人最新的状态,并且可以快速更新,避免重复渲染。
|
|
|
|
|
const latestRobotData = new Map<string, RobotRealtimeInfo>();
|
|
|
|
|
|
|
|
|
|
// 用于存储 requestAnimationFrame 的 ID,方便在组件卸载或 WebSocket 关闭时取消动画循环。
|
|
|
|
|
let animationFrameId: number;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 渲染循环函数。
|
|
|
|
|
* 这个函数会通过 requestAnimationFrame 以浏览器的刷新频率(通常是60fps)被反复调用。
|
|
|
|
|
* 这是实现流畅动画的核心。
|
|
|
|
|
*/
|
|
|
|
|
const renderLoop = () => {
|
|
|
|
|
// 检查是否有新的数据需要渲染。如果没有,则本次循环不执行任何操作,节省性能。
|
|
|
|
|
if (latestRobotData.size > 0) {
|
|
|
|
|
// 遍历所有待更新的机器人数据
|
|
|
|
|
latestRobotData.forEach((data, id) => {
|
|
|
|
|
const { x, y, active, angle, path, isWaring, isFault, ...rest } = data;
|
|
|
|
|
// 确保机器人仍然存在于编辑器中
|
|
|
|
|
if (!editor.value?.checkRobotById(id)) return;
|
|
|
|
|
|
|
|
|
|
// 更新机器人的非位置属性(如状态等)
|
|
|
|
|
editor.value?.updateRobot(id, rest);
|
|
|
|
|
|
|
|
|
|
// 更新机器人的位置和可见性
|
|
|
|
|
if (isNil(x) || isNil(y)) {
|
|
|
|
|
// 如果坐标为空,则隐藏机器人
|
|
|
|
|
editor.value.updatePen(id, { visible: false });
|
|
|
|
|
} else {
|
|
|
|
|
// 如果坐标有效,则刷新机器人在画布上的位置、角度等
|
|
|
|
|
editor.value.refreshRobot(id, { x, y, active, angle, path, isWaring, isFault });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// 本次渲染完成后,清空数据缓冲区,等待下一批新数据的到来。
|
|
|
|
|
latestRobotData.clear();
|
2025-05-25 00:07:22 +08:00
|
|
|
|
}
|
2025-07-26 04:30:43 +08:00
|
|
|
|
// 请求浏览器在下一次重绘之前再次调用 renderLoop,形成动画循环。
|
|
|
|
|
animationFrameId = requestAnimationFrame(renderLoop);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* WebSocket 的 onmessage 事件处理器。
|
|
|
|
|
* 这个函数只做一件事:接收数据并将其快速存入缓冲区。
|
|
|
|
|
* 这种“数据与渲染分离”的设计是避免UI阻塞的关键。
|
|
|
|
|
*/
|
|
|
|
|
ws.onmessage = (e) => {
|
|
|
|
|
const data = <RobotRealtimeInfo>JSON.parse(e.data || '{}');
|
|
|
|
|
// 将最新数据存入 Map,如果已有相同ID的数据,则会被自动覆盖。
|
|
|
|
|
latestRobotData.set(data.id, data);
|
2025-05-25 00:07:22 +08:00
|
|
|
|
};
|
2025-07-26 04:30:43 +08:00
|
|
|
|
|
2025-05-25 00:07:22 +08:00
|
|
|
|
client.value = ws;
|
2025-07-26 04:30:43 +08:00
|
|
|
|
|
|
|
|
|
// WebSocket 连接成功后,立即启动渲染循环。
|
|
|
|
|
renderLoop();
|
|
|
|
|
|
|
|
|
|
// 增强 WebSocket 的 onclose 事件,以确保在连接关闭时清理资源。
|
|
|
|
|
const originalOnClose = ws.onclose;
|
|
|
|
|
ws.onclose = (event) => {
|
|
|
|
|
// 停止渲染循环,防止不必要的计算和内存泄漏。
|
|
|
|
|
cancelAnimationFrame(animationFrameId);
|
|
|
|
|
// 如果原始的 onclose 处理函数存在,则继续执行它。
|
|
|
|
|
if (originalOnClose) {
|
|
|
|
|
originalOnClose.call(ws, event);
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-05-25 00:07:22 +08:00
|
|
|
|
};
|
2025-05-22 18:06:30 +08:00
|
|
|
|
//#endregion
|
|
|
|
|
|
2025-07-18 15:52:26 +08:00
|
|
|
|
//#region 服务初始化
|
2025-05-22 18:06:30 +08:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
editor.value = new EditorService(container.value!);
|
2025-07-18 15:52:26 +08:00
|
|
|
|
storageLocationService.value = new StorageLocationService(editor.value, props.sid);
|
2025-05-22 18:06:30 +08:00
|
|
|
|
});
|
2025-07-18 15:52:26 +08:00
|
|
|
|
//#endregion
|
2025-05-22 18:06:30 +08:00
|
|
|
|
|
2025-07-18 15:52:26 +08:00
|
|
|
|
//#region 生命周期管理
|
2025-05-25 00:07:22 +08:00
|
|
|
|
onMounted(async () => {
|
|
|
|
|
await readScene();
|
|
|
|
|
await editor.value?.initRobots();
|
2025-06-06 22:36:16 +08:00
|
|
|
|
await monitorScene();
|
2025-07-18 15:52:26 +08:00
|
|
|
|
await storageLocationService.value?.startMonitoring({ interval: 3 });
|
2025-07-02 09:27:40 +08:00
|
|
|
|
// 自动保存和恢复视图状态
|
|
|
|
|
await handleAutoSaveAndRestoreViewState();
|
2025-05-25 00:07:22 +08:00
|
|
|
|
});
|
2025-07-18 15:52:26 +08:00
|
|
|
|
|
2025-05-25 00:07:22 +08:00
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
client.value?.close();
|
2025-07-18 15:52:26 +08:00
|
|
|
|
storageLocationService.value?.destroy();
|
2025-05-25 00:07:22 +08:00
|
|
|
|
});
|
2025-07-18 15:52:26 +08:00
|
|
|
|
//#endregion
|
2025-05-25 00:07:22 +08:00
|
|
|
|
|
2025-07-18 15:52:26 +08:00
|
|
|
|
//#region 选择状态管理
|
2025-05-22 18:06:30 +08:00
|
|
|
|
const current = ref<{ type: 'robot' | 'point' | 'line' | 'area'; id: string }>();
|
2025-07-18 15:52:26 +08:00
|
|
|
|
|
|
|
|
|
// 监听编辑器选择状态变化
|
2025-05-22 18:06:30 +08:00
|
|
|
|
watch(
|
|
|
|
|
() => editor.value?.selected.value[0],
|
|
|
|
|
(v) => {
|
|
|
|
|
const pen = editor.value?.getPenById(v);
|
|
|
|
|
if (pen?.id) {
|
|
|
|
|
current.value = { type: <'point' | 'line' | 'area'>pen.name, id: pen.id };
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (current.value?.type === 'robot') return;
|
|
|
|
|
current.value = undefined;
|
|
|
|
|
},
|
|
|
|
|
);
|
2025-07-18 15:52:26 +08:00
|
|
|
|
|
|
|
|
|
// 计算当前选择的对象类型
|
2025-05-22 18:06:30 +08:00
|
|
|
|
const isRobot = computed(() => current.value?.type === 'robot');
|
|
|
|
|
const isPoint = computed(() => current.value?.type === 'point');
|
|
|
|
|
const isRoute = computed(() => current.value?.type === 'line');
|
|
|
|
|
const isArea = computed(() => current.value?.type === 'area');
|
|
|
|
|
|
2025-07-18 15:52:26 +08:00
|
|
|
|
/**
|
|
|
|
|
* 选择机器人
|
|
|
|
|
* @param id 机器人ID
|
|
|
|
|
*/
|
2025-05-22 18:06:30 +08:00
|
|
|
|
const selectRobot = (id: string) => {
|
|
|
|
|
current.value = { type: 'robot', id };
|
|
|
|
|
editor.value?.inactive();
|
2025-07-07 16:38:39 +08:00
|
|
|
|
// 聚焦到机器人位置
|
|
|
|
|
editor.value?.gotoById(id);
|
2025-05-22 18:06:30 +08:00
|
|
|
|
};
|
2025-07-18 15:52:26 +08:00
|
|
|
|
//#endregion
|
2025-07-01 14:38:21 +08:00
|
|
|
|
|
2025-07-18 15:52:26 +08:00
|
|
|
|
//#region 视图状态管理
|
2025-07-02 09:27:40 +08:00
|
|
|
|
const { saveViewState, autoSaveAndRestoreViewState, isSaving } = useViewState();
|
2025-07-01 14:38:21 +08:00
|
|
|
|
|
2025-07-18 15:52:26 +08:00
|
|
|
|
/**
|
|
|
|
|
* 保存当前视图状态
|
|
|
|
|
*/
|
2025-07-01 14:38:21 +08:00
|
|
|
|
const handleSaveViewState = async () => {
|
|
|
|
|
if (!editor.value) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await saveViewState(editor.value, props.sid, props.id);
|
|
|
|
|
message.success('视图状态保存成功');
|
|
|
|
|
} catch {
|
|
|
|
|
message.error('视图状态保存失败');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-18 15:52:26 +08:00
|
|
|
|
/**
|
|
|
|
|
* 自动保存和恢复视图状态
|
|
|
|
|
*/
|
2025-07-02 09:27:40 +08:00
|
|
|
|
const handleAutoSaveAndRestoreViewState = async () => {
|
2025-07-01 14:38:21 +08:00
|
|
|
|
if (!editor.value) return;
|
|
|
|
|
|
2025-07-02 09:27:40 +08:00
|
|
|
|
await autoSaveAndRestoreViewState(editor.value, props.sid, props.id);
|
2025-07-01 14:38:21 +08:00
|
|
|
|
};
|
2025-07-18 15:52:26 +08:00
|
|
|
|
//#endregion
|
|
|
|
|
|
|
|
|
|
//#region UI状态管理
|
|
|
|
|
const show = ref<boolean>(true);
|
|
|
|
|
//#endregion
|
2025-05-22 18:06:30 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<a-layout class="full">
|
|
|
|
|
<a-layout-header class="p-16" style="height: 64px">
|
|
|
|
|
<a-flex justify="space-between" align="center">
|
2025-07-02 11:39:27 +08:00
|
|
|
|
<a-typography-text class="title">{{ title }}--{{ isMonitorMode ? '场景监控' : '场景仿真' }}</a-typography-text>
|
2025-07-01 14:46:31 +08:00
|
|
|
|
<a-button type="primary" :loading="isSaving" @click="handleSaveViewState"> 保存比例 </a-button>
|
2025-05-22 18:06:30 +08:00
|
|
|
|
</a-flex>
|
|
|
|
|
</a-layout-header>
|
|
|
|
|
|
|
|
|
|
<a-layout class="p-16">
|
|
|
|
|
<a-layout-sider :width="320">
|
|
|
|
|
<a-tabs type="card">
|
|
|
|
|
<a-tab-pane key="1" :tab="$t('机器人')">
|
|
|
|
|
<RobotGroups v-if="editor" :token="EDITOR_KEY" :sid="sid" :current="current?.id" @change="selectRobot" />
|
|
|
|
|
</a-tab-pane>
|
2025-07-18 15:52:26 +08:00
|
|
|
|
<a-tab-pane key="2" :tab="$t('库区')">
|
2025-05-22 18:06:30 +08:00
|
|
|
|
<PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" only-area1 />
|
2025-07-18 15:52:26 +08:00
|
|
|
|
</a-tab-pane>
|
2025-05-22 18:06:30 +08:00
|
|
|
|
<a-tab-pane key="3" :tab="$t('高级组')">
|
|
|
|
|
<PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" />
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
</a-tabs>
|
|
|
|
|
</a-layout-sider>
|
|
|
|
|
<a-layout-content>
|
|
|
|
|
<div ref="container" class="editor-container full"></div>
|
|
|
|
|
</a-layout-content>
|
|
|
|
|
</a-layout>
|
|
|
|
|
</a-layout>
|
2025-05-25 00:07:22 +08:00
|
|
|
|
|
|
|
|
|
<template v-if="current?.id">
|
|
|
|
|
<a-float-button style="top: 80px; right: 16px" shape="square" @click="show = !show">
|
|
|
|
|
<template #icon><i class="icon detail" /></template>
|
|
|
|
|
</a-float-button>
|
|
|
|
|
<div v-if="show" class="card-container">
|
|
|
|
|
<RobotDetailCard v-if="isRobot" :token="EDITOR_KEY" :current="current.id" />
|
2025-07-18 15:52:26 +08:00
|
|
|
|
<PointDetailCard
|
|
|
|
|
v-if="isPoint"
|
|
|
|
|
:token="EDITOR_KEY"
|
|
|
|
|
:current="current.id"
|
|
|
|
|
:storage-locations="storageLocationService?.getLocationsByPointId(current.id)"
|
|
|
|
|
/>
|
2025-05-25 00:07:22 +08:00
|
|
|
|
<RouteDetailCard v-if="isRoute" :token="EDITOR_KEY" :current="current.id" />
|
|
|
|
|
<AreaDetailCard v-if="isArea" :token="EDITOR_KEY" :current="current.id" />
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
2025-05-22 18:06:30 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
.editor-container {
|
|
|
|
|
background-color: transparent !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-container {
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 80px;
|
|
|
|
|
right: 64px;
|
|
|
|
|
z-index: 100;
|
|
|
|
|
width: 320px;
|
|
|
|
|
height: calc(100% - 96px);
|
|
|
|
|
overflow: visible;
|
2025-05-25 16:45:45 +08:00
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
|
|
|
|
& > * {
|
|
|
|
|
pointer-events: all;
|
|
|
|
|
}
|
2025-05-22 18:06:30 +08:00
|
|
|
|
}
|
|
|
|
|
</style>
|