# 机器人运动监控组件详细分析

## 1. 组件架构概述

### 1.1 核心组件结构

`movement-supervision.vue` 是机器人运动监控的主要组件，负责实时显示机器人在场景中的位置和状态。

```typescript
// 组件核心属性
type Props = {
  sid: string; // 场景ID
  id?: string; // 机器人组ID（可选）
};
```

### 1.2 依赖服务架构

- **EditorService**: 基于Meta2D的场景编辑器服务
- **WebSocket服务**: 提供实时数据通信
- **场景API服务**: 处理场景数据的增删改查

## 2. 组件生命周期详解

### 2.1 组件初始化流程

```typescript
onMounted(async () => {
  await readScene(); // 步骤1: 加载场景数据
  await editor.value?.initRobots(); // 步骤2: 初始化机器人
  await monitorScene(); // 步骤3: 建立WebSocket监控
});
```

#### 步骤1: readScene() - 场景数据加载

```typescript
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);
};
```

**关键问题点**: 每个页面实例都独立调用API获取场景数据，可能导致：

- 不同时间点获取的数据版本不一致
- 网络延迟造成的数据获取时差
- 场景数据在获取期间被其他页面修改

#### 步骤2: initRobots() - 机器人初始化

```typescript
public async initRobots(): Promise<void> {
  await Promise.all(
    this.robots.map(async ({ id, label, type }) => {
      const pen: MapPen = {
        ...this.#mapRobotImage(type, true),
        id,
        name: 'robot',
        tags: ['robot'],
        x: 0,              // 关键: 初始位置固定为(0,0)
        y: 0,              // 关键: 初始位置固定为(0,0)
        width: 74,
        height: 74,
        lineWidth: 1,
        robot: { type },
        visible: false,    // 关键: 初始状态为不可见
        text: label,
        textTop: -24,
        whiteSpace: 'nowrap',
        ellipsis: false,
        locked: LockState.Disable,
      };
      await this.addPen(pen, false, true, true);
    }),
  );
}
```

**问题分析**:

- 所有机器人初始位置都设为`(0,0)`
- 初始状态为`visible: false`，需要WebSocket数据才能显示
- 如果WebSocket连接延迟，不同页面的机器人可能长时间处于不可见状态

#### 步骤3: monitorScene() - WebSocket监控建立

```typescript
const monitorScene = async () => {
  client.value?.close(); // 关闭之前的连接
  const ws = await monitorSceneById(props.sid); // 创建新连接
  if (isNil(ws)) return;

  ws.onmessage = (e) => {
    const { id, x, y, active, angle, path, ...rest } = <RobotRealtimeInfo>JSON.parse(e.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 });
    }
  };
  client.value = ws;
};
```

## 3. 机器人实时移动机制深度分析

### 3.1 WebSocket消息处理流程

每当接收到WebSocket消息时，会执行以下处理逻辑：

1. **消息解析**: 将JSON字符串解析为`RobotRealtimeInfo`对象
2. **机器人验证**: 调用`checkRobotById(id)`验证机器人是否存在
3. **基本信息更新**: 调用`updateRobot(id, rest)`更新电量、状态等信息
4. **位置处理**: 根据坐标是否存在进行不同处理

### 3.2 位置更新核心逻辑: refreshRobot()

