# 带看集成指南

本文档面向**接入众趣 zqsdk 的开发者**，说明如何在自己的项目中实现「带看」功能。带看即两个用户（发起方与接收方）通过 IM 实时同步 VR 视角与操作，实现远程协同看房体验。你的项目需要：通过 URL 参数区分角色、集成 IM 通道、注册 SDK 带看事件、处理消息协议，并实现带看入口、场景列表与操作权限蒙层等 UI。

---

## 一、功能简介与前置条件

### 1.1 什么是带看

带看是基于 zqsdk 的**实时协同看房**能力：两个用户各自打开同一模型的 VR 页面，通过 IM（即时通讯）通道实时同步相机视角、场景切换、标尺操作等，实现「一方操作，另一方同步跟随」的远程带看体验。

核心能力包括：

- **视角实时同步**：一方移动/旋转相机，另一方自动跟随到相同视角。
- **场景切换同步**：一方切换到某个场景（如客厅、主卧），另一方同步跳转。
- **标尺操作同步**：一方开启/关闭全景标尺，另一方同步显隐。
- **操作权限管理**：SDK 自动管理操作权限（本地/远端/自由），当远端正在操作时，本地显示「对方正在操作」蒙层。

### 1.2 角色说明

带看涉及两个角色，通过 URL 参数区分：

| 角色 | URL 参数 | 启动方式 | 说明 |
|------|----------|----------|------|
| **发起方** | `showtakelook=on` | 正常启动 `housePlay.start()` | 页面上显示「带看」按钮，用户点击后建立 IM 连接 |
| **接收方** | `takelook=on` | 引导模式 `housePlay.start('guid')` | 页面加载后自动初始化 IM，等待发起方同步视角 |

### 1.3 前置条件

- 已接入众趣 zqsdk，并在 `zqsdk.init` 的 `complete` 回调中拿到 `window.housePlay`（参见 [VR 初始化展示指南](./VR初始化展示指南.md)）。
- 已准备好 IM 服务：本仓库默认使用**网易云信 NIM SDK**，你也可以替换为腾讯云 TIM 或其他 IM 服务（需实现适配器接口）。
- IM 相关凭证（`appid`、`userid`、`sig`、`funcuserid`）由你的业务后端生成，通过 URL 参数传入前端页面。

### 1.4 示例截图
![带看同步展示效果](../../screenshot/takelook/带看效果.png)

---

## 二、整体架构与消息协议

### 2.1 架构概览

```
┌─────────────────────────────────────────────────────────┐
│                    发起方页面                              │
│  ┌──────────┐   ┌──────────┐   ┌───────────────────┐    │
│  │ 带看按钮  │──▶│ TakeLook │──▶│  IM 适配器 (NIM)   │───┐│
│  └──────────┘   │  Store    │   └───────────────────┘   ││
│                 │          │◀── housePlay 带看事件       ││
│                 └──────────┘                             ││
└─────────────────────────────────────────────────────────┘│
                                                           │
                        IM 通道（网易云信 / 腾讯云等）        │
                                                           │
┌─────────────────────────────────────────────────────────┐│
│                    接收方页面                              ││
│  ┌──────────────┐   ┌──────────┐   ┌─────────────────┐  ││
│  │ 操作权限蒙层  │◀──│ TakeLook │◀──│ IM 适配器 (NIM)  │◀─┘│
│  │ 场景列表面板  │   │  Store    │   └─────────────────┘   │
│  └──────────────┘   │          │──▶ housePlay 同步方法    │
│                     └──────────┘                          │
└─────────────────────────────────────────────────────────┘
```

### 2.2 消息协议

带看通过 IM 传输的消息遵循统一的 `TakeLookMessage` 协议，分为**状态类**和**操作类**两种：

#### 状态类消息（连接初始化）

用于双方建立连接并完成初始视角同步：

```typescript
interface TakeLookStateMessage {
  type: 'state'
  data: {
    type: 'connectState'
    state: 'initdone' | 'initstate' | 'initstatedone'
    data?: unknown  // initstate 时携带当前 3D 坐标与角度
  }
}
```

