1 changed files with 563 additions and 0 deletions
@ -0,0 +1,563 @@ |
|||||
|
# Monitor Globe + Markers Implementation Plan |
||||
|
|
||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. |
||||
|
|
||||
|
**Goal:** `/monitor` 默认地球全景(中国居中),地图点位改为立体水滴机巢/无人机图标。 |
||||
|
|
||||
|
**Architecture:** `MapLayer` 开启 `projection: 'globe'` 并以 overview 相机初始化;Monitor 用 Mapbox `symbol` 加载按状态区分的 pin 图;进页/清选中/「查看全部」走 `resetOverviewCamera()`,不再 `fitBounds` 设备包围盒。 |
||||
|
|
||||
|
**Tech Stack:** Vue 3、Mapbox GL、`public/assets/icons/*-pin-*.svg` |
||||
|
|
||||
|
**Spec:** `docs/superpowers/specs/2026-08-25-monitor-globe-markers-design.md` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## File map |
||||
|
|
||||
|
| File | Responsibility | |
||||
|
|------|----------------| |
||||
|
| `public/assets/icons/dock-pin-*.svg` | 机巢水滴 pin(online/mission/alarm/offline/pending) | |
||||
|
| `public/assets/icons/drone-pin-*.svg` | 无人机水滴 pin(同上) | |
||||
|
| `src/config/map.js` | `MAP_OVERVIEW`、`MAP_VIEW.detailZoom`;保留 env detail center | |
||||
|
| `src/layout/MapLayer.vue` | `projection: 'globe'`、`minZoom: 1.5`、初始 overview 相机 | |
||||
|
| `src/views/MonitorView/MonitorView.vue` | pin `loadImage`、symbol 图层、overview 相机行为 | |
||||
|
|
||||
|
不改:Tasks/Replay 业务图层语义、侧栏 `dock.svg`/`drone.svg`、绑定虚线语义、独立 `/globe` 路由。 |
||||
|
|
||||
|
本仓库无地图单测;每任务以 `npm run build` + 必要的浏览器冒烟为准。 |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### Task 1: 重画水滴 pin SVG |
||||
|
|
||||
|
**Files:** |
||||
|
- Modify: `public/assets/icons/dock-pin.svg` |
||||
|
- Modify: `public/assets/icons/dock-pin-online.svg` |
||||
|
- Modify: `public/assets/icons/dock-pin-mission.svg` |
||||
|
- Modify: `public/assets/icons/dock-pin-alarm.svg` |
||||
|
- Modify: `public/assets/icons/dock-pin-offline.svg` |
||||
|
- Modify: `public/assets/icons/dock-pin-pending.svg` |
||||
|
- Modify: `public/assets/icons/drone-pin.svg` |
||||
|
- Modify: `public/assets/icons/drone-pin-online.svg` |
||||
|
- Modify: `public/assets/icons/drone-pin-mission.svg` |
||||
|
- Modify: `public/assets/icons/drone-pin-alarm.svg` |
||||
|
- Modify: `public/assets/icons/drone-pin-offline.svg` |
||||
|
- Modify: `public/assets/icons/drone-pin-pending.svg` |
||||
|
|
||||
|
- [ ] **Step 1: 统一 viewBox 与造型约定** |
||||
|
|
||||
|
所有 pin:`viewBox="0 0 48 64"`;底部椭圆阴影;水滴针身 + 白描边;针内白色剪影。 |
||||
|
|
||||
|
颜色: |
||||
|
|
||||
|
| key | dock fill | drone fill | |
||||
|
|-----|-----------|------------| |
||||
|
| online | `#1F6FBF` | `#1F9B6A` | |
||||
|
| mission | `#2D82DA` | `#2D82DA` | |
||||
|
| alarm | `#CC3D43` | `#CC3D43` | |
||||
|
| offline | `#84909B` | `#84909B` | |
||||
|
| pending | `#C48A2A` | `#C48A2A` | |
||||
|
| 无后缀 `dock-pin.svg` / `drone-pin.svg` | 同 online | 同 online | |
||||
|
|
||||
|
- [ ] **Step 2: 写入机巢 pin 模板(按状态替换 FILL)** |
||||
|
|
||||
|
每个 `dock-pin-*.svg` 整文件: |
||||
|
|
||||
|
```svg |
||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 64" fill="none"> |
||||
|
<ellipse cx="24" cy="58.5" rx="9" ry="3.2" fill="#0B1724" opacity="0.28"/> |
||||
|
<path d="M24 56.5C24 56.5 40 40.2 40 24.5a16 16 0 1 0-32 0C8 40.2 24 56.5 24 56.5Z" fill="FILL" stroke="#FFFFFF" stroke-width="2.6" stroke-linejoin="round"/> |
||||
|
<path d="M16.2 18.8c1.8-5.2 6.2-8.4 11.4-8.8" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" opacity="0.35"/> |
||||
|
<path d="M16.4 25.2 24 18.6l7.6 6.6V35.2c0 .7-.6 1.3-1.3 1.3h-3.2v-4.8h-4.2v4.8h-3.2c-.7 0-1.3-.6-1.3-1.3V25.2Z" fill="#FFFFFF"/> |
||||
|
<path d="M20.2 27.4h7.6" stroke="FILL" stroke-width="1.6" stroke-linecap="round"/> |
||||
|
</svg> |
||||
|
``` |
||||
|
|
||||
|
把 `FILL` 换成上表颜色。`dock-pin.svg` = online。 |
||||
|
|
||||
|
- [ ] **Step 3: 写入无人机 pin 模板(按状态替换 FILL)** |
||||
|
|
||||
|
每个 `drone-pin-*.svg` 整文件: |
||||
|
|
||||
|
```svg |
||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 64" fill="none"> |
||||
|
<ellipse cx="24" cy="58.5" rx="9" ry="3.2" fill="#0B1724" opacity="0.28"/> |
||||
|
<path d="M24 56.5C24 56.5 40 40.2 40 24.5a16 16 0 1 0-32 0C8 40.2 24 56.5 24 56.5Z" fill="FILL" stroke="#FFFFFF" stroke-width="2.6" stroke-linejoin="round"/> |
||||
|
<path d="M16.2 18.8c1.8-5.2 6.2-8.4 11.4-8.8" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" opacity="0.35"/> |
||||
|
<path d="M17.2 19.4 21.2 23.4M30.8 19.4 26.8 23.4M17.2 32.6 21.2 28.6M30.8 32.6 26.8 28.6" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round"/> |
||||
|
<circle cx="15.8" cy="17.8" r="3" stroke="#FFFFFF" stroke-width="2"/> |
||||
|
<circle cx="32.2" cy="17.8" r="3" stroke="#FFFFFF" stroke-width="2"/> |
||||
|
<circle cx="15.8" cy="34.2" r="3" stroke="#FFFFFF" stroke-width="2"/> |
||||
|
<circle cx="32.2" cy="34.2" r="3" stroke="#FFFFFF" stroke-width="2"/> |
||||
|
<rect x="20.2" y="22.4" width="7.6" height="7.6" rx="2" fill="#FFFFFF"/> |
||||
|
<circle cx="24" cy="26.2" r="1.2" fill="FILL"/> |
||||
|
</svg> |
||||
|
``` |
||||
|
|
||||
|
- [ ] **Step 4: Commit** |
||||
|
|
||||
|
```bash |
||||
|
git add public/assets/icons/dock-pin*.svg public/assets/icons/drone-pin*.svg |
||||
|
git commit -m "$(cat <<'EOF' |
||||
|
feat: 重画监控地图机巢/无人机水滴 pin |
||||
|
|
||||
|
覆盖 dock-pin/drone-pin 各状态 SVG,统一 48x64 立体水滴造型。 |
||||
|
EOF |
||||
|
)" |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### Task 2: map config + MapLayer globe overview |
||||
|
|
||||
|
**Files:** |
||||
|
- Modify: `src/config/map.js` |
||||
|
- Modify: `src/layout/MapLayer.vue` |
||||
|
|
||||
|
- [ ] **Step 1: 扩展 `src/config/map.js`** |
||||
|
|
||||
|
整文件替换为: |
||||
|
|
||||
|
```js |
||||
|
const token = import.meta.env.APP_MAPBOX_TOKEN || import.meta.env.VITE_MAPBOX_TOKEN || '' |
||||
|
|
||||
|
export const MAP_STYLES = { |
||||
|
satellite: 'mapbox://styles/mapbox/satellite-streets-v12', |
||||
|
street: 'mapbox://styles/mapbox/streets-v12', |
||||
|
} |
||||
|
|
||||
|
/** 监控全球总览:固定中国居中,不跟 env 贵州中心 */ |
||||
|
export const MAP_OVERVIEW = { |
||||
|
center: [104.0, 35.0], |
||||
|
zoom: 1.8, |
||||
|
} |
||||
|
|
||||
|
export const MAP_VIEW = { |
||||
|
detailZoom: 16, |
||||
|
minZoom: 1.5, |
||||
|
} |
||||
|
|
||||
|
export default { |
||||
|
token, |
||||
|
center: [ |
||||
|
Number(import.meta.env.APP_MAP_CENTER_LNG) || 106.62341584179686, |
||||
|
Number(import.meta.env.APP_MAP_CENTER_LAT) || 26.657826154258505, |
||||
|
], |
||||
|
zoom: Number(import.meta.env.APP_MAP_ZOOM) || 5, |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- [ ] **Step 2: 更新 `MapLayer.vue` 初始化** |
||||
|
|
||||
|
`initMap` 内 `new mapboxgl.Map` 改为: |
||||
|
|
||||
|
```js |
||||
|
import mapConfig, { MAP_STYLES, MAP_OVERVIEW, MAP_VIEW } from '@/config/map' |
||||
|
``` |
||||
|
|
||||
|
```js |
||||
|
map = new mapboxgl.Map({ |
||||
|
container: 'map', |
||||
|
style: MAP_STYLES.satellite, |
||||
|
center: MAP_OVERVIEW.center, |
||||
|
zoom: MAP_OVERVIEW.zoom, |
||||
|
minZoom: MAP_VIEW.minZoom, |
||||
|
attributionControl: false, |
||||
|
projection: 'globe', |
||||
|
}) |
||||
|
``` |
||||
|
|
||||
|
保留现有 token 缺失路径、`initTimer` 清理、`onUnmounted` `map.remove()`。 |
||||
|
|
||||
|
- [ ] **Step 3: Build** |
||||
|
|
||||
|
```bash |
||||
|
npm run build |
||||
|
``` |
||||
|
|
||||
|
Expected: exit 0。 |
||||
|
|
||||
|
- [ ] **Step 4: Commit** |
||||
|
|
||||
|
```bash |
||||
|
git add src/config/map.js src/layout/MapLayer.vue |
||||
|
git commit -m "$(cat <<'EOF' |
||||
|
feat: MapLayer 开启 globe 并以中国总览相机初始化 |
||||
|
|
||||
|
新增 MAP_OVERVIEW / MAP_VIEW,minZoom 1.5,projection globe。 |
||||
|
EOF |
||||
|
)" |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### Task 3: Monitor symbol 图层 + overview 相机 |
||||
|
|
||||
|
**Files:** |
||||
|
- Modify: `src/views/MonitorView/MonitorView.vue` |
||||
|
|
||||
|
- [ ] **Step 1: 常量与 import** |
||||
|
|
||||
|
把: |
||||
|
|
||||
|
```js |
||||
|
import mapConfig, { MAP_STYLES } from '@/config/map' |
||||
|
``` |
||||
|
|
||||
|
改为: |
||||
|
|
||||
|
```js |
||||
|
import mapConfig, { MAP_STYLES, MAP_OVERVIEW, MAP_VIEW } from '@/config/map' |
||||
|
``` |
||||
|
|
||||
|
图层 id: |
||||
|
|
||||
|
```js |
||||
|
const DEVICE_SOURCE = 'monitor-devices' |
||||
|
const DEVICE_SYMBOL = 'monitor-devices-symbol' |
||||
|
const DEVICE_HALO = 'monitor-devices-halo' |
||||
|
const DEVICE_LABEL = 'monitor-devices-label' |
||||
|
const BINDING_SOURCE = 'monitor-binding' |
||||
|
const BINDING_LINE = 'monitor-binding-line' |
||||
|
|
||||
|
const PIN_KINDS = ['dock', 'drone'] |
||||
|
const PIN_STATUSES = ['online', 'mission', 'alarm', 'offline', 'pending'] |
||||
|
|
||||
|
function pinImageId(kind, status) { |
||||
|
const s = PIN_STATUSES.includes(status) ? status : 'offline' |
||||
|
const k = PIN_KINDS.includes(kind) ? kind : 'dock' |
||||
|
return `${k}-pin-${s}` |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
删除旧 `DEVICE_CIRCLE` 引用(全文件替换为新 id)。 |
||||
|
|
||||
|
- [ ] **Step 2: Feature properties 增加 icon** |
||||
|
|
||||
|
`devicesFeatureCollection` 内 properties 改为: |
||||
|
|
||||
|
```js |
||||
|
const selected = selectedId.value === asset.id |
||||
|
const status = asset.statusClass || 'offline' |
||||
|
return { |
||||
|
type: 'Feature', |
||||
|
properties: { |
||||
|
assetId: String(asset.id), |
||||
|
name: asset.name || '', |
||||
|
kind: asset.type, |
||||
|
status, |
||||
|
selected: selected ? 1 : 0, |
||||
|
icon: pinImageId(asset.type, status), |
||||
|
}, |
||||
|
geometry: { type: 'Point', coordinates: [lon, lat] }, |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
可删除仅给 circle 用的 `color` / `radius`(若图例仍用 `STATUS_COLORS` 则保留该常量)。 |
||||
|
|
||||
|
- [ ] **Step 3: 加载 pin 图** |
||||
|
|
||||
|
在 map helpers 区新增: |
||||
|
|
||||
|
```js |
||||
|
async function ensurePinImages(map) { |
||||
|
if (!map) return |
||||
|
const jobs = [] |
||||
|
for (const kind of PIN_KINDS) { |
||||
|
for (const status of PIN_STATUSES) { |
||||
|
const id = pinImageId(kind, status) |
||||
|
if (map.hasImage(id)) continue |
||||
|
const url = `/assets/icons/${id}.svg` |
||||
|
jobs.push( |
||||
|
new Promise((resolve) => { |
||||
|
map.loadImage(url, (err, image) => { |
||||
|
if (!err && image && !map.hasImage(id)) { |
||||
|
map.addImage(id, image, { pixelRatio: 2 }) |
||||
|
} |
||||
|
resolve() |
||||
|
}) |
||||
|
}), |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
await Promise.all(jobs) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
注意:`setStyle` 后自定义 image 会丢,必须在 `rebuildAfterStyle` / `ensureMonitorLayers` 前重新 `ensurePinImages`。 |
||||
|
|
||||
|
若当前 mapbox-gl 版本 `loadImage` 返回 Promise,可改用: |
||||
|
|
||||
|
```js |
||||
|
const image = await map.loadImage(url) |
||||
|
if (!map.hasImage(id)) map.addImage(id, image.data || image, { pixelRatio: 2 }) |
||||
|
``` |
||||
|
|
||||
|
以实现时实际 API 为准(先 `rg "loadImage" node_modules/mapbox-gl` 或查类型)。优先 Promise 写法。 |
||||
|
|
||||
|
- [ ] **Step 4: `ensureMonitorLayers` 改为 symbol + 可选 halo** |
||||
|
|
||||
|
```js |
||||
|
async function ensureMonitorLayers() { |
||||
|
if (!mapInstance || !mapInstance.isStyleLoaded()) return |
||||
|
await ensurePinImages(mapInstance) |
||||
|
|
||||
|
if (!mapInstance.getSource(DEVICE_SOURCE)) { |
||||
|
mapInstance.addSource(DEVICE_SOURCE, { type: 'geojson', data: devicesFeatureCollection() }) |
||||
|
} |
||||
|
if (!mapInstance.getSource(BINDING_SOURCE)) { |
||||
|
mapInstance.addSource(BINDING_SOURCE, { type: 'geojson', data: bindingFeatureCollection() }) |
||||
|
} |
||||
|
|
||||
|
if (!mapInstance.getLayer(BINDING_LINE)) { |
||||
|
mapInstance.addLayer({ |
||||
|
id: BINDING_LINE, |
||||
|
type: 'line', |
||||
|
source: BINDING_SOURCE, |
||||
|
paint: { |
||||
|
'line-color': 'rgba(117,191,246,0.95)', |
||||
|
'line-width': 2, |
||||
|
'line-dasharray': [2, 1.5], |
||||
|
'line-opacity': 0.95, |
||||
|
}, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
if (!mapInstance.getLayer(DEVICE_HALO)) { |
||||
|
mapInstance.addLayer({ |
||||
|
id: DEVICE_HALO, |
||||
|
type: 'circle', |
||||
|
source: DEVICE_SOURCE, |
||||
|
filter: ['==', ['get', 'selected'], 1], |
||||
|
paint: { |
||||
|
'circle-radius': 22, |
||||
|
'circle-color': 'rgba(45,130,218,0.22)', |
||||
|
'circle-stroke-width': 0, |
||||
|
}, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
if (!mapInstance.getLayer(DEVICE_SYMBOL)) { |
||||
|
mapInstance.addLayer({ |
||||
|
id: DEVICE_SYMBOL, |
||||
|
type: 'symbol', |
||||
|
source: DEVICE_SOURCE, |
||||
|
layout: { |
||||
|
'icon-image': ['get', 'icon'], |
||||
|
'icon-size': [ |
||||
|
'case', |
||||
|
['==', ['get', 'selected'], 1], |
||||
|
1.25, |
||||
|
1, |
||||
|
], |
||||
|
'icon-anchor': 'bottom', |
||||
|
'icon-allow-overlap': true, |
||||
|
'icon-ignore-placement': true, |
||||
|
}, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
if (!mapInstance.getLayer(DEVICE_LABEL)) { |
||||
|
mapInstance.addLayer({ |
||||
|
id: DEVICE_LABEL, |
||||
|
type: 'symbol', |
||||
|
source: DEVICE_SOURCE, |
||||
|
minzoom: 4, |
||||
|
layout: { |
||||
|
'text-field': ['get', 'name'], |
||||
|
'text-size': 11, |
||||
|
'text-offset': [0, 0.6], |
||||
|
'text-anchor': 'top', |
||||
|
'text-allow-overlap': false, |
||||
|
}, |
||||
|
paint: { |
||||
|
'text-color': '#fff', |
||||
|
'text-halo-color': 'rgba(24,35,45,.86)', |
||||
|
'text-halo-width': 1.2, |
||||
|
}, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
layersReady = true |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
`clearMonitorLayers` 删除顺序改为: |
||||
|
|
||||
|
```js |
||||
|
for (const id of [DEVICE_LABEL, DEVICE_SYMBOL, DEVICE_HALO, BINDING_LINE]) { |
||||
|
if (mapInstance.getLayer(id)) mapInstance.removeLayer(id) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- [ ] **Step 5: 相机 helper** |
||||
|
|
||||
|
新增: |
||||
|
|
||||
|
```js |
||||
|
function resetOverviewCamera() { |
||||
|
if (!mapInstance) return |
||||
|
mapInstance.easeTo({ |
||||
|
center: MAP_OVERVIEW.center, |
||||
|
zoom: MAP_OVERVIEW.zoom, |
||||
|
pitch: 0, |
||||
|
bearing: 0, |
||||
|
duration: 450, |
||||
|
}) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
改 `fitAll`: |
||||
|
|
||||
|
```js |
||||
|
function fitAll() { |
||||
|
if (selectedId.value) { |
||||
|
clearSelection() |
||||
|
return |
||||
|
} |
||||
|
resetOverviewCamera() |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
`flyToSelected` 使用 `MAP_VIEW.detailZoom`: |
||||
|
|
||||
|
```js |
||||
|
mapInstance.easeTo({ |
||||
|
center: [lon, lat], |
||||
|
zoom: Math.max(mapInstance.getZoom(), MAP_VIEW.detailZoom), |
||||
|
duration: 450, |
||||
|
}) |
||||
|
``` |
||||
|
|
||||
|
可删除 `fitAllMarkers`,或保留但不再被调用。推荐删除并修所有调用点。 |
||||
|
|
||||
|
- [ ] **Step 6: setup / rebuild / watch 调用点** |
||||
|
|
||||
|
`rebuildAfterStyle`: |
||||
|
|
||||
|
```js |
||||
|
async function rebuildAfterStyle() { |
||||
|
pendingStyleHandler = null |
||||
|
if (!mapInstance) return |
||||
|
layersReady = false |
||||
|
await ensureMonitorLayers() |
||||
|
syncMarkers() |
||||
|
updateMapScale() |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
`setupMap` 的 `onReady`: |
||||
|
|
||||
|
```js |
||||
|
const onReady = async () => { |
||||
|
pendingLoadHandler = null |
||||
|
if (!mapInstance) return |
||||
|
await ensureMonitorLayers() |
||||
|
syncMarkers() |
||||
|
if (selectedId.value) flyToSelected() |
||||
|
else resetOverviewCamera() |
||||
|
updateMapScale() |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
`watch(selectedId)`: |
||||
|
|
||||
|
```js |
||||
|
watch(selectedId, () => { |
||||
|
syncMarkers() |
||||
|
if (selectedId.value) flyToSelected() |
||||
|
else resetOverviewCamera() |
||||
|
}) |
||||
|
``` |
||||
|
|
||||
|
全局搜索 `fitAllMarkers`,全部改为 `resetOverviewCamera` 或删除。 |
||||
|
|
||||
|
`onMapClick` 命中图层改为: |
||||
|
|
||||
|
```js |
||||
|
const layers = [DEVICE_SYMBOL, DEVICE_LABEL].filter((id) => mapInstance.getLayer(id)) |
||||
|
``` |
||||
|
|
||||
|
可选:symbol 上 `mouseenter`/`mouseleave` 设 `map.getCanvas().style.cursor`。 |
||||
|
|
||||
|
- [ ] **Step 7: Build** |
||||
|
|
||||
|
```bash |
||||
|
npm run build |
||||
|
``` |
||||
|
|
||||
|
Expected: exit 0。 |
||||
|
|
||||
|
- [ ] **Step 8: Commit** |
||||
|
|
||||
|
```bash |
||||
|
git add src/views/MonitorView/MonitorView.vue |
||||
|
git commit -m "$(cat <<'EOF' |
||||
|
feat: 监控地图改水滴 symbol 并默认全球总览相机 |
||||
|
|
||||
|
加载状态 pin;清选中/查看全部回中国居中;去掉进页 fitBounds。 |
||||
|
EOF |
||||
|
)" |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### Task 4: 浏览器冒烟验收 |
||||
|
|
||||
|
**Files:** 无代码;验证 Task 1–3。 |
||||
|
|
||||
|
- [ ] **Step 1: 启动** |
||||
|
|
||||
|
```bash |
||||
|
npm run dev -- --host 0.0.0.0 --port 5173 |
||||
|
``` |
||||
|
|
||||
|
登录进 `/monitor`(1920×1080)。 |
||||
|
|
||||
|
- [ ] **Step 2: 验收清单** |
||||
|
|
||||
|
1. 进页:地球外形可见,中国大致居中,zoom 全球级;**不**自动 fit 到设备包围盒。 |
||||
|
2. 可见机巢蓝针 / 无人机绿针(有对应状态设备时告警红、离线灰可辨)。 |
||||
|
3. 点击设备:飞近;详情面板打开。 |
||||
|
4. 点空白或关详情:回到全球中国居中。 |
||||
|
5. 点「查看全部」:同样回 overview。 |
||||
|
6. 卫星 ↔ 地图:点位与绑定虚线仍在。 |
||||
|
7. 控制台无 map/style/image 报错。 |
||||
|
|
||||
|
- [ ] **Step 3: 收尾 commit(仅当冒烟中修了小问题)** |
||||
|
|
||||
|
若无代码改动则跳过。有则: |
||||
|
|
||||
|
```bash |
||||
|
git add -A |
||||
|
git commit -m "$(cat <<'EOF' |
||||
|
fix: 监控全球视角与 pin 冒烟问题修复 |
||||
|
EOF |
||||
|
)" |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Spec coverage |
||||
|
|
||||
|
| Spec 项 | Task | |
||||
|
|---------|------| |
||||
|
| `projection: 'globe'` + minZoom 1.5 | 2 | |
||||
|
| `MAP_OVERVIEW` 中国居中 / zoom 1.8 | 2 | |
||||
|
| 进页不 fitBounds | 3 | |
||||
|
| 清选中 / 查看全部 → overview | 3 | |
||||
|
| 点选 detailZoom | 3 | |
||||
|
| 重画 pin SVG 覆盖 | 1 | |
||||
|
| circle → symbol + selected 放大/光晕 | 3 | |
||||
|
| label minzoom ≥ 4 | 3 | |
||||
|
| setStyle 后重建 + 重载 image | 3 | |
||||
|
| 绑定虚线保留 | 3(不改语义) | |
||||
|
| 不做 /globe、cluster、侧栏图标 | 全任务遵守 | |
||||
|
| build + 冒烟 | 2/3/4 | |
||||
|
|
||||
|
## Placeholder / consistency check |
||||
|
|
||||
|
- 无 TBD/TODO。 |
||||
|
- 图层 id 统一 `DEVICE_SYMBOL` / `DEVICE_HALO`。 |
||||
|
- 相机 API 统一 `resetOverviewCamera` / `MAP_OVERVIEW` / `MAP_VIEW.detailZoom`。 |
||||
|
- pin id 统一 `` `${kind}-pin-${status}` `` 对应 `/assets/icons/...svg`。 |
||||
Loading…
Reference in new issue