```typescript
public refreshRobot(id: RobotInfo['id'], info: Partial<RobotRealtimeInfo>): void {
  const pen = this.getPenById(id);
  const { rotate: or, robot } = pen ?? {};
  if (!robot?.type) return;

  // 获取当前机器人位置
  const { x: ox, y: oy } = this.getPenRect(pen!);

  // 解析新的位置信息（默认值为37,37是机器人中心点）
  const { x: cx = 37, y: cy = 37, active, angle, path: points } = info;

  // 关键坐标转换: 从中心点坐标转换为左上角坐标
  const x = cx - 37;  // 机器人宽度74，中心偏移37
  const y = cy - 37;  // 机器人高度74，中心偏移37

  const rotate = angle ?? or;  // 角度更新

  // 路径坐标转换
  const path =
    points?.map((p) => ({ x: p.x - cx, y: p.y - cy })) ??  // 新路径相对于机器人中心
    robot.path?.map((p) => ({ x: p.x + ox! - x, y: p.y + oy! - y }));  // 旧路径坐标调整

  const o = { ...robot, ...omitBy({ active, path }, isNil) };

  if (isNil(active)) {
    // active为null时，只更新位置不改变图标
    this.setValue(
      { id, x, y, rotate, robot: o, visible: true },
      { render: true, history: false, doEvent: false }
    );
  } else {
    // active有值时，同时更新图标状态（运行/停止状态图标不同）
    this.setValue(
      { id, ...this.#mapRobotImage(robot.type, active), x, y, rotate, robot: o, visible: true },
      { render: true, history: false, doEvent: false }
    );
  }
}
```

### 3.3 机器人图标映射逻辑

```typescript
#mapRobotImage(
  type: RobotType,
  active?: boolean,
): Required<Pick<MapPen, 'image' | 'iconWidth' | 'iconHeight' | 'iconTop'>> {
  const theme = this.data().theme;
  const image = import.meta.env.BASE_URL +
    (active ? `/robot/${type}-active-${theme}.png` : `/robot/${type}-${theme}.png`);
  return {
    image,
    iconWidth: 34,
    iconHeight: 54,
    iconTop: -5
  };
}
```

### 3.4 机器人绘制函数

```typescript
function drawRobot(ctx: CanvasRenderingContext2D, pen: MapPen): void {
  const theme = sTheme.editor;
  const { lineWidth: s = 1 } = pen.calculative ?? {};
  const { x = 0, y = 0, width: w = 0, height: h = 0, rotate: deg = 0 } = pen.calculative?.worldRect ?? {};
  const { active, path } = pen.robot ?? {};

  if (!active) return; // 关键: 非活跃状态不绘制路径

  const ox = x + w / 2; // 机器人中心X坐标
  const oy = y + h / 2; // 机器人中心Y坐标

  ctx.save();
  // 绘制机器人本体（椭圆）
  ctx.ellipse(ox, oy, w / 2, h / 2, 0, 0, Math.PI * 2);
  ctx.fillStyle = get(theme, 'robot.fill') ?? '';
  ctx.fill();
  ctx.strokeStyle = get(theme, 'robot.stroke') ?? '';
  ctx.stroke();

  // 绘制运动路径
  if (path?.length) {
    ctx.strokeStyle = get(theme, 'robot.line') ?? '';
    ctx.lineCap = 'round';
    ctx.lineWidth = s * 4;
    ctx.setLineDash([s * 5, s * 10]); // 虚线样式
    ctx.translate(ox, oy);
    ctx.rotate((-deg * Math.PI) / 180); // 根据机器人角度旋转

    // 绘制路径线条
    ctx.beginPath();
    ctx.moveTo(0, 0);
    path.forEach((d) => ctx.lineTo(d.x * s, d.y * s));
    ctx.stroke();

    // 绘制路径终点箭头
    const { x: ex1 = 0, y: ey1 = 0 } = nth(path, -1) ?? {};
    const { x: ex2 = 0, y: ey2 = 0 } = nth(path, -2) ?? {};
    const r = Math.atan2(ey1 - ey2, ex1 - ex2) + Math.PI;
    ctx.setLineDash([0]);
    ctx.translate(ex1 * s, ey1 * s);
    ctx.beginPath();
    ctx.moveTo(Math.cos(r + Math.PI / 4) * s * 10, Math.sin(r + Math.PI / 4) * s * 10);
    ctx.lineTo(0, 0);
    ctx.lineTo(Math.cos(r - Math.PI / 4) * s * 10, Math.sin(r - Math.PI / 4) * s * 10);
    ctx.stroke();
    ctx.setTransform(1, 0, 0, 1, 0, 0);
  }
  ctx.restore();
}
```