#### 操作类消息 — 核心操作（相机同步）

用于实时同步相机移动、旋转等操作：

```typescript
interface TakeLookCoreOperation {
  type: 'operation'
  data: {
    type: 'CORE_OPERATION'
    data: unknown  // SDK uploadLocalActions 事件的原始数据
  }
}
```

#### 操作类消息 — UI 操作（场景切换、标尺等）

用于同步场景切换、标尺显隐等 UI 级操作：

```typescript
interface TakeLookUIOperation {
  type: 'operation'
  data: {
    type: 'UI_OPERATION'
    data: {
      type: 'UI'
      target: 'switchScene' | 'ruler' | string
      action: unknown  // switchScene 时为场景对象，ruler 时为 boolean
    }
  }
}
```

### 2.3 连接初始化握手流程

双方通过三次状态消息完成初始视角同步：

```
发起方                                    接收方
  │                                         │
  │  SDK 资源加载完成                         │  SDK 资源加载完成
  │  (LocalProgramLoadingCompleted)          │  (LocalProgramLoadingCompleted)
  │                                         │
  │  IM 连接成功后补发                        │
  │──── initdone ──────────────────────────▶│
  │                                         │  收到 initdone
  │                                         │  获取当前 3D 坐标
  │◀──── initstate (携带坐标数据) ───────────│
  │                                         │
  │  收到 initstate                          │
  │  执行 firstSyncRemoteActions             │
  │  (同步到对方视角)                         │
  │                                         │
  │  firstSyncActionsCompleted 触发          │
  │  setConnectionStatus(true)               │
  │──── initstatedone ─────────────────────▶│
  │                                         │  收到 initstatedone
  │                                         │  setConnectionStatus(true)
  │                                         │
  │◀═══════ 实时视角同步开始 ═══════════════▶│
```

---

## 三、集成步骤概览

| 步骤 | 内容 |
|------|------|
| 1 | 准备 URL 参数，区分发起方（`showtakelook=on`）与接收方（`takelook=on`） |
| 2 | 集成 IM 通道（NIM / TIM 或自定义适配器） |
| 3 | 在 SDK `complete` 回调中判断带看模式，注册带看事件并按角色启动 |
| 4 | 实现消息收发与协议处理逻辑 |
| 5 | 实现带看 UI：入口按钮、场景列表面板、操作权限蒙层 |
| 6 | （可选）实现 iframe 嵌入的双端演示页面 |

以下按步骤展开。

---

## 四、第一步：准备 URL 参数

带看功能通过 URL 参数传递角色信息和 IM 凭证。你的业务后端需要为双方各生成一组参数。

### 4.1 参数列表

| 参数 | 必填 | 说明 |
|------|------|------|
| `m` | 是 | 模型 ID |
| `showtakelook` | 发起方必填 | 设为 `on` 表示显示带看入口按钮 |
| `takelook` | 接收方必填 | 设为 `on` 表示以带看接收方模式启动 |
| `appid` | 是 | IM 服务的 AppKey / AppID |
| `userid` | 是 | 当前用户的 IM 账号 |
| `sig` | 是 | 当前用户的 IM 鉴权签名 / Token |
| `funcuserid` | 是 | 对方用户的 IM 账号（消息发送目标） |

### 4.2 URL 示例

**发起方**（经纪人端）：

```
https://your-domain.com/vr?m=模型ID&showtakelook=on&appid=YOUR_APP_ID&userid=agent001&sig=TOKEN_A&funcuserid=customer001
```

**接收方**（客户端）：

```
https://your-domain.com/vr?m=模型ID&takelook=on&appid=YOUR_APP_ID&userid=customer001&sig=TOKEN_B&funcuserid=agent001
```

注意 `userid` 和 `funcuserid` 是**互换**的：发起方的 `funcuserid` 是接收方的 `userid`，反之亦然。

---

## 五、第二步：集成 IM 通道

