第04章 核心组件 PsvContainer 详解
4.1 组件职责
PsvContainer.vue 是项目中最核心的组件,它是唯一与 Photo Sphere Viewer 直接交互的组件。它的职责是:
- 封装 PSV Viewer 的完整生命周期(创建、更新、销毁)
- 配置并管理两个插件:MarkersPlugin(全景标记)和 PlanPlugin(Leaflet 地图)
- 建立全景标记与地图热点的双向联动
- 通过
defineExpose暴露语义化接口给父组件调用
组件文件位于 src/components/PsvContainer.vue,共 232 行。
4.2 Props 与 Emits
4.2.1 Props
// PsvContainer.vue:18-23
const props = defineProps<{
scene: Scene // 当前场景
markers: MarkerData[] // 当前场景的标记列表
previewMarker?: MarkerData | null // 待确认的红色脉冲预览 pin
}>()
scene:当前场景对象,包含全景图地址、坐标、方位角等信息markers:当前场景的标记列表previewMarker:添加标记时的预览 pin(红色脉冲动画),可选
4.2.2 Emits
// PsvContainer.vue:25-30
const emit = defineEmits<{
'click-empty': [position: { yaw: number, pitch: number }] // 点击全景空白处
'map-pick': [coords: [number, number]] // 点击地图选点
'marker-click': [id: string] // 点击全景标记或地图热点
}>()
三个事件分别对应三种交互:
click-empty:点击全景空白处(非标记),携带球形坐标 yaw/pitchmap-pick:点击地图选点,携带经纬度坐标marker-click:点击全景标记或地图热点,携带标记 id
4.3 模块级插件实例变量
// PsvContainer.vue:32-37
let viewer: Viewer | null = null
let markersPlugin: MarkersPlugin | null = null
let planPlugin: PlanPlugin | null = null
let viewerSceneId: string | null = null // 记录当前 viewer 绑定的场景
let previewShown = false
这些变量在模块级声明,用于在组件的不同生命周期钩子和 watcher 中共享插件实例。
4.4 PSV 实例化
4.4.1 创建 Viewer
// PsvContainer.vue:39-122
viewer = new Viewer({
container: containerRef.value,
panorama: props.scene.panorama,
caption: props.scene.name,
loadingImg: BASE_URL + 'loader.gif',
navbar: ['zoom', 'fullscreen', 'caption', 'markers', 'markersList'],
lang: { /* 全中文 UI 文案 */ },
plugins: [
PlanPlugin.withConfig({
coordinates: props.scene.coordinates,
bearing: props.scene.bearing,
defaultZoom: props.scene.defaultZoom,
size: { width: 'min(300px, 72vw)', height: 'min(300px, 34vh)' },
position: 'bottom left',
visibleOnLoad: true,
}),
MarkersPlugin.withConfig({
markers: props.markers.map(toPsvMarkerConfig),
}),
],
})
关键配置说明:
- container:PSV 挂载的 DOM 容器
- panorama:全景图片地址
- caption:全景标题(显示在导航栏)
- loadingImg:加载动画
- navbar:导航栏按钮,包括缩放、全屏、标题、标记列表
- lang:全中文 UI 文案
- plugins:配置两个插件
4.4.2 PlanPlugin 配置
PlanPlugin 用于嵌入 Leaflet 地图:
coordinates:地图中心坐标bearing:地图方位角(与全景视角对齐)defaultZoom:默认缩放级别size:地图面板尺寸(响应式)position:面板位置(左下角)visibleOnLoad:加载时可见
重要提示:不要使用
configureLeaflet选项。它的语义是”完全接管 Leaflet 配置”,传入后默认 OSM 底图不会添加,地图会变成灰色空面板。点击监听改为创建后通过getLeaflet()挂载。
4.4.3 MarkersPlugin 配置
MarkersPlugin 用于全景标记:
MarkersPlugin.withConfig({
markers: props.markers.map(toPsvMarkerConfig),
})
将标记数据通过 toPsvMarkerConfig 转换为 PSV 标记配置格式。
4.5 插件获取与事件绑定
4.5.1 获取插件实例
// PsvContainer.vue:92-118
markersPlugin = viewer.getPlugin<MarkersPlugin>(MarkersPlugin)
planPlugin = viewer.getPlugin<PlanPlugin>(PlanPlugin)
4.5.2 地图点击精确选点
planPlugin?.getLeaflet().on('click', (e) => {
emit('map-pick', [e.latlng.lng, e.latlng.lat])
})
通过 getLeaflet() 获取 Leaflet 地图实例,监听点击事件,将经纬度坐标 emit 出去。
4.5.3 全景点击获取位置
viewer.addEventListener('click', (e) => {
if (e.data.marker) return // 点击标记时不触发
emit('click-empty', { yaw: e.data.yaw, pitch: e.data.pitch })
})
监听全景点击事件,如果点击的是标记则忽略,否则 emit 球形坐标。
4.5.4 标记点击事件
markersPlugin?.addEventListener('select-marker', ({ marker }) => {
emit('marker-click', marker.id)
})
planPlugin?.addEventListener('select-hotspot', ({ hotspotId }) => {
emit('marker-click', hotspotId)
})
全景标记点击(select-marker)和地图热点点击(select-hotspot)都 emit 同一个 marker-click 事件,由 App 统一决定是旋转视角还是切换场景。
4.6 预览 pin 配置
4.6.1 构造预览配置
// PsvContainer.vue:134-152
function previewConfig() {
// 构造一个 id 为 PREVIEW_ID 的红色 pin 标记
// 同时带 data.plan 让地图上也显示
}
4.6.2 同步预览
// PsvContainer.vue:154-164
function syncPreview() {
// 先移除旧的预览,再按需添加
}
syncPreview() 负责预览 pin 的增删:先移除旧的再按需添加,避免重复。
4.7 标记刷新
// PsvContainer.vue:166-171
function refreshMarkers() {
markersPlugin?.setMarkers(props.markers.map(toPsvMarkerConfig))
}
refreshMarkers() 用 setMarkers() 全量替换标记。
4.8 场景切换 watcher
// PsvContainer.vue:175-187
watch(() => props.scene.id, (id) => {
if (!viewer || viewerSceneId === id) return
viewerSceneId = id
planPlugin?.setOptions({ bearing: props.scene.bearing })
planPlugin?.setCoordinates(props.scene.coordinates)
planPlugin?.setZoom(props.scene.defaultZoom)
viewer.setPanorama(props.scene.panorama, {
caption: props.scene.name,
showLoader: true,
})
})
关键设计:场景切换时复用 viewer 而非销毁重建。相比销毁重建,这种方式:
- 保留 WebGL 上下文
- 保留 Leaflet 瓦片缓存
- 切换更快且带过渡动画
标记刷新交给下方的 markers watcher(切场景时 currentMarkers 返回新引用必然触发),避免 setMarkers 重复执行两次。
4.9 标记 watcher
// PsvContainer.vue:190-199
watch(() => props.markers, () => {
// 用 flush: 'post' 增量刷新
}, { flush: 'post' })
watch(() => props.previewMarker, () => {
// 同步预览
})
两个 watcher 都依赖”每次返回新引用”来触发,无需 deep。
4.10 生命周期
// PsvContainer.vue:201-207
onMounted(() => createViewer())
onUnmounted(() => destroyViewer())
onMounted:创建 vieweronUnmounted:销毁 viewer(viewer.destroy()并置空引用)
4.11 暴露接口
// PsvContainer.vue:210-218
defineExpose({ gotoMarker, refreshMarkers })
通过 defineExpose 暴露语义化接口:
gotoMarker(id):内部调markersPlugin?.gotoMarker(id),这是点击标记触发全景旋转的入口refreshMarkers():刷新标记
设计要点:暴露语义化接口,不直接暴露插件实例,保持组件封装性。
4.12 组件在整体架构中的位置
App.vue
└── PsvContainer (defineExpose: gotoMarker, refreshMarkers)
├── Viewer (全景查看器)
├── MarkersPlugin (全景标记)
└── PlanPlugin (Leaflet 地图)
App.vue 通过 psvRef 引用 PsvContainer 实例,调用其暴露的 gotoMarker 方法实现”点击标记旋转视角”。
4.13 下一步
理解了核心组件后,进入 第05章 全景与地图双向联动机制 深入理解联动原理。