## 4. 多页面位置不一致问题深度分析

### 4.1 根本原因：缺乏全局状态同步机制

每个页面实例都是完全独立的，具体表现为：

1. **独立的EditorService实例**

   - 每个页面创建独立的`new EditorService(container.value!)`
   - 各自维护独立的机器人状态映射`#robotMap`
   - 无法共享机器人位置信息

2. **独立的WebSocket连接**
   - 每个页面调用`monitorSceneById(props.sid)`创建独立连接
   - 服务器可能向不同连接推送不同时间点的数据
   - 网络延迟导致消息到达时间不同

### 4.2 具体问题场景分析

#### 场景1: 初始化时间差异

```typescript
// 页面A在时间T1执行
onMounted(async () => {
  await readScene(); // T1时刻的场景数据
  await initRobots(); // 创建机器人，位置(0,0)，visible:false
  await monitorScene(); // T1+100ms建立WebSocket
});

// 页面B在时间T2执行（T2 > T1）
onMounted(async () => {
  await readScene(); // T2时刻的场景数据（可能已更新）
  await initRobots(); // 创建机器人，位置(0,0)，visible:false
  await monitorScene(); // T2+80ms建立WebSocket
});
```

**结果**: 两个页面获取的初始场景数据可能不同，机器人列表或配置存在差异。

#### 场景2: WebSocket消息时序差异

```typescript
// WebSocket消息处理逻辑
ws.onmessage = (e) => {
  const { id, x, y, active, angle, path, ...rest } = JSON.parse(e.data || '{}');

  if (isNil(x) || isNil(y)) {
    // 关键问题: 无坐标消息会隐藏机器人
    editor.value.updatePen(id, { visible: false });
  } else {
    editor.value.refreshRobot(id, { x, y, active, angle, path });
  }
};
```

**问题分析**:

- 页面A先收到有坐标的消息，机器人显示在位置(100, 200)
- 页面B后收到无坐标的消息，机器人被隐藏
- 页面C收到旧的坐标消息，机器人显示在位置(80, 150)

#### 场景3: 坐标转换精度问题

```typescript
// refreshRobot中的坐标转换
const { x: cx = 37, y: cy = 37, active, angle, path: points } = info;
const x = cx - 37; // 默认值37导致的问题
const y = cy - 37;

// 当服务器发送的坐标为null/undefined时
// cx和cy都会使用默认值37，导致机器人位置为(0,0)
```

**问题**: 不同页面接收到的消息中坐标字段可能为`null`、`undefined`或有效数值，默认值处理导致位置计算不一致。

#### 场景4: 机器人状态检查差异

```typescript
if (!editor.value?.checkRobotById(id)) return;

// checkRobotById实现
public checkRobotById(id: RobotInfo['id']): boolean {
  return this.#robotMap.has(id);
}
```

**问题**: 不同页面的`#robotMap`内容可能不同，导致某些页面忽略特定机器人的更新消息。

### 4.3 路径绘制不一致问题

```typescript
// 路径坐标转换逻辑
const path =
  points?.map((p) => ({ x: p.x - cx, y: p.y - cy })) ?? // 新路径处理
  robot.path?.map((p) => ({ x: p.x + ox! - x, y: p.y + oy! - y })); // 旧路径处理
```

**问题分析**:

1. 新路径使用`p.x - cx, p.y - cy`进行坐标转换
2. 旧路径使用`p.x + ox! - x, p.y + oy! - y`进行坐标转换
3. 两种转换方式在特定情况下可能产生不同结果
4. 不同页面可能处于新旧路径的不同阶段

### 4.4 渲染状态不同步

```typescript
// setValue方法的渲染参数
this.setValue(
  { id, x, y, rotate, robot: o, visible: true },
  { render: true, history: false, doEvent: false }, // 立即渲染，不记录历史
);
```

**问题**:

- `render: true`表示立即重新渲染
- 不同页面的渲染时机不同步
- 可能出现某个页面正在渲染时收到新消息的情况