带看需要一个 IM 通道来传输消息。本仓库提供了**适配器模式**，默认实现了网易云信 NIM 适配器，并预留了腾讯云 TIM 适配器接口。

### 5.1 IM 适配器接口

无论使用哪种 IM 服务，都需要实现以下接口：

```typescript
interface IIMAdapter {
  /** 初始化并连接 */
  init(options: { appId: string; userId: string; userSig: string }): Promise<void>
  /** 发送消息给指定用户 */
  sendMessage(toUserId: string, data: TakeLookMessage): void
  /** 断开连接并清理 */
  destroy?(): void
  /** 监听事件（至少支持 'message' 事件） */
  on?(event: string, callback: (data: unknown) => void): void
}
```

收到消息时，适配器需要解析 IM 原始消息并 emit 一个 `message` 事件，数据格式为：

```typescript
interface IMReceivedMessage {
  from: string              // 发送方 userId
  to?: string               // 接收方 userId
  content: TakeLookMessage  // 解析后的带看业务消息
}
```

### 5.2 使用网易云信 NIM（默认）

安装依赖：

```bash
npm install netease-nim-web-sdk
```

NIM 适配器的核心逻辑：

1. **初始化**：调用 `NIM.getInstance` 传入 `appKey`（即 appId）、`account`（即 userId）、`token`（即 sig），在 `onconnect` 回调中 resolve。
2. **接收消息**：在 `oncustomsysmsg` 回调中解析消息内容。NIM 的自定义系统消息 `content` 为 JSON 字符串，格式为 `{ type: 'VRContent', VRContent: '...' }`，需要两次 JSON.parse 提取出 `TakeLookMessage`。
3. **发送消息**：调用 `imInstance.sendCustomSysMsg`，将 `TakeLookMessage` 序列化后包装为 `{ type: 'VRContent', VRContent: JSON.stringify(data) }` 发送。

```javascript
// 接收消息的解析逻辑
function parseNIMMessage(msg) {
  let content = JSON.parse(msg.content)  // { type: 'VRContent', VRContent: '...' }
  let business

  if (content.type === 'VRContent' && content.VRContent != null) {
    business = JSON.parse(content.VRContent)  // TakeLookMessage
  } else {
    business = content
  }

  return { from: msg.from, content: business }
}

// 发送消息的封装逻辑
function sendNIMMessage(imInstance, toUserId, data) {
  let msg = { VRContent: JSON.stringify(data), type: 'VRContent' }

  imInstance.sendCustomSysMsg({
    scene: 'p2p',
    to: toUserId,
    isPushable: false,
    needPushNick: false,
    content: JSON.stringify(msg),
    sendToOnlineUsersOnly: false,
    apnsText: JSON.stringify(msg),
    done: function(error) {
      if (error) console.warn('NIM send failed', error)
    }
  })
}
```

### 5.3 替换为其他 IM 服务

若需使用腾讯云 TIM 或其他 IM，只需实现 `IIMAdapter` 接口并替换适配器创建函数即可。适配器内部负责：

1. 连接 IM 服务
2. 将收到的原始消息解析为 `IMReceivedMessage` 格式并 emit `message` 事件
3. 将 `TakeLookMessage` 序列化后通过 IM 发送

---

## 六、第三步：在 SDK complete 中判断带看模式并启动

在 `zqsdk.init` 的 `complete` 回调中，根据 URL 参数判断是否为带看模式，并按角色执行不同的启动逻辑。

```javascript
complete: function() {
  let housePlay = window.housePlay
  if (!housePlay) return

  // 先注册基础事件（加载进度、UI 就绪等）
  registerBaseEvents(housePlay)

  let isTakeLookParam = getUrlParam('takelook') === 'on'      // 接收方
  let isShowTakeLookParam = getUrlParam('showtakelook') === 'on' // 发起方
  let isTakeLookMode = isTakeLookParam || isShowTakeLookParam

  if (isTakeLookMode) {
    // 注册带看事件（见第四步）
    initTakeLookEvents(housePlay)

    if (isTakeLookParam) {
      // ── 接收方 ──
      // 以 guid 模式启动，等待发起方同步视角
      housePlay.start('guid')

      // 自动初始化 IM
      let appid = getUrlParam('appid')
      let userid = getUrlParam('userid')
      let sig = getUrlParam('sig')
      let funcuserid = getUrlParam('funcuserid')

      if (appid && userid && sig && funcuserid) {
        initIM({ appid, userid, sig, funcuserid })
      }
    } else {
      // ── 发起方 ──
      // 正常启动，用户点击「带看」按钮后再建立 IM
      housePlay.start()
    }
  } else {
    // 非带看模式：正常启动
    housePlay.start()
  }
}
```

