第06章 标记管理功能详解

6.1 标记管理概述

标记管理是 map-360-demo 的核心交互功能,涵盖标记的添加、编辑、删除、搜索、导入导出等完整生命周期。本章详细讲解每个功能的实现。

6.2 标记类型

标记分为两种类型:

类型 图标 行为
info(信息标记) 📍 蓝色 pin 点击后全景旋转到对应位置
link(跳转标记) ➜ 绿色箭头 点击后切换到目标场景

类型定义在 src/types/index.ts

type MarkerType = 'info' | 'link'

6.3 添加标记流程

6.3.1 状态机设计

添加/编辑标记由 useAddMarkerFlow.ts 状态机管理,核心状态:

// useAddMarkerFlow.ts:24-30
const adding = ref(false)          // 是否在添加模式
const showModal = ref(false)       // 是否显示弹窗
const editingId = ref('')          // 编辑中的标记 id(空=新增)
const pendingPosition = ref<{ yaw: number, pitch: number } | null>(null)  // 选点 yaw/pitch
const pendingCoords = ref<[number, number] | null>(null)  // 选点经纬度

6.3.2 添加模式

// useAddMarkerFlow.ts:71-77
function startAddMarker() {
  adding.value = true
  editingId.value = ''
  // 清空编辑状态
}

点击「添加标记」按钮进入添加模式,此时:

  1. 顶部出现提示条(”点击全景或地图选择位置”)
  2. 点击全景空白处或地图可精确定位

6.3.3 全景点击选点

// useAddMarkerFlow.ts:96-102
function onClickEmpty(position: { yaw: number, pitch: number }) {
  pendingPosition.value = position
  pendingCoords.value = estimateGps(scene, position.yaw, position.pitch)  // 估算经纬度
  showModal.value = true
}

点击全景空白处:

  1. 保存球形坐标 yaw/pitch
  2. 通过 estimateGps 估算经纬度
  3. 打开弹窗

6.3.4 地图点击选点

// useAddMarkerFlow.ts:104-113
function onMapPick(coords: [number, number]) {
  pendingCoords.value = coords
  pendingPosition.value = {
    yaw: yawFromGps(scene, coords),  // 反推 yaw
    pitch: 0,
  }
  showModal.value = true
}

点击地图:

  1. 保存经纬度坐标
  2. 通过 yawFromGps 反推 yaw(使 360 预览 pin 朝向地图所选方向)
  3. 打开弹窗

6.3.5 预览 pin

// useAddMarkerFlow.ts:48-69
const previewMarker = computed(() => {
  // 仅当有 pendingPosition 和 pendingCoords 时返回
  // 编辑模式下若位置与原有标记完全相同则不显示预览(避免与蓝 pin 重叠)
  return { id: PREVIEW_ID, ... }
})

预览 pin 是添加标记时的红色脉冲标记,用于即时反馈选点位置。

6.3.6 弹窗确认

// useAddMarkerFlow.ts:134-161
function onModalConfirm(payload: MarkerPayload) {
  // 若手动改过坐标则重新 yawFromGps 反推 yaw
  // 构造 base 对象
  // 按 editingId 决定 onUpdate 还是 onAdd
  // 新增时补 sceneId
  exitAdding()
}

确认后:

  1. 若手动改过坐标,重新反推 yaw 保持方向一致
  2. 构造标记对象
  3. 新增或更新
  4. 退出添加模式

6.4 编辑标记流程

6.4.1 进入编辑模式

// useAddMarkerFlow.ts:79-85
function openEdit(marker: MarkerData) {
  editingId.value = marker.id
  pendingPosition.value = marker.position
  pendingCoords.value = marker.coordinates
  showModal.value = true
}

点击标记行的 ✎ 按钮进入编辑模式:

  1. 记录编辑中的标记 id
  2. 预填位置和坐标
  3. 打开弹窗

6.4.2 弹窗预填

MarkerModal.vuewatch(visible) 时预填表单:

// MarkerModal.vue:183-206
watch(visible, () => {
  if (editingMarker) {
    // 预填 name/description/type/targetSceneId
  } else {
    // 清空表单
  }
})

6.5 删除标记

// App.vue:177-189
function onDeleteMarker(id: string) {
  if (confirm('确定删除该标记吗?')) {
    removeMarker(id)
  }
}

删除前用原生 confirm() 二次确认,确认后调用 removeMarker

6.6 搜索标记

MarkerList.vue 实现搜索过滤:

// MarkerList.vue:113-119
const visibleMarkers = computed(() => {
  const q = searchQuery.value.toLowerCase()
  return sortedMarkers.value.filter(m =>
    m.name.toLowerCase().includes(q) ||
    m.description.toLowerCase().includes(q)
  )
})

按名称/描述小写包含过滤。

6.7 标记排序

// MarkerList.vue:109-111
const sortedMarkers = computed(() => {
  return [...props.markers].sort((a, b) => b.createdAt - a.createdAt)
})

createdAt 降序排列,新标记置顶。

6.8 导入导出

6.8.1 导出

// App.vue:191-201
function onExport() {
  const json = exportMarkers()  // JSON.stringify(markers, null, 2)
  const blob = new Blob([json], { type: 'application/json' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = `map-360-markers-${new Date().toISOString().slice(0, 10)}.json`
  a.click()
  setTimeout(() => URL.revokeObjectURL(url), 100)  // 释放内存
}

导出流程:

  1. 将标记序列化为美化 JSON
  2. 创建 Blob 对象
  3. 创建临时 <a download> 触发下载
  4. 文件名带日期
  5. 100ms 后释放 URL

6.8.2 导入

// App.vue:203-206
function onImport(json: string) {
  const result = importMarkers(json)
  show(result.message, result.ok ? 'success' : 'error')
}

导入流程(useAppState.ts:139-170):

  1. 解析 JSON
  2. 校验是否为数组
  3. 过滤非法标记(isValidMarker
  4. 丢弃场景不存在的标记
  5. 丢弃 link 标记目标场景不存在的
  6. 重新分配 id 避免冲突
  7. 合并到现有标记

6.9 恢复默认

// useAppState.ts:119-128
function resetMarkers() {
  markers.value = deepClone(DEFAULT_MARKERS)  // 深拷贝,防止污染常量
}

深拷贝 DEFAULT_MARKERS(嵌套 position/coordinates 也拷贝),防止污染常量,watch 自动持久化。

6.10 标记列表 UI

MarkerList.vue 提供底部标记列表,功能包括:

  • 折叠collapsed ref,箭头旋转动画
  • link 徽章:link 标记显示绿色 ➜ 目标场景名 徽章
  • 时间格式化:当天显示 HH:mm,否则 MM-DD HH:mm
  • 导入文件:隐藏 <input type="file">FileReader.readAsText 读取

6.11 下一步

理解了标记管理后,进入 第07章 状态管理与数据持久化 了解状态管理机制。