## 5. 解决方案详细设计

### 5.1 方案一: 全局状态管理器

```typescript
/**
 * 全局机器人状态管理器
 * 单例模式，确保所有页面共享同一份状态
 */
class GlobalRobotStateManager {
  private static instance: GlobalRobotStateManager;

  // 存储所有机器人的最新状态
  private robotStates = new Map<string, RobotRealtimeInfo>();

  // 订阅者列表，用于通知状态变化
  private subscribers = new Set<(robotId: string, info: RobotRealtimeInfo) => void>();

  // 连接管理，避免重复连接
  private connections = new Map<string, WebSocket>();

  static getInstance(): GlobalRobotStateManager {
    if (!this.instance) {
      this.instance = new GlobalRobotStateManager();
    }
    return this.instance;
  }

  /**
   * 订阅机器人状态变化
   */
  subscribe(callback: (robotId: string, info: RobotRealtimeInfo) => void): () => void {
    this.subscribers.add(callback);

    // 立即推送当前所有机器人状态
    this.robotStates.forEach((info, robotId) => {
      callback(robotId, info);
    });

    // 返回取消订阅函数
    return () => this.subscribers.delete(callback);
  }

  /**
   * 更新机器人状态并通知所有订阅者
   */
  updateRobotState(robotId: string, info: RobotRealtimeInfo): void {
    // 合并状态更新
    const currentState = this.robotStates.get(robotId) || ({} as RobotRealtimeInfo);
    const newState = { ...currentState, ...info };

    this.robotStates.set(robotId, newState);

    // 通知所有订阅者
    this.subscribers.forEach((callback) => {
      try {
        callback(robotId, newState);
      } catch (error) {
        console.error('机器人状态更新回调执行失败:', error);
      }
    });
  }

  /**
   * 获取或创建WebSocket连接（复用连接）
   */
  async getOrCreateConnection(sceneId: string): Promise<WebSocket | null> {
    // 检查现有连接
    const existingConnection = this.connections.get(sceneId);
    if (existingConnection && existingConnection.readyState === WebSocket.OPEN) {
      return existingConnection;
    }

    try {
      const ws = await monitorSceneById(sceneId);
      if (!ws) return null;

      // 设置消息处理
      ws.onmessage = (e) => {
        try {
          const robotInfo = JSON.parse(e.data || '{}') as RobotRealtimeInfo;
          this.updateRobotState(robotInfo.id, robotInfo);
        } catch (error) {
          console.error('WebSocket消息解析失败:', error);
        }
      };

      // 设置连接关闭处理
      ws.onclose = () => {
        this.connections.delete(sceneId);
      };

      // 存储连接
      this.connections.set(sceneId, ws);
      return ws;
    } catch (error) {
      console.error('创建WebSocket连接失败:', error);
      return null;
    }
  }
}
```

### 5.2 方案二: 改进的组件实现

```typescript
// 改进后的movement-supervision.vue核心逻辑
<script setup lang="ts">
import { GlobalRobotStateManager } from '@/services/global-robot-state';

const props = defineProps<Props>();
const globalStateManager = GlobalRobotStateManager.getInstance();

// 移除原有的monitorScene函数，改用全局状态管理

const initializeMonitoring = async () => {
  // 确保WebSocket连接存在
  await globalStateManager.getOrCreateConnection(props.sid);

  // 订阅机器人状态变化
  const unsubscribe = globalStateManager.subscribe((robotId, robotInfo) => {
    if (!editor.value?.checkRobotById(robotId)) return;

    // 更新机器人基本信息
    const { id, x, y, active, angle, path, ...rest } = robotInfo;
    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 });
    }
  });

  // 组件卸载时取消订阅
  onUnmounted(() => {
    unsubscribe();
  });
};

onMounted(async () => {
  await readScene();
  await editor.value?.initRobots();
  await initializeMonitoring();  // 使用改进的初始化方法
});
</script>
```

### 5.3 方案三: EditorService增强