**关键区别**：
- **接收方**使用 `housePlay.start('guid')` 以引导模式启动，SDK 不会自动开始漫游，而是等待远端同步视角。
- **发起方**使用 `housePlay.start()` 正常启动，用户可自由操作，点击带看按钮后才建立 IM 连接。

---

## 七、第四步：注册带看事件与消息处理

### 7.1 注册 housePlay 带看事件

在 housePlay 可用后注册以下带看专用事件：

```javascript
function initTakeLookEvents(housePlay) {
  // 1. SDK 资源加载完成 → 发送 initdone 开始握手
  housePlay.on('LocalProgramLoadingCompleted', function() {
    sendMessage({
      type: 'state',
      data: { state: 'initdone', type: 'connectState' }
    })
  })

  // 2. 本地操作上传 → 发送 CORE_OPERATION 同步相机
  housePlay.on('uploadLocalActions', function(data) {
    sendMessage({
      type: 'operation',
      data: { type: 'CORE_OPERATION', data: data }
    })
  })

  // 3. 首次同步完成 → 设置连接状态并通知对方
  housePlay.on('firstSyncActionsCompleted', function() {
    housePlay.setConnectionStatus(true)
    sendMessage({
      type: 'state',
      data: { state: 'initstatedone', type: 'connectState' }
    })
  })

  // 4. 操作权限变化 → 控制「对方正在操作」蒙层
  housePlay.on('validOperatorChanged', function(operator) {
    if (operator === 'remote') {
      yourApp.showRemoteOperatingOverlay()
    } else {
      yourApp.hideRemoteOperatingOverlay()
    }
  })
}
```

### 7.2 处理收到的消息

IM 适配器收到消息后，按消息类型分发处理：

```javascript
function handleReceiveMessage(msg) {
  let housePlay = window.housePlay
  if (!housePlay) return

  let content = msg.content

  // ── 操作类消息 ──
  if (content.type === 'operation') {
    // 设置操作权限为远程正在操作
    housePlay.setValidOperator('remote')

    if (content.data.type === 'CORE_OPERATION') {
      // 核心操作：同步相机视角
      housePlay.syncRemoteActions(content.data.data)
    } else if (content.data.type === 'UI_OPERATION') {
      let target = content.data.data.target
      let action = content.data.data.action

      if (target === 'switchScene') {
        // 场景切换
        housePlay.warpToScene(action)
      } else if (target === 'ruler') {
        // 标尺显隐
        yourApp.setRulerVisible(!!action)
      }
    }
    return
  }

  // ── 状态类消息（连接初始化握手） ──
  if (content.type === 'state' && content.data.type === 'connectState') {
    let state = content.data.state

    if (state === 'initdone') {
      // 对方资源加载完成，回复当前视角状态
      let currentState = housePlay.getCurrentState()
      sendMessage({
        type: 'state',
        data: { type: 'connectState', state: 'initstate', data: currentState }
      })
    } else if (state === 'initstate' && content.data.data != null) {
      // 收到对方视角，执行首次同步
      housePlay.firstSyncRemoteActions(content.data.data)
    } else if (state === 'initstatedone') {
      // 握手完成，开始实时同步
      housePlay.setConnectionStatus(true)
    }
  }
}
```

### 7.3 补发 initdone 的时机

