提交室内导览交互与讲解优化

This commit is contained in:
lyf
2026-06-14 23:48:13 +08:00
parent a7c1879f60
commit feb7310a46
33 changed files with 3257 additions and 361 deletions

View File

@@ -59,7 +59,7 @@
</view>
<view v-if="exhibit.audio.status !== 'playable'" class="audio-note">
<text class="audio-note-title">音频待开放</text>
<text class="audio-note-title">音频待同步</text>
<text class="audio-note-desc">
{{ exhibit.audio.unavailableReason || '当前仅提供图文讲解,正式音频接入后将显示播放入口。' }}
</text>
@@ -154,6 +154,9 @@ import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
import {
explainUseCase
} from '@/usecases/explainUseCase'
import {
guideUseCase
} from '@/usecases/guideUseCase'
import {
toExplainDetailPageViewModel,
type ExplainContentType,
@@ -171,11 +174,11 @@ const defaultDetail: ExplainDetailPageViewModel = {
subtitle: '位置待补充',
contentType: 'exhibit',
coverImages: ['/static/exhibit-placeholder.jpg'],
summary: '该讲解内容待正式内容补充。',
body: '该讲解内容待正式内容补充。',
summary: '该讲解内容待 SGS 场景设置或 CMS 内容补充。',
body: '该讲解内容待 SGS 场景设置或 CMS 内容补充。',
audio: {
status: 'unavailable',
unavailableReason: '正式讲解音频尚未接入媒体仓储'
unavailableReason: 'SGS 场景设置音频资源尚未同步到当前 H5 项目'
},
chapters: [],
relatedItems: []
@@ -194,14 +197,14 @@ const audioStatusText = computed(() => {
if (exhibit.value.audio.status === 'none') {
return '图文讲解'
}
return '音频待开放'
return exhibit.value.audio.unavailableReason?.includes('包含音频地址') ? '音频待同步' : '图文讲解'
})
const contentTypeText = computed(() => {
const map: Record<ExplainContentType, string> = {
hall: '展厅讲解',
hall: '空间讲解',
zone: '展区讲解',
exhibit: '讲解',
exhibit: '展品讲解',
theme: '主题讲解'
}
return map[exhibit.value.contentType]
@@ -243,7 +246,7 @@ const handlePlayAudio = async () => {
isPlaying.value = !isPlaying.value
}
const handleNavigate = () => {
const handleNavigate = async () => {
if (!exhibit.value.location?.poiId) {
uni.showToast({
title: '该讲解暂无三维位置数据',
@@ -252,6 +255,15 @@ const handleNavigate = () => {
return
}
const matchedPoi = await guideUseCase.getPoiById(exhibit.value.location.poiId)
if (!matchedPoi) {
uni.showToast({
title: '该讲解位置尚未映射到当前三维导览资源',
icon: 'none'
})
return
}
uni.navigateTo({
url: `/pages/route/detail?facilityId=${encodeURIComponent(exhibit.value.location.poiId)}&target=${encodeURIComponent(exhibit.value.title)}&state=preview`
})

View File

@@ -22,7 +22,7 @@
<view class="stats-row">
<view class="stat-item">
<text class="stat-value">{{ hall.exhibitCount }}</text>
<text class="stat-label">讲解</text>
<text class="stat-label">讲解内容</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ hall.floorLabel }}</text>
@@ -32,7 +32,7 @@
<!-- 讲解列表 -->
<view class="exhibits-section">
<text class="section-title">展厅讲解内容</text>
<text class="section-title">空间讲解内容</text>
<view class="exhibits-grid">
<ExhibitCard
v-for="exhibit in exhibits"
@@ -63,6 +63,9 @@ import ExhibitCard from '@/components/content/ExhibitCard.vue'
import {
explainUseCase
} from '@/usecases/explainUseCase'
import {
guideUseCase
} from '@/usecases/guideUseCase'
import {
toExplainExhibitViewModel,
toHallDetailViewModel,
@@ -81,7 +84,7 @@ const hall = ref<HallDetailViewModel>({
id: '',
name: '展厅内容',
floorLabel: '楼层待补充',
description: '该展厅介绍待正式内容补充。',
description: '该空间介绍待 SGS 场景设置或 CMS 内容补充。',
image: '/static/hall-placeholder.jpg',
exhibitCount: 0,
area: '待补充'
@@ -111,7 +114,7 @@ const handleExhibitClick = (exhibit: any) => {
})
}
const handleNavigate = () => {
const handleNavigate = async () => {
if (!hall.value.poiId) {
uni.showToast({
title: '该展厅暂无三维位置数据',
@@ -120,6 +123,15 @@ const handleNavigate = () => {
return
}
const matchedPoi = await guideUseCase.getPoiById(hall.value.poiId)
if (!matchedPoi) {
uni.showToast({
title: '该空间位置尚未映射到当前三维导览资源',
icon: 'none'
})
return
}
uni.navigateTo({
url: `/pages/route/detail?facilityId=${encodeURIComponent(hall.value.poiId)}&target=${encodeURIComponent(hall.value.name)}&state=preview`
})

View File

@@ -21,16 +21,21 @@
:normalize-floor-id="normalizeGuideFloorId"
indoor-initial-view="overview"
:indoor-view="indoorView"
:layer-mode="indoorLayerMode"
:active-floor="activeGuideFloor"
indoor-view-toggle-top="154px"
layer-mode-toggle-top="154px"
floor-top="244px"
:show-indoor-view-toggle="is3DMode"
:show-floor="is3DMode && indoorView === 'floor'"
:tools="[]"
:show-layer-mode-toggle="is3DMode"
:show-floor="is3DMode"
:tools="guide3DTools"
@search-tap="handleGuideSearchTap"
@mode-change="handleModeChange"
@floor-change="handleFloorChange"
@indoor-view-change="handleIndoorViewChange"
@poi-click="handleGuidePoiClick"
@selection-clear="handleGuideSelectionClear"
@tool-click="handleGuideToolClick"
@auto-switch="handleAutoSwitch"
>
<template #overlay>
<view v-if="guideOutdoorState === 'entrance' && !is3DMode" class="entrance-tip">
@@ -135,15 +140,22 @@ import {
import {
toExplainCardViewModel
} from '@/view-models/explainViewModels'
import type {
GuideRenderPoi
} from '@/domain/guideModel'
import type {
MuseumFloor
} from '@/domain/museum'
type GuideIndoorView = 'overview' | 'floor' | 'multi'
type LayerDisplayMode = 'single' | 'multi'
// 3D 模式状态
const is3DMode = ref(false)
const guideOutdoorState = ref<'home' | 'entrance'>('home')
const indoorView = ref<'overview' | 'floor'>('overview')
const indoorView = ref<GuideIndoorView>('overview')
const activeGuideFloor = ref('1F')
const selectedGuidePoi = ref<GuideRenderPoi | null>(null)
// 状态
const searchKeyword = ref('')
@@ -157,6 +169,16 @@ const indoorNavAssetBaseUrl = guideUseCase.getAssetBaseUrl()
const indoorModelSource = guideUseCase.getModelSource()
const guideFloors = ref<MuseumFloor[]>([])
const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloorId(labelOrId)
const indoorLayerMode = computed<LayerDisplayMode>(() => (
indoorView.value === 'multi' ? 'multi' : 'single'
))
const guide3DTools = computed(() => {
if (!is3DMode.value) return []
return selectedGuidePoi.value
? ['重置', '俯视', '斜视', '清除']
: ['重置', '俯视', '斜视']
})
const shortcutItems = [
{ id: 'toilet', name: '卫生间', abbr: '卫' },
@@ -351,11 +373,40 @@ const handleCollectMarker = (marker: MarkerDetail) => {
const handleFloorChange = (floor: string) => {
activeGuideFloor.value = floor
indoorView.value = 'floor'
selectedGuidePoi.value = null
console.log('切换楼层:', floor)
}
const handleIndoorViewChange = (view: 'overview' | 'floor') => {
const handleIndoorViewChange = (view: GuideIndoorView) => {
indoorView.value = view
if (view !== 'floor') {
selectedGuidePoi.value = null
}
}
const handleAutoSwitch = (event: { from: 'overview' | 'floor'; to: 'overview' | 'floor'; trigger: string; distance: number }) => {
console.log('自动切换视图:', event)
indoorView.value = event.to
if (event.to !== 'floor') {
selectedGuidePoi.value = null
}
}
const handleGuidePoiClick = (poi: GuideRenderPoi) => {
selectedGuidePoi.value = poi
indoorView.value = 'floor'
const matchedFloor = guideFloors.value.find((floor) => floor.id === poi.floorId)
activeGuideFloor.value = matchedFloor?.label || poi.floorId
}
const handleGuideSelectionClear = () => {
selectedGuidePoi.value = null
}
const handleGuideToolClick = (tool: string) => {
if (tool === '清除') {
selectedGuidePoi.value = null
}
}
// 进入 3D 室内模式
@@ -364,6 +415,7 @@ const handleEnter3DMode = () => {
indoorView.value = 'overview'
is3DMode.value = true
guideOutdoorState.value = 'home'
selectedGuidePoi.value = null
}
const handleModeChange = (mode: '2d' | '3d') => {
@@ -372,6 +424,7 @@ const handleModeChange = (mode: '2d' | '3d') => {
if (mode === '2d') {
indoorView.value = 'overview'
guideOutdoorState.value = 'home'
selectedGuidePoi.value = null
} else {
indoorView.value = 'overview'
}
@@ -379,7 +432,9 @@ const handleModeChange = (mode: '2d' | '3d') => {
const guideStatusLabel = computed(() => {
if (is3DMode.value) {
return indoorView.value === 'overview' ? '室内全馆' : '室内楼层'
if (indoorView.value === 'overview') return '建筑外观'
if (indoorView.value === 'multi') return '室内多层'
return '室内单层'
}
if (guideOutdoorState.value === 'entrance') {
@@ -391,16 +446,26 @@ const guideStatusLabel = computed(() => {
const guideTaskTitle = computed(() => {
if (is3DMode.value) {
return indoorView.value === 'overview' ? '查看完整建筑' : '查看楼层位置'
if (selectedGuidePoi.value) return selectedGuidePoi.value.name
if (indoorView.value === 'overview') return '查看建筑外观'
if (indoorView.value === 'multi') return '查看多层关系'
return '查看单层位置'
}
return '从室外进入馆内'
})
const guideTaskSubtitle = computed(() => {
if (is3DMode.value) {
return indoorView.value === 'overview'
? '切换到楼层后,可查看 POI 与三维位置预览'
: '选择目标设施后,可查看楼层与三维位置预览'
if (selectedGuidePoi.value) {
return `${activeGuideFloor.value} · ${selectedGuidePoi.value.primaryCategoryZh || '位置预览'} · 已高亮`
}
if (indoorView.value === 'overview') {
return '放大模型可自动进入当前楼层'
}
if (indoorView.value === 'multi') {
return '用于比较楼层关系,选择具体楼层可回到单层'
}
return '缩小模型可自动回到建筑外观,也可切换多层展示'
}
return ''
@@ -408,6 +473,7 @@ const guideTaskSubtitle = computed(() => {
const guidePrimaryAction = computed(() => {
if (is3DMode.value) {
if (selectedGuidePoi.value) return '查看相关内容'
return '选择目标地点'
}
@@ -416,6 +482,13 @@ const guidePrimaryAction = computed(() => {
const handleGuidePrimaryAction = () => {
if (is3DMode.value) {
if (selectedGuidePoi.value) {
uni.navigateTo({
url: `/pages/search/index?keyword=${encodeURIComponent(selectedGuidePoi.value.name)}`
})
return
}
uni.navigateTo({
url: '/pages/search/index'
})
@@ -507,7 +580,7 @@ const handleExplainFeaturedClick = async (exhibit: Exhibit) => {
await handleAudioClick(exhibit)
}
const handleExplainLocationClick = (exhibit: Exhibit) => {
const handleExplainLocationClick = async (exhibit: Exhibit) => {
if (!exhibit.poiId) {
uni.showToast({
title: '该讲解暂无三维位置数据',
@@ -516,6 +589,15 @@ const handleExplainLocationClick = (exhibit: Exhibit) => {
return
}
const matchedPoi = await guideUseCase.getPoiById(exhibit.poiId)
if (!matchedPoi) {
uni.showToast({
title: '该讲解位置尚未映射到当前三维导览资源',
icon: 'none'
})
return
}
uni.navigateTo({
url: `/pages/route/detail?facilityId=${encodeURIComponent(exhibit.poiId)}&target=${encodeURIComponent(exhibit.title)}&state=preview`
})