```typescript
// 为EditorService添加状态缓存和同步机制
export class EditorService extends Meta2d {
  // 添加状态缓存
  private robotStateCache = new Map<string, RobotRealtimeInfo>();

  /**
   * 改进的坐标转换方法
   */
  private normalizeCoordinates(info: Partial<RobotRealtimeInfo>): { x: number; y: number } | null {
    const { x: cx, y: cy } = info;

    // 严格的坐标验证
    if (typeof cx !== 'number' || typeof cy !== 'number' || isNaN(cx) || isNaN(cy) || cx < 0 || cy < 0) {
      return null; // 返回null表示无效坐标
    }

    // 坐标转换：从中心点转换为左上角
    return {
      x: cx - 37, // 机器人宽度74，中心偏移37
      y: cy - 37, // 机器人高度74，中心偏移37
    };
  }

  /**
   * 改进的refreshRobot方法
   */
  public refreshRobot(id: RobotInfo['id'], info: Partial<RobotRealtimeInfo>): void {
    const pen = this.getPenById(id);
    const { rotate: or, robot } = pen ?? {};
    if (!robot?.type) return;

    // 使用改进的坐标转换
    const coords = this.normalizeCoordinates(info);

    // 无效坐标处理
    if (!coords) {
      this.setValue({ id, visible: false }, { render: true, history: false, doEvent: false });
      return;
    }

    const { x, y } = coords;
    const { active, angle, path: points } = info;
    const rotate = angle ?? or;

    // 路径处理优化
    let path: Point[] | undefined;
    if (points && Array.isArray(points)) {
      // 新路径：相对于机器人中心的坐标
      path = points.map((p) => ({
        x: (p.x || 0) - (info.x || 37),
        y: (p.y || 0) - (info.y || 37),
      }));
    } else if (robot.path) {
      // 保持原有路径，但需要调整坐标
      const { x: ox, y: oy } = this.getPenRect(pen!);
      path = robot.path.map((p) => ({
        x: p.x + ox - x,
        y: p.y + oy - y,
      }));
    }

    const robotState = { ...robot, ...omitBy({ active, path }, isNil) };

    // 根据active状态决定渲染方式
    if (typeof active === 'boolean') {
      // 有明确的活跃状态，更新图标
      this.setValue(
        {
          id,
          ...this.#mapRobotImage(robot.type, active),
          x,
          y,
          rotate,
          robot: robotState,
          visible: true,
        },
        { render: true, history: false, doEvent: false },
      );
    } else {
      // 无活跃状态信息，只更新位置
      this.setValue(
        { id, x, y, rotate, robot: robotState, visible: true },
        { render: true, history: false, doEvent: false },
      );
    }
  }
}
```

## 6. 性能优化建议

### 6.1 渲染优化

- 使用`requestAnimationFrame`批量处理渲染更新
- 实现视口裁剪，只渲染可见区域的机器人
- 添加机器人状态变化的diff检测，避免无效渲染

### 6.2 内存管理

- 定期清理过期的机器人状态缓存
- 使用WeakMap存储临时状态，避免内存泄漏
- 在组件卸载时正确清理WebSocket连接和事件监听器

### 6.3 网络优化

- 实现WebSocket连接池，复用连接
- 添加消息压缩，减少网络传输量
- 使用心跳机制检测连接状态

## 7. 总结

机器人运动监控组件的多页面位置不一致问题主要源于：

1. **架构设计缺陷**: 缺乏全局状态管理，每个页面独立维护状态
2. **WebSocket连接独立性**: 多个连接可能接收到不同时间点的数据
3. **初始化时序问题**: 不同页面的初始化时间不同，导致状态基线不一致
4. **坐标转换逻辑**: 默认值处理和坐标转换在边界情况下存在问题
5. **状态验证不足**: 缺乏对接收数据的有效性验证

通过实施全局状态管理、WebSocket连接复用、状态缓存机制和坐标转换优化等解决方案，可以有效解决这些问题，确保多页面间机器人位置的一致性。