发起方通常在用户点击「带看」按钮后才建立 IM 连接，此时 SDK 的 `LocalProgramLoadingCompleted` 事件可能已经触发过了。因此 IM 连接成功后需要**补发 initdone**，否则双方无法完成握手：

```javascript
function initIM(options) {
  // ... IM 初始化 ...

  adapter.init({ appId, userId, userSig }).then(function() {
    // IM 连接成功

    // 如果 SDK 资源已加载完成，补发 initdone
    if (localProgramLoaded) {
      sendMessage({
        type: 'state',
        data: { state: 'initdone', type: 'connectState' }
      })
    }
  })
}
```

### 7.4 场景切换与标尺同步

在带看模式下，场景切换和标尺操作需要同时执行本地操作并发送 IM 消息给对方：

```javascript
// 场景切换：本地跳转 + 发送消息
function switchScene(scene) {
  sendMessage({
    type: 'operation',
    data: {
      type: 'UI_OPERATION',
      data: { type: 'UI', target: 'switchScene', action: scene }
    }
  })
  housePlay.gotoScenebyId(scene.index)
}

// 标尺同步：仅在带看已连接时发送
function rulerOperation(open) {
  if (!isTakeLookConnected) return

  sendMessage({
    type: 'operation',
    data: {
      type: 'UI_OPERATION',
      data: { type: 'UI', target: 'ruler', action: open }
    }
  })
}
```

---

## 八、第五步：实现带看 UI

UI 完全由你自定义，建议具备以下三个组件：

### 8.1 带看入口按钮

**显示条件**：SDK 已就绪 + URL 含 `showtakelook=on` + 尚未建立带看连接 + 非讲房模式 + 非换装模式。

**点击行为**：
1. 从 URL 参数中获取 `appid`、`userid`、`sig`、`funcuserid`
2. 调用 IM 初始化建立连接
3. （可选）通过 `postMessage` 通知父页面，由父页面为接收方生成 URL 并打开

```javascript
function handleTakeLookBtnClick() {
  let appid = getUrlParam('appid')
  let userid = getUrlParam('userid')
  let sig = getUrlParam('sig')
  let funcuserid = getUrlParam('funcuserid')

  initIM({ appid, userid, sig, funcuserid }).then(function() {
    // 可选：通知父页面（iframe 嵌入场景）
    window.top.postMessage({
      msg: 'MessageFromIframePage',
      data: {
        appid: appid,
        userid: funcuserid,    // 对方的 userid
        sig: sig,
        funcuserid: userid     // 当前用户作为对方的 funcuserid
      }
    }, '*')
  })
}
```

### 8.2 场景列表面板

**显示条件**：带看已连接 + 场景列表不为空。

**数据来源**：从 `housePlay.model` 中提取场景列表。优先读取 `settings.nestscenes.scenes`，若不存在则回退到 `model.heroLocations`。过滤掉「总述」和脚本项。

```javascript
function getScenesFromHousePlay(housePlay) {
  // 优先使用 nestscenes
  let scenes = housePlay.settings?.nestscenes?.scenes
  if (scenes && scenes.length > 0) {
    return scenes
      .filter(function(s) { return s.name && s.name !== '总述' && s.script !== 1 })
      .map(function(s, i) { return { name: s.name, index: i, id: s.id } })
  }

  // 回退到 heroLocations
  let heroLocations = housePlay.model?.heroLocations
  if (heroLocations && heroLocations.length > 0) {
    return heroLocations
      .filter(function(s) { return s?.name && s.name !== '总述' && s.script !== 1 })
      .map(function(s, i) { return { name: s.name, index: i } })
  }

  return []
}
```

**交互**：点击场景卡片时调用 `switchScene(scene)` 同时执行本地跳转和远端同步。

### 8.3 操作权限蒙层

**显示条件**：当 SDK 触发 `validOperatorChanged` 且 operator 为 `'remote'` 时显示全屏蒙层，提示「对方正在操作」；当 operator 变为 `'local'` 或 `'free'` 时隐藏。

蒙层应覆盖整个 VR 区域，设置 `pointer-events: auto` 和 `cursor: not-allowed`，阻止本地用户在远端操作期间进行交互。

