From cf2a2f97f538ef3786f77d3233b419cdc5e543fb Mon Sep 17 00:00:00 2001 From: xudan Date: Fri, 26 Sep 2025 18:31:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E5=9B=9E=E6=94=BE=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=9B=9E=E6=94=BE=E6=97=A5=E6=9C=9F=E5=B9=B6?= =?UTF-8?q?=E9=9B=86=E6=88=90=E6=92=AD=E6=94=BE=E6=8E=A7=E5=88=B6=E5=99=A8?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E5=AE=9E=E6=97=B6=E7=9B=91=E6=8E=A7?= =?UTF-8?q?=E4=B8=8E=E5=9B=9E=E6=94=BE=E6=A8=A1=E5=BC=8F=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/PlaybackController.vue | 105 +++++++++++++ src/hooks/usePlaybackWebSocket.ts | 207 ++++++++++++++++++++++++++ src/pages/movement-supervision.vue | 79 +++++++++- 3 files changed, 390 insertions(+), 1 deletion(-) create mode 100644 src/components/PlaybackController.vue create mode 100644 src/hooks/usePlaybackWebSocket.ts diff --git a/src/components/PlaybackController.vue b/src/components/PlaybackController.vue new file mode 100644 index 0000000..62a72e9 --- /dev/null +++ b/src/components/PlaybackController.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/src/hooks/usePlaybackWebSocket.ts b/src/hooks/usePlaybackWebSocket.ts new file mode 100644 index 0000000..a7ac04b --- /dev/null +++ b/src/hooks/usePlaybackWebSocket.ts @@ -0,0 +1,207 @@ + +import { message } from 'ant-design-vue'; +import { type Ref, ref, type ShallowRef,shallowRef } from 'vue'; + +import type { RobotRealtimeInfo } from '../apis/robot'; +import type { EditorService } from '../services/editor.service'; + +// Define the structure of WebSocket messages for playback +type PlaybackMessage = { + type: 'AMR' | 'SCENE'; + timestamp: number; + data: any; +}; + +// Hook's return type definition +export interface UsePlaybackWebSocketReturn { + // Connection status + isConnected: Ref; + + // Playback state + isPlaying: Ref; + currentTime: Ref; + totalDuration: Ref; + + // Data for rendering + currentSceneId: Ref; + sceneJson: Ref; + + // Methods + connect: (historySceneId: string) => void; + disconnect: () => void; + play: () => void; + pause: () => void; + seek: (timestamp: number) => void; + changeSpeed: (speed: number) => void; // Placeholder for speed control +} + +export function usePlaybackWebSocket(editorService: ShallowRef): UsePlaybackWebSocketReturn { + const client = shallowRef(null); + const isConnected = ref(false); + const isPlaying = ref(false); + + const currentTime = ref(0); + const totalDuration = ref(0); + const currentSceneId = ref(null); + const sceneJson = ref(null); + + const latestRobotData = new Map(); + let animationFrameId: number; + + const connect = (historySceneId: string) => { + if (client.value) { + disconnect(); + } + + // TODO: Replace with the actual WebSocket host from environment variables or config. + const wsHost = import.meta.env.VITE_WEBSOCKET_URL || `ws://${window.location.host}`; + const wsUrl = `${wsHost}/history/scene/logplayback/${historySceneId}`; + + const ws = new WebSocket(wsUrl); + client.value = ws; + + ws.onopen = () => { + isConnected.value = true; + message.success('回放连接已建立'); + startRenderLoop(); + }; + + ws.onmessage = (event) => { + const msg = JSON.parse(event.data) as PlaybackMessage; + + if (msg.timestamp) { + currentTime.value = msg.timestamp; + } + + if (msg.type === 'SCENE') { + // As per user feedback, data is the full scene JSON string + sceneJson.value = msg.data; + latestRobotData.clear(); // Clear robot data when scene changes + } else if (msg.type === 'AMR') { + (msg.data as RobotRealtimeInfo[]).forEach(robot => { + latestRobotData.set(robot.id, robot); + }); + } + }; + + ws.onclose = () => { + isConnected.value = false; + isPlaying.value = false; + stopRenderLoop(); + message.info('回放连接已断开'); + }; + + ws.onerror = (error) => { + console.error('回放 WebSocket 发生错误:', error); + message.error('回放连接发生错误'); + disconnect(); + }; + }; + + const disconnect = () => { + if (client.value) { + client.value.close(); + client.value = null; + } + }; + + const sendCommand = (command: string) => { + if (client.value && client.value.readyState === WebSocket.OPEN) { + client.value.send(command); + } else { + message.warn('回放连接未建立或已断开,无法发送指令'); + } + }; + + const play = () => { + sendCommand('PLAY'); + isPlaying.value = true; + }; + + const pause = () => { + // The API doc says PAUSE:{timestamp}, using current time. + sendCommand(`PAUSE:${currentTime.value}`); + isPlaying.value = false; + }; + + const seek = (timestamp: number) => { + sendCommand(`SEEK:${timestamp}`); + // If not playing, seeking should not start playing. + }; + + const changeSpeed = (speed: number) => { + // The provided API doc does not include speed control. + // This is a placeholder for future implementation if the backend supports it. + console.warn(`播放速度控制 (${speed}x) 尚未实现`); + }; + + const batchUpdateRobots = (updates: Array<{ id: string; data: RobotRealtimeInfo }>) => { + const editor = editorService.value; + if (!editor || updates.length === 0) return; + + // This logic is adapted from movement-supervision.vue for consistency. + updates.forEach(({ id, data }) => { + if (editor.checkRobotById(id)) { + const { x, y, angle, ...rest } = data; + editor.updateRobot(id, rest); + editor.setValue({ + id, + x: x - 60, + y: y - 60, + rotate: -angle! + 180, + visible: true, + }, { render: false }); + } + }); + }; + + const renderLoop = () => { + const updates: Array<{ id: string; data: RobotRealtimeInfo }> = []; + + // Naive implementation: process all buffered data in one frame. + // Can be optimized with time slicing like in movement-supervision.vue if needed. + for (const [id, data] of latestRobotData.entries()) { + updates.push({ id, data }); + } + latestRobotData.clear(); + + if (updates.length > 0) { + batchUpdateRobots(updates); + } + + // Check for playback end + if (isPlaying.value && currentTime.value >= totalDuration.value && totalDuration.value > 0) { + pause(); + // Optionally, set current time to total duration to prevent overflow on slider + currentTime.value = totalDuration.value; + } + + editorService.value?.render(); + + animationFrameId = requestAnimationFrame(renderLoop); + }; + + const startRenderLoop = () => { + stopRenderLoop(); // Ensure no multiple loops are running + renderLoop(); + }; + + const stopRenderLoop = () => { + cancelAnimationFrame(animationFrameId); + }; + + return { + isConnected, + isPlaying, + currentTime, + totalDuration, + currentSceneId, + sceneJson, + connect, + disconnect, + play, + pause, + seek, + changeSpeed, + }; +} diff --git a/src/pages/movement-supervision.vue b/src/pages/movement-supervision.vue index 30cf1c9..6658a4c 100644 --- a/src/pages/movement-supervision.vue +++ b/src/pages/movement-supervision.vue @@ -1,15 +1,17 @@