From 149fcb5a37120e8dcaad76046cdeb613fe4f0091 Mon Sep 17 00:00:00 2001 From: xudan Date: Thu, 30 Oct 2025 09:39:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E7=8E=AF=E5=A2=83token=EF=BC=8C=E4=BC=98=E5=8C=96=E6=9C=BA?= =?UTF-8?q?=E5=99=A8=E4=BA=BA=E5=9B=BE=E5=85=83=E6=89=B9=E9=87=8F=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 2 +- src/hooks/usePlaybackWebSocket.ts | 86 +---------------------- src/hooks/useRobotRenderer.ts | 105 ++++++++++++++++++++++++++++ src/pages/movement-supervision.vue | 108 +---------------------------- 4 files changed, 111 insertions(+), 190 deletions(-) create mode 100644 src/hooks/useRobotRenderer.ts diff --git a/.env.development b/.env.development index b9b60c8..1902deb 100644 --- a/.env.development +++ b/.env.development @@ -4,5 +4,5 @@ ENV_WEBSOCKET_BASE=/ws ENV_STORAGE_WEBSOCKET_BASE=/vwedWs # 开发环境token配置 - 可以手动设置或从另一个项目获取后填入 -ENV_DEV_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NjE2NzY1NDksInVzZXJuYW1lIjoiYWRtaW4ifQ.q0SIqem1KTUfPm9wgjAG8YG6UmYiIcs_QV6AEJVmr8U +ENV_DEV_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NjE5MTYzNTIsInVzZXJuYW1lIjoiYWRtaW4ifQ.UEDfpkrB6y1xuNQt5AEKAnAyeEb4HKI8VC0hDXm4r9E ENV_DEV_TENANT_ID=1000 diff --git a/src/hooks/usePlaybackWebSocket.ts b/src/hooks/usePlaybackWebSocket.ts index 2e7c0e5..dd27c97 100644 --- a/src/hooks/usePlaybackWebSocket.ts +++ b/src/hooks/usePlaybackWebSocket.ts @@ -1,4 +1,3 @@ -import { LockState } from '@meta2d/core'; import { message } from 'ant-design-vue'; import { type Ref, ref, type ShallowRef, shallowRef } from 'vue'; @@ -6,6 +5,7 @@ import { type RobotRealtimeInfo } from '../apis/robot'; import type { EditorService } from '../services/editor.service'; import { useViewState } from '../services/useViewState'; import ws from '../services/ws'; +import { useRobotRenderer } from './useRobotRenderer'; // Define the structure of WebSocket messages for playback type PlaybackMessage = { @@ -14,10 +14,6 @@ type PlaybackMessage = { data: any; }; -type PlaybackSceneData = { - json: string; - totalDuration: number; -}; // Hook's return type definition // Hook's return type definition @@ -60,6 +56,7 @@ export function usePlaybackWebSocket( let hasReceivedAmrData = false; // 新增:用于确保 onFirstAmrData 只调用一次的标志 const { calculateCenterPoint, jumpToPosition } = useViewState(); + const { batchUpdateRobots } = useRobotRenderer(editorService); const locateCenter = () => { if (editorService.value) { @@ -174,85 +171,6 @@ export function usePlaybackWebSocket( console.warn(`播放速度控制 (${speed}x) 尚未实现`); }; - const batchUpdateRobots = (updates: Array<{ id: string; data: RobotRealtimeInfo }>) => { - const editor = editorService.value; - if (!editor || updates.length === 0) return; - - const allPenUpdates: any[] = []; - - updates.forEach(({ id, data }) => { - const robotExists = editor.checkRobotById(id); - if (robotExists) { - const { x, y, angle, active, path: points, isWaring, isFault, isCharging, isCarrying, canOrder, ...rest } = - data; - - // 1. 更新机器人缓存的业务数据 (与实时模式对齐) - editor.updateRobot(id, { - ...rest, - isCharging: (isCharging as any) ?? 0, - isCarrying: (isCarrying as any) ?? 0, - canOrder: false as any, - }); - - // 2. 准备图元更新负载对象 - const penUpdatePayload: any = { id }; - const robotState: any = {}; - - // 2.1 路径处理 (如果数据中包含) - if (points?.length) { - const cx = x || 37; - const cy = y || 37; - robotState.path = points.map((p) => ({ x: p.x - cx, y: p.y - cy })); - } - - // 2.2 合并其他机器人状态 - if (active !== undefined) robotState.active = active||true; - if (isWaring !== undefined) robotState.isWaring = isWaring; - if (isFault !== undefined) robotState.isFault = isFault; - if (isCharging !== undefined) robotState.isCharging = isCharging; - if (isCarrying !== undefined) robotState.isCarrying = isCarrying; - if (canOrder !== undefined) robotState.canOrder = canOrder; - - if (Object.keys(robotState).length > 0) { - penUpdatePayload.robot = robotState; - } - - // 2.3 合并位置、可见性和角度 - if (x != null && y != null) { - penUpdatePayload.x = x - 60; - penUpdatePayload.y = y - 60; - penUpdatePayload.visible = true; // [核心修复] 明确设置 visible 为 true - penUpdatePayload.locked = LockState.None; - } - if (angle != null) { - penUpdatePayload.rotate = -angle + 180; - } - - // 仅当有实际更新时才推入数组 - if (Object.keys(penUpdatePayload).length > 1) { - allPenUpdates.push(penUpdatePayload); - } - } - }); - - // 4. 批量更新所有图元 - if (allPenUpdates.length > 0) { - allPenUpdates.forEach((update) => { - editor.setValue(update, { render: false, history: false, doEvent: false }); - }); - - // 5. [时序问题修复] 在所有图元属性更新后,再统一更新状态覆盖图标 - // 这样可以确保 updateRobotStatusOverlay 读取到最新的 visible: true 状态 - allPenUpdates.forEach((update) => { - const { id, x, y, rotate } = update; - const newPositionForOverlay = x !== undefined && y !== undefined ? { x, y, rotate } : undefined; - editor.updateRobotStatusOverlay?.(id, false, newPositionForOverlay); - }); - } - - // 5. 统一渲染 - editor.render(); - }; const renderLoop = () => { try { diff --git a/src/hooks/useRobotRenderer.ts b/src/hooks/useRobotRenderer.ts new file mode 100644 index 0000000..cd5625e --- /dev/null +++ b/src/hooks/useRobotRenderer.ts @@ -0,0 +1,105 @@ +import { LockState } from '@meta2d/core'; +import { isNil } from 'lodash-es'; +import type { ShallowRef } from 'vue'; + +import type { RobotRealtimeInfo } from '../apis/robot'; +import type { EditorService } from '../services/editor.service'; + +/** + * 封装了实时模式与回放模式下通用的机器人图元批量更新与渲染逻辑 + * @param editorService - 编辑器服务的 shallowRef 引用 + * @returns 包含 batchUpdateRobots 方法的对象 + */ +export function useRobotRenderer(editorService: ShallowRef) { + /** + * 批量更新机器人数据,减少渲染调用次数 + * @param updates 需要更新的机器人数据数组 + */ + const batchUpdateRobots = (updates: Array<{ id: string; data: RobotRealtimeInfo }>) => { + const editor = editorService.value; + if (!editor || updates.length === 0) return; + + // 用于收集所有图元(pen)的更新数据 + const allPenUpdates: any[] = []; + + updates.forEach(({ id, data }) => { + const { + x, + y, + active, + angle, + path: points, + isWaring, + isFault, + isCharging = 0, + isCarrying = 0, + canOrder, + ...rest + } = data; + + // 1. 更新机器人缓存的业务数据 + editor.updateRobot(id, { + ...rest, + isCharging: isCharging as any, + isCarrying: isCarrying as any, + canOrder: canOrder as any, + }); + + // 2. 准备图元(pen)的更新负载对象,将多个更新合并 + const penUpdatePayload: any = { id }; + const robotState: any = {}; + + // 2.1 处理路径并将其放入 robotState + if (points?.length) { + const cx = x ?? 37; + const cy = y ?? 37; + robotState.path = points.map((p) => ({ x: p.x - cx, y: p.y - cy })); + } + + // 2.2 合并其他机器人状态 + if (active !== undefined) robotState.active = active; + if (isWaring !== undefined) robotState.isWaring = isWaring; + if (isFault !== undefined) robotState.isFault = isFault; + if (isCharging !== undefined) robotState.isCharging = isCharging; + if (isCarrying !== undefined) robotState.isCarrying = isCarrying; + if (canOrder !== undefined) robotState.canOrder = canOrder; + + if (Object.keys(robotState).length > 0) { + penUpdatePayload.robot = robotState; + } + + // 2.3 合并位置、可见性和角度 + if (!isNil(x) && !isNil(y)) { + penUpdatePayload.x = x - 60; + penUpdatePayload.y = y - 60; + penUpdatePayload.visible = true; + penUpdatePayload.locked = LockState.None; + } + if (angle != null) { + penUpdatePayload.rotate = -angle + 180; + } + + if (Object.keys(penUpdatePayload).length > 1) { + allPenUpdates.push(penUpdatePayload); + } + + const newPositionForOverlay = + penUpdatePayload.x !== undefined && penUpdatePayload.y !== undefined + ? { x: penUpdatePayload.x, y: penUpdatePayload.y, rotate: penUpdatePayload.rotate } + : undefined; + editor.updateRobotStatusOverlay?.(id, false, newPositionForOverlay); + }); + + if (allPenUpdates.length > 0) { + allPenUpdates.forEach((update) => { + editor.setValue(update, { render: false, history: false, doEvent: false }); + }); + } + + editor.render(); + }; + + return { + batchUpdateRobots, + }; +} \ No newline at end of file diff --git a/src/pages/movement-supervision.vue b/src/pages/movement-supervision.vue index 2cee57e..8a3b18c 100644 --- a/src/pages/movement-supervision.vue +++ b/src/pages/movement-supervision.vue @@ -16,6 +16,7 @@ import FollowViewNotification from '../components/follow-view-notification.vue'; import PlaybackController from '../components/PlaybackController.vue'; import RobotLabels from '../components/robot-labels.vue'; import { usePlaybackWebSocket } from '../hooks/usePlaybackWebSocket'; +import { useRobotRenderer } from '../hooks/useRobotRenderer'; import { autoDoorSimulationService, type AutoDoorWebSocketData } from '../services/auto-door-simulation.service'; import { type ContextMenuState, @@ -111,6 +112,7 @@ const playback = usePlaybackWebSocket(editor, async () => { // [关键修复] 只有在收到第一帧机器人数据后才初始化机器人图元 await editor.value?.initRobots(); }); +const { batchUpdateRobots: batchUpdateRobotsRenderer } = useRobotRenderer(editor); watch(mode, async (newMode) => { if (newMode === 'live') { @@ -276,110 +278,6 @@ const monitorScene = async () => { // 用于存储 requestAnimationFrame 的 ID,方便在组件卸载或 WebSocket 关闭时取消动画循环。 let animationFrameId: number; - /** - * 批量更新机器人数据,减少渲染调用次数 - * @param updates 需要更新的机器人数据数组 - */ - const batchUpdateRobots = (updates: Array<{ id: string; data: RobotRealtimeInfo }>) => { - console.log('[实时模式] batchUpdateRobots 被调用,更新数量:', updates.length); - if (!editor.value || updates.length === 0) return; - - // 用于收集所有图元(pen)的更新数据 - const allPenUpdates: any[] = []; - - updates.forEach(({ id, data }) => { - const { - x, - y, - active, - angle, - path: points, - isWaring, - isFault, - isCharging = 0, - isCarrying = 0, - canOrder, - ...rest - } = data; - - // 1. 更新机器人缓存的业务数据 - console.log('[实时模式] 更新机器人业务数据:', id, { x, y, active, angle }); - editor.value?.updateRobot(id, { - ...rest, - isCharging: isCharging as any, - isCarrying: isCarrying as any, - canOrder: canOrder as any, - }); - - // 2. 准备图元(pen)的更新负载对象,将多个更新合并 - const penUpdatePayload: any = { id }; - const robotState: any = {}; - - // 2.1 处理路径并将其放入 robotState - // 处理路径坐标转换,参考refreshRobot方法的逻辑 - if (points?.length && !isMonitorMode.value) { - // 新路径:相对于机器人中心的坐标 - const cx = x || 37; // 机器人中心X坐标,默认37 - const cy = y || 37; // 机器人中心Y坐标,默认37 - robotState.path = points.map((p) => ({ x: p.x - cx, y: p.y - cy })); - } - - // 2.2 合并其他机器人状态 - if (active !== undefined) robotState.active = active; - if (isWaring !== undefined) robotState.isWaring = isWaring; - if (isFault !== undefined) robotState.isFault = isFault; - if (isCharging !== undefined) robotState.isCharging = isCharging; - if (isCarrying !== undefined) robotState.isCarrying = isCarrying; - if (canOrder !== undefined) robotState.canOrder = canOrder; - - // 将合并后的状态赋给 payload - if (Object.keys(robotState).length > 0) { - penUpdatePayload.robot = robotState; - } - - // 2.3 合并位置、可见性和角度 - if (!isNil(x) && !isNil(y)) { - penUpdatePayload.x = x - 60; - penUpdatePayload.y = y - 60; - penUpdatePayload.visible = true; - penUpdatePayload.locked = LockState.None; - console.log('[实时模式] 设置机器人可见性:', id, { x: x - 60, y: y - 60, visible: true }); - } - if (angle != null) { - penUpdatePayload.rotate = -angle + 180; - } - - // 只有当有实际更新时才推入数组 - if (Object.keys(penUpdatePayload).length > 1) { - allPenUpdates.push(penUpdatePayload); - } - - // 3. 更新状态覆盖图标 (此API调用如果不能合并,则保留在循环内) - // 使用刚刚为机器人计算出的新位置来更新覆盖物,以确保同步 - const newPositionForOverlay = - penUpdatePayload.x !== undefined && penUpdatePayload.y !== undefined - ? { x: penUpdatePayload.x, y: penUpdatePayload.y, rotate: penUpdatePayload.rotate } - : undefined; - editor.value?.updateRobotStatusOverlay?.(id, false, newPositionForOverlay); - }); - - // 4. 使用Meta2D的批量更新方法一次性提交所有更改 - if (allPenUpdates.length > 0) { - allPenUpdates.forEach((update) => { - editor.value?.setValue(update, { render: false, history: false, doEvent: false }); - }); - } - - // 5. 批量更新完成后,统一渲染一次 - /* - 为了让机器人在地图上最基本地被绘制出来并能够移动,后台推送的 WebSocket 数据最少需要 id, x, y 这 3 - 个字段。 - - 为了达到一个功能上比较完整的视觉效果(带光圈、能旋转),则最少需要 id, x, y, angle, active 这 5 - 个字段。 - */ - editor.value?.render(); - }; /** * 渲染循环函数。 @@ -411,7 +309,7 @@ const monitorScene = async () => { // 批量更新机器人,减少渲染调用次数 if (updates.length > 0) { - batchUpdateRobots(updates); + batchUpdateRobotsRenderer(updates); } // 处理缓冲的自动门点数据