---

## 九、第六步（可选）：iframe 双端演示

在开发调试阶段，可以使用 iframe 在同一页面中同时嵌入发起方和接收方，模拟双端带看效果。

```html
<!DOCTYPE html>
<html>
<head>
  <title>带看演示</title>
  <style>
    #app { display: flex; height: 80vh; padding: 40px; }
    #app > iframe { margin: 30px 100px; width: 1000px; }
  </style>
</head>
<body>
  <div id="app">
    <!-- 发起方 -->
    <iframe src="http://localhost:8080?m=模型ID&showtakelook=on&appid=APP_ID&userid=user_A&sig=TOKEN_A&funcuserid=user_B"></iframe>
    <!-- 接收方（初始为空，由 postMessage 动态设置） -->
    <iframe id="iframe2" src=""></iframe>
  </div>
  <script>
    window.addEventListener('message', function(event) {
      if (event.data.msg === 'MessageFromIframePage') {
        var data = event.data.data
        var frame = document.querySelector('#iframe2')
        frame.contentWindow.location.href =
          'http://localhost:8080?m=模型ID&takelook=on' +
          '&appid=' + data.appid +
          '&userid=' + data.userid +
          '&sig=' + data.sig +
          '&funcuserid=' + data.funcuserid
      }
    })
  </script>
</body>
</html>
```

流程：
1. 左侧 iframe 加载发起方页面，用户点击「带看」按钮
2. 发起方通过 `postMessage` 将对方的 IM 凭证发送给父页面
3. 父页面监听 `message` 事件，为右侧 iframe 拼接接收方 URL 并加载
4. 双方 IM 连接成功后自动完成握手，开始实时同步

---

## 十、API 速查表

### 10.1 URL 参数

| 参数 | 值 | 说明 |
|------|-----|------|
| `showtakelook` | `on` | 发起方：显示带看入口按钮，正常启动 |
| `takelook` | `on` | 接收方：以 guid 模式启动，自动初始化 IM |
| `appid` | 字符串 | IM 服务的 AppKey |
| `userid` | 字符串 | 当前用户的 IM 账号 |
| `sig` | 字符串 | 当前用户的 IM 鉴权 Token |
| `funcuserid` | 字符串 | 对方用户的 IM 账号 |

### 10.2 housePlay 方法（带看相关）

| 方法 | 说明 |
|------|------|
| `housePlay.start('guid')` | 以引导模式启动（接收方使用），等待远端同步 |
| `housePlay.getCurrentState()` | 获取当前 3D 坐标与角度（用于初始同步） |
| `housePlay.firstSyncRemoteActions(state)` | 首次同步远端视角（收到 initstate 时调用） |
| `housePlay.syncRemoteActions(actions)` | 实时同步远端操作（收到 CORE_OPERATION 时调用） |
| `housePlay.setConnectionStatus(true)` | 设置带看连接状态为已连接 |
| `housePlay.setValidOperator(operator)` | 设置操作权限：`'local'` / `'remote'` / `'free'` |
| `housePlay.warpToScene(scene)` | 跳转到指定场景（收到 switchScene 时调用） |
| `housePlay.gotoScenebyId(index)` | 按索引跳转场景（本地主动切换时调用） |

### 10.3 housePlay 事件（带看相关）

| 事件 | 回调参数 | 说明 |
|------|----------|------|
| `LocalProgramLoadingCompleted` | 无 | SDK 资源加载完成，应发送 initdone 开始握手 |
| `uploadLocalActions` | `data` | 本地相机操作数据，需通过 IM 发送给对方 |
| `firstSyncActionsCompleted` | 无 | 首次视角同步完成，应设置连接状态并通知对方 |
| `validOperatorChanged` | `operator` | 操作权限变化：`'remote'` 时显示蒙层，其他时隐藏 |

### 10.4 消息类型速查

