第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 = ''
// 清空编辑状态
}
点击「添加标记」按钮进入添加模式,此时:
- 顶部出现提示条(”点击全景或地图选择位置”)
- 点击全景空白处或地图可精确定位
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
}
点击全景空白处:
- 保存球形坐标 yaw/pitch
- 通过
estimateGps估算经纬度 - 打开弹窗
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
}
点击地图:
- 保存经纬度坐标
- 通过
yawFromGps反推 yaw(使 360 预览 pin 朝向地图所选方向) - 打开弹窗
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()
}
确认后:
- 若手动改过坐标,重新反推 yaw 保持方向一致
- 构造标记对象
- 新增或更新
- 退出添加模式
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
}
点击标记行的 ✎ 按钮进入编辑模式:
- 记录编辑中的标记 id
- 预填位置和坐标
- 打开弹窗
6.4.2 弹窗预填
MarkerModal.vue 在 watch(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) // 释放内存
}
导出流程:
- 将标记序列化为美化 JSON
- 创建 Blob 对象
- 创建临时
<a download>触发下载 - 文件名带日期
- 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):
- 解析 JSON
- 校验是否为数组
- 过滤非法标记(
isValidMarker) - 丢弃场景不存在的标记
- 丢弃 link 标记目标场景不存在的
- 重新分配 id 避免冲突
- 合并到现有标记
6.9 恢复默认
// useAppState.ts:119-128
function resetMarkers() {
markers.value = deepClone(DEFAULT_MARKERS) // 深拷贝,防止污染常量
}
深拷贝 DEFAULT_MARKERS(嵌套 position/coordinates 也拷贝),防止污染常量,watch 自动持久化。
6.10 标记列表 UI
MarkerList.vue 提供底部标记列表,功能包括:
- 折叠:
collapsedref,箭头旋转动画 - link 徽章:link 标记显示绿色
➜ 目标场景名徽章 - 时间格式化:当天显示
HH:mm,否则MM-DD HH:mm - 导入文件:隐藏
<input type="file">,FileReader.readAsText读取
6.11 下一步
理解了标记管理后,进入 第07章 状态管理与数据持久化 了解状态管理机制。