You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

15 KiB

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_OVERVIEWMAP_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 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 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
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

整文件替换为:

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 初始化

initMapnew mapboxgl.Map 改为:

import mapConfig, { MAP_STYLES, MAP_OVERVIEW, MAP_VIEW } from '@/config/map'
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
npm run build

Expected: exit 0。

  • Step 4: Commit
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

把:

import mapConfig, { MAP_STYLES } from '@/config/map'

改为:

import mapConfig, { MAP_STYLES, MAP_OVERVIEW, MAP_VIEW } from '@/config/map'

图层 id:

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 改为:

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 区新增:

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,可改用:

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
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 删除顺序改为:

for (const id of [DEVICE_LABEL, DEVICE_SYMBOL, DEVICE_HALO, BINDING_LINE]) {
  if (mapInstance.getLayer(id)) mapInstance.removeLayer(id)
}
  • Step 5: 相机 helper

新增:

function resetOverviewCamera() {
  if (!mapInstance) return
  mapInstance.easeTo({
    center: MAP_OVERVIEW.center,
    zoom: MAP_OVERVIEW.zoom,
    pitch: 0,
    bearing: 0,
    duration: 450,
  })
}

fitAll

function fitAll() {
  if (selectedId.value) {
    clearSelection()
    return
  }
  resetOverviewCamera()
}

flyToSelected 使用 MAP_VIEW.detailZoom

mapInstance.easeTo({
  center: [lon, lat],
  zoom: Math.max(mapInstance.getZoom(), MAP_VIEW.detailZoom),
  duration: 450,
})

可删除 fitAllMarkers,或保留但不再被调用。推荐删除并修所有调用点。

  • Step 6: setup / rebuild / watch 调用点

rebuildAfterStyle

async function rebuildAfterStyle() {
  pendingStyleHandler = null
  if (!mapInstance) return
  layersReady = false
  await ensureMonitorLayers()
  syncMarkers()
  updateMapScale()
}

setupMaponReady

const onReady = async () => {
  pendingLoadHandler = null
  if (!mapInstance) return
  await ensureMonitorLayers()
  syncMarkers()
  if (selectedId.value) flyToSelected()
  else resetOverviewCamera()
  updateMapScale()
}

watch(selectedId)

watch(selectedId, () => {
  syncMarkers()
  if (selectedId.value) flyToSelected()
  else resetOverviewCamera()
})

全局搜索 fitAllMarkers,全部改为 resetOverviewCamera 或删除。

onMapClick 命中图层改为:

const layers = [DEVICE_SYMBOL, DEVICE_LABEL].filter((id) => mapInstance.getLayer(id))

可选:symbol 上 mouseenter/mouseleavemap.getCanvas().style.cursor

  • Step 7: Build
npm run build

Expected: exit 0。

  • Step 8: Commit
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: 启动
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(仅当冒烟中修了小问题)

若无代码改动则跳过。有则:

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