| 消息类型 | data.type | 用途 |
|----------|-----------|------|
| `state` | `connectState` | 连接初始化握手（initdone → initstate → initstatedone） |
| `operation` | `CORE_OPERATION` | 实时相机视角同步 |
| `operation` | `UI_OPERATION` | UI 操作同步（场景切换、标尺等） |

---

## 十一、常见问题与注意事项

1. **IM 连接时机与 initdone 补发**  
   发起方在用户点击按钮后才建立 IM，此时 `LocalProgramLoadingCompleted` 可能已触发。IM 连接成功后必须检查并补发 `initdone`，否则握手无法完成。

2. **start('guid') 与 start() 的区别**  
   接收方必须使用 `start('guid')` 以引导模式启动，SDK 不会自动漫游而是等待远端同步视角。发起方使用 `start()` 正常启动。若接收方误用 `start()`，双方视角会不一致。

3. **userid 与 funcuserid 互换**  
   发起方的 `funcuserid` 就是接收方的 `userid`，反之亦然。业务后端生成 URL 时需注意正确配对。

4. **操作权限管理**  
   SDK 通过 `validOperatorChanged` 事件自动管理操作权限。当远端正在操作时，本地应显示蒙层阻止交互。不要手动调用 `setValidOperator` 来覆盖 SDK 的自动管理，除非在收到远端操作消息时需要将权限设为 `'remote'`。

5. **标尺同步**  
   在带看模式下切换标尺时，除了调用 SDK 的 `enablePanoramaSize` / `disablePanoramaSize` 控制本地显示外，还需通过 `rulerOperation` 发送 UI_OPERATION 消息同步给对方。

6. **场景列表数据源**  
   场景列表优先从 `housePlay.settings.nestscenes.scenes` 获取（新版数据），若不存在则回退到 `housePlay.model.heroLocations`（旧版数据）。两者都需过滤掉 `name === '总述'` 和 `script === 1` 的项。

7. **NIM 消息格式**  
   网易云信 NIM 使用自定义系统消息（`sendCustomSysMsg`），消息内容需要包装为 `{ type: 'VRContent', VRContent: JSON.stringify(业务数据) }` 格式。接收时需要两次 JSON.parse。若替换为其他 IM，消息封装格式可自行定义，只需保证解析后为 `TakeLookMessage` 即可。

8. **postMessage 安全性**  
   iframe 嵌入场景中使用 `postMessage` 通知父页面时，生产环境建议将 `'*'` 替换为具体的 origin，避免安全风险。

---

## 十二、本仓库实现参考

若需参考本仓库的具体实现，可查阅以下文件：

| 文件 | 说明 |
|------|------|
| `src/types/takelook.ts` | 带看消息协议类型定义 |
| `src/stores/takelook.ts` | 带看 Store：状态管理、事件注册、IM 初始化、消息收发 |
| `src/plugins/im/types.ts` | IM 适配器接口定义 |
| `src/plugins/im/BaseIM.ts` | IM 适配器基类（基于 EventEmitter） |
| `src/plugins/im/NIM/nim-adapter.ts` | 网易云信 NIM 适配器实现 |
| `src/plugins/im/TIM/tim-adapter.ts` | 腾讯云 TIM 适配器（预留，暂未实现） |
| `src/plugins/im/index.ts` | IM 插件入口，导出适配器创建函数 |
| `src/modules/pc/takelook/NestTakelook.vue` | 带看模块根组件，编排按钮、蒙层、面板 |
| `src/modules/pc/takelook/NestTakeLookBtn.vue` | 带看入口按钮组件 |
| `src/modules/pc/takelook/NestTakeLookPanel.vue` | 带看场景列表面板组件（支持拖拽滚动） |
| `src/modules/pc/takelook/NestTakeLookOverlay.vue` | 「对方正在操作」蒙层组件 |
| `src/stores/vr.ts` | VR Store 中带看模式的判断与启动逻辑 |
| `src/stores/toolbar.ts` | 工具栏 Store 中标尺同步带看的逻辑 |
| `src/stores/url-params.ts` | URL 参数解析（含带看相关参数） |
| `takelook-demo.html` | iframe 双端演示页面 |
