diff --git a/docs/superpowers/specs/2026-08-27-mapbox-migration-design.md b/docs/superpowers/specs/2026-08-27-mapbox-migration-design.md new file mode 100644 index 0000000..df94376 --- /dev/null +++ b/docs/superpowers/specs/2026-08-27-mapbox-migration-design.md @@ -0,0 +1,174 @@ +# Mapbox 全量替换 maptalks + +日期:2026-08-27 +范围:`MapLayer` / `mapHelper` / `config/map` + Monitor / Tasks / Replay +状态:待用户确认 + +## 1. 背景与目标 + +现状运行时仍是 `maptalks@1.12.1` + 天地图 WMTS。共享层 `MapLayer` 单例经 `commonRefs('map')` + `mapHelper` 暴露;`MonitorView` / `TasksView` / `ReplayView` 直接使用 maptalks Marker / VectorLayer / LineString / setBaseLayer。 + +目标: + +1. 引擎从 **maptalks 全量切到 mapbox-gl** +2. 底图用 **Mapbox 官方样式**:卫星 / 街道;环境变量 `APP_MAPBOX_TOKEN` +3. 保留常驻 `MapLayer` + `commonRefs('map')` + `mapHelper` **薄封装** +4. Monitor / Tasks / Replay **全部**改 Mapbox API;删除 `maptalks` 依赖 +5. Tasks 航线编辑继续 `replaceMapContainer` / `restoreMapContainer` 搬迁共享地图容器 +6. 设备点、航点、航迹、绑定线统一用 **GeoJSON Source + Layer** + +非目标: + +- 不做统一 `MapService` 大门面 +- 不保留天地图 / 双 provider +- 不改球形总览 Three.js(若已有或后续独立做) +- 本轮不重做机巢/无人机 pin 图标视觉(可先 `circle` / 基础 `symbol`,后续再接 pin) + +## 2. 方案选型 + +| 方案 | 结论 | +|------|------| +| A. 薄封装替换 MapLayer/mapHelper,页面直用 mapbox API | **采用** | +| B. 统一 MapService 门面 | 本轮不做,抽象成本高 | +| C. 每页独立地图实例 | 与常驻地图 + Tasks 搬迁语义冲突 | + +底图:Mapbox 官方 `satellite-streets` / `streets`,不再使用天地图 WMTS。 +渲染:GeoJSON Source + Layer(非 DOM Marker 为主路径)。 + +## 3. 共享层 + +### 3.1 `src/config/map.js` + +- `token`:`import.meta.env.APP_MAPBOX_TOKEN || import.meta.env.VITE_MAPBOX_TOKEN || ''` +- 保留 `center` / `zoom`(现有 `APP_MAP_CENTER_*` / `APP_MAP_ZOOM`) +- 导出: + +```js +export const MAP_STYLES = { + satellite: 'mapbox://styles/mapbox/satellite-streets-v12', + street: 'mapbox://styles/mapbox/streets-v12', +} +``` + +- 删除 `createTiandituLayerOptions` / `createTiandituBaseLayer` + +### 3.2 `src/layout/MapLayer.vue` + +- 引入 `mapbox-gl` 与 CSS +- `mapboxgl.accessToken = mapConfig.token` +- 缺 token:打错误日志,`commonRefs.setRef('map', null)`,不抛崩 +- 有 token: + +```js +map = new mapboxgl.Map({ + container: 'map', + style: MAP_STYLES.satellite, + center: mapConfig.center, // [lng, lat] + zoom: mapConfig.zoom, + minZoom: 3, + attributionControl: false, +}) +``` + +- `commonRefs.setRef('map', map)` + `mapHelper.setMap(map)` +- `onUnmounted`:`map.remove()`,清 ref / helper + +### 3.3 `src/core/mapHelper.js` + +保留对外方法,内部改 Mapbox: + +| 方法 | 行为 | +|------|------| +| `setMap` / `getMap` | 持有 `mapboxgl.Map` | +| `toggleMapMode({ is2D, is3D })` | 2D:`setPitch(0)` `setBearing(0)`,关闭 dragRotate/pitch;3D:pitch≈45,开启旋转/倾角 | +| `replaceMapContainer(newParent)` | 保存 view → 把 `#map` DOM 挪到 `newParent` → `map.resize()` | +| `restoreMapContainer()` | 移回原父节点 → 恢复 center/zoom/pitch/bearing → `map.resize()` | + +去掉 maptalks `checkSize`;一律 `resize()`。 + +### 3.4 依赖与 env + +- `package.json`:增加 `mapbox-gl`;删除 `maptalks` +- `.env.example` / `.env.development` / `.env.production`: + - 增加 `APP_MAPBOX_TOKEN=` + - 删除或降级 `APP_TIANDITU_TOKEN` 说明(本轮不再使用) + - 保留中心/缩放变量 +- CSS:去掉 `.maptalks-wrapper` 特例;Tasks 宿主适配 `#map` / `.mapboxgl-map` + +## 4. 页面层 + +坐标约定:统一 `[lng, lat]`。 +`map.setStyle(...)` 会清掉自定义 source/layer —— **切换样式后必须在 `style.load` 重建业务层并回填数据**。 + +### 4.1 MonitorView + +| 能力 | Mapbox 落地 | +|------|-------------| +| 设备点 | source `monitor-devices` + `circle`(或基础 `symbol`)+ 可选 `symbol` 文本层 | +| 选中态 | `feature-state` 或按 selectedId 重写 properties / paint | +| 绑定虚线 | source `monitor-binding` + `line`(`line-dasharray`);仅「选中机巢 + 其飞行中无人机」或「选中飞行无人机 + 所属机巢」 | +| 卫星/地图 | `map.setStyle(MAP_STYLES[mode])`;`style.load` 后重建 devices/binding 层 | +| fitAll | `fitBounds`;清选中回 overview | +| 选中飞向 | `flyTo` / `easeTo` | +| 比例尺 | 保留现有 zoom→米估算,或挂 `ScaleControl`(二选一,UI 位置对齐现工具条) | +| 点击选中 | `queryRenderedFeatures` 命中设备层 | + +离开 Monitor:移除本页 source/layer 与事件,避免污染 Tasks/Replay。 + +### 4.2 TasksView + +- 进入编辑:`mapHelper.replaceMapContainer(host)` → `resize()` → `toggleMapMode({ is2D: true })` +- 航线:source `task-route`(LineString)+ `task-waypoints`(Point features,可带序号属性) +- 地图点击加点:`map.on('click', ...)` → `e.lngLat` +- 离开:清业务层 → `restoreMapContainer()` + +### 4.3 ReplayView + +- source `replay-traj` line + 起/终/当前点(可同一 points source 不同 layer filter) +- `fitBounds` 包住轨迹 +- 离开清层 + +## 5. 数据流与生命周期 + +```mermaid +flowchart LR + MainContainer --> MapLayer + MapLayer -->|"commonRefs + mapHelper"| Pages + Pages --> Monitor + Pages --> Tasks + Pages --> Replay + Tasks -->|"replace/restore DOM"| MapLayer +``` + +1. 登录后壳层挂载 → `MapLayer` 初始化 Mapbox → 写入 ref +2. 业务页 `await commonRefs.getRef('map')` 拿到实例后加自己的 source/layer +3. 路由离开必须卸本页图层与监听;Tasks 额外 restore 容器 +4. 无 token:地图不初始化,业务页需容忍 `map == null`(空态/提示,不白屏) + +## 6. 风险与处理 + +| 风险 | 处理 | +|------|------| +| `setStyle` 清空业务层 | 统一 `style.load` 重建 + 数据回填函数 | +| Tasks 搬迁后尺寸为 0 | replace/restore 后立刻 `resize()`;必要时 `requestAnimationFrame` 再 resize | +| token 无效/缺省 | 跳过 init + 日志;页面降级 | +| 旧文档按 maptalks/天地图写 | 本 spec 为准;相关旧计划标注过时,不在本轮改文 | +| 包体变大 | 可接受;不引入 `maptalks.mapboxgl` 双引擎 | + +## 7. 验收 + +- [ ] 配置有效 `APP_MAPBOX_TOKEN` 后登录可见 Mapbox 卫星底图 +- [ ] 缺 token 不白屏崩,有明确控制台错误 +- [ ] Monitor:卫星↔街道切换后点/线仍在;选中飞向;fitAll;绑定虚线条件正确;比例尺可见 +- [ ] Tasks:编辑器内地图可点加点、航线/航点绘制;离开恢复主壳地图位置 +- [ ] Replay:航迹线 + fitBounds +- [ ] `package.json` 无 `maptalks`;源码无 `import ... maptalks` +- [ ] `1920×1080` 冒烟:Monitor 主路径可用 + +## 8. 实现顺序(计划阶段细化) + +1. 依赖与 env / `config/map` / `MapLayer` / `mapHelper` +2. Monitor 点线与底图切换 / fit / 绑定线 +3. Replay 航迹 +4. Tasks 搬迁 + 航线编辑交互 +5. 删 maptalks 残留 CSS/引用;冒烟验收