优化导览预览与讲解展示层
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
<template>
|
||||
<GuidePageFrame
|
||||
:active-tab="currentTab"
|
||||
:variant="currentTab === 'explain' ? 'static' : 'overlay'"
|
||||
variant="overlay"
|
||||
:top-tabs-variant="currentTab === 'guide' ? 'segmented' : 'underline'"
|
||||
:top-tabs-top="currentTab === 'guide' ? '108px' : '0'"
|
||||
:top-tabs-top="currentTab === 'guide' ? '80px' : '0'"
|
||||
:show-top-tabs="currentTab === 'guide'"
|
||||
@tab-change="handleTabChange"
|
||||
>
|
||||
<GuideMapShell
|
||||
@@ -13,10 +14,10 @@
|
||||
:mode-layout="'status'"
|
||||
:mode-status="guideStatusLabel"
|
||||
mode-status-tone="glass"
|
||||
search-top="46px"
|
||||
search-top="20px"
|
||||
search-variant="home"
|
||||
:show-mode-row="false"
|
||||
mode-top="104px"
|
||||
mode-top="80px"
|
||||
:map-type="guideMapType"
|
||||
:outdoor-variant="guideOutdoorVariant"
|
||||
:indoor-asset-base-url="indoorNavAssetBaseUrl"
|
||||
@@ -33,7 +34,7 @@
|
||||
show-floor-header
|
||||
:show-layer-mode-toggle="false"
|
||||
:show-floor="is3DMode"
|
||||
tools-top="154px"
|
||||
tools-top="130px"
|
||||
show-zoom-controls
|
||||
zoom-controls-top="560px"
|
||||
show-location-control
|
||||
@@ -184,37 +185,14 @@
|
||||
</GuideMapShell>
|
||||
|
||||
<view v-else-if="currentTab === 'explain'" class="explain-page">
|
||||
<ExplainList
|
||||
:cards="explainExhibits"
|
||||
<ExplainHallSelect
|
||||
:halls="explainHallItems"
|
||||
:loading="explainLoading"
|
||||
:error="explainError"
|
||||
show-browse-all
|
||||
@exhibit-click="handleExplainExhibitClick"
|
||||
@featured-click="handleExplainFeaturedClick"
|
||||
@audio-click="handleAudioClick"
|
||||
@location-click="handleExplainLocationClick"
|
||||
@hall-click="handleExplainHallClick"
|
||||
@browse-all="handleExplainBrowseAll"
|
||||
/>
|
||||
|
||||
<FloatingAudioButton
|
||||
v-if="currentAudio"
|
||||
:is-playing="isAudioPlaying"
|
||||
@click="handleFloatingButtonClick"
|
||||
@back="handleExplainBack"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 音频播放器 -->
|
||||
<AudioPlayer
|
||||
:visible="showAudioPlayer"
|
||||
:audio="currentAudio"
|
||||
:auto-play="true"
|
||||
@update:visible="handleAudioVisibleChange"
|
||||
@play="handleAudioPlay"
|
||||
@pause="handleAudioPause"
|
||||
@ended="handleAudioEnded"
|
||||
@error="handleAudioError"
|
||||
/>
|
||||
</GuidePageFrame>
|
||||
</template>
|
||||
|
||||
@@ -227,9 +205,9 @@ import RoutePlannerPanel from '@/components/navigation/RoutePlannerPanel.vue'
|
||||
import type {
|
||||
RoutePointOption
|
||||
} from '@/components/navigation/RoutePointPicker.vue'
|
||||
import ExplainList, { type Exhibit } from '@/components/explain/ExplainList.vue'
|
||||
import FloatingAudioButton from '@/components/audio/FloatingAudioButton.vue'
|
||||
import AudioPlayer, { type AudioItem } from '@/components/audio/AudioPlayer.vue'
|
||||
import ExplainHallSelect, {
|
||||
type ExplainHallSelectItem
|
||||
} from '@/components/explain/ExplainHallSelect.vue'
|
||||
import {
|
||||
getLastGuideLocationPreviewUrl,
|
||||
isGuideTopTab,
|
||||
@@ -244,15 +222,13 @@ import {
|
||||
import {
|
||||
explainUseCase
|
||||
} from '@/usecases/explainUseCase'
|
||||
import {
|
||||
toExplainCardViewModel
|
||||
} from '@/view-models/explainViewModels'
|
||||
import type {
|
||||
GuideRenderPoi
|
||||
} from '@/domain/guideModel'
|
||||
import type {
|
||||
GuideRouteResult,
|
||||
GuideRouteTarget,
|
||||
MuseumExhibit,
|
||||
MuseumFloor
|
||||
} from '@/domain/museum'
|
||||
|
||||
@@ -355,11 +331,7 @@ const routeSimulationStepText = computed(() => {
|
||||
return hasConnector ? '路线示意 · 跨楼层连接点' : '路线示意 · 同层位置关系'
|
||||
})
|
||||
|
||||
// 音频播放器状态
|
||||
const showAudioPlayer = ref(false)
|
||||
const isAudioPlaying = ref(false)
|
||||
const currentAudio = ref<AudioItem | null>(null)
|
||||
const explainExhibits = ref<Exhibit[]>([])
|
||||
const explainHallItems = ref<ExplainHallSelectItem[]>([])
|
||||
const explainLoading = ref(false)
|
||||
const explainError = ref('')
|
||||
let explainLoadPromise: Promise<void> | null = null
|
||||
@@ -469,7 +441,7 @@ const loadGuideFloors = async () => {
|
||||
}
|
||||
|
||||
const loadExplainExhibits = async () => {
|
||||
if (explainExhibits.value.length) return
|
||||
if (explainHallItems.value.length) return
|
||||
if (explainLoadPromise) return explainLoadPromise
|
||||
|
||||
explainLoading.value = true
|
||||
@@ -477,12 +449,15 @@ const loadExplainExhibits = async () => {
|
||||
|
||||
explainLoadPromise = (async () => {
|
||||
try {
|
||||
const exhibits = await explainUseCase.listExplainExhibits()
|
||||
explainExhibits.value = exhibits.map(toExplainCardViewModel)
|
||||
const [halls, exhibits] = await Promise.all([
|
||||
explainUseCase.listHalls(),
|
||||
explainUseCase.listExplainExhibits()
|
||||
])
|
||||
explainHallItems.value = buildExplainHallItems(halls, exhibits)
|
||||
} catch (error) {
|
||||
console.error('加载讲解内容失败:', error)
|
||||
explainError.value = '讲解内容加载失败,请稍后重试'
|
||||
explainExhibits.value = []
|
||||
explainHallItems.value = []
|
||||
} finally {
|
||||
explainLoading.value = false
|
||||
explainLoadPromise = null
|
||||
@@ -492,6 +467,68 @@ const loadExplainExhibits = async () => {
|
||||
return explainLoadPromise
|
||||
}
|
||||
|
||||
const normalizeExplainKeyword = (value?: string) => (value || '').trim().toLowerCase()
|
||||
|
||||
const buildExplainHallItems = (
|
||||
halls: Awaited<ReturnType<typeof explainUseCase.listHalls>>,
|
||||
exhibits: MuseumExhibit[]
|
||||
): ExplainHallSelectItem[] => {
|
||||
const countsByHallId = new Map<string, number>()
|
||||
const countsByHallName = new Map<string, number>()
|
||||
const keywordsByHallId = new Map<string, Set<string>>()
|
||||
const keywordsByHallName = new Map<string, Set<string>>()
|
||||
|
||||
exhibits.forEach((exhibit) => {
|
||||
const keywordParts = [
|
||||
exhibit.name,
|
||||
exhibit.hallName,
|
||||
exhibit.floorLabel,
|
||||
exhibit.description,
|
||||
exhibit.guideTitle,
|
||||
exhibit.guideText,
|
||||
...(exhibit.tags || [])
|
||||
].filter(Boolean) as string[]
|
||||
|
||||
if (exhibit.hallId) {
|
||||
countsByHallId.set(exhibit.hallId, (countsByHallId.get(exhibit.hallId) || 0) + 1)
|
||||
const keywords = keywordsByHallId.get(exhibit.hallId) || new Set<string>()
|
||||
keywordParts.forEach((part) => keywords.add(part))
|
||||
keywordsByHallId.set(exhibit.hallId, keywords)
|
||||
}
|
||||
|
||||
if (exhibit.hallName) {
|
||||
countsByHallName.set(exhibit.hallName, (countsByHallName.get(exhibit.hallName) || 0) + 1)
|
||||
const keywords = keywordsByHallName.get(exhibit.hallName) || new Set<string>()
|
||||
keywordParts.forEach((part) => keywords.add(part))
|
||||
keywordsByHallName.set(exhibit.hallName, keywords)
|
||||
}
|
||||
})
|
||||
|
||||
return halls.map((hall) => {
|
||||
const explainCount = countsByHallId.get(hall.id)
|
||||
|| countsByHallName.get(hall.name)
|
||||
|| hall.exhibitCount
|
||||
|| 0
|
||||
const keywordParts = [
|
||||
hall.name,
|
||||
hall.floorLabel,
|
||||
hall.description,
|
||||
hall.area,
|
||||
...Array.from(keywordsByHallId.get(hall.id) || []),
|
||||
...Array.from(keywordsByHallName.get(hall.name) || [])
|
||||
].filter(Boolean) as string[]
|
||||
|
||||
return {
|
||||
id: hall.id,
|
||||
name: hall.name,
|
||||
floorLabel: hall.floorLabel,
|
||||
image: hall.image,
|
||||
explainCount,
|
||||
searchText: normalizeExplainKeyword(keywordParts.join(' '))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleGuideSearchTap = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/search/index'
|
||||
@@ -843,131 +880,21 @@ const guideSearchText = computed(() => {
|
||||
return '请输入地点进行搜索'
|
||||
})
|
||||
|
||||
// 音频相关处理
|
||||
const handleAudioClick = async (exhibit: Exhibit) => {
|
||||
if (exhibit.audioUrl) {
|
||||
currentAudio.value = {
|
||||
id: `media-${exhibit.id}`,
|
||||
name: exhibit.title,
|
||||
audioUrl: exhibit.audioUrl,
|
||||
image: exhibit.coverImage,
|
||||
duration: exhibit.audioDuration
|
||||
}
|
||||
showAudioPlayer.value = true
|
||||
return
|
||||
}
|
||||
|
||||
const selection = await explainUseCase.selectAudioForExhibit(exhibit.id)
|
||||
|
||||
if (!selection?.playable || !selection.media?.url) {
|
||||
uni.showToast({
|
||||
title: selection?.unavailableMessage || exhibit.audioStatusText || '该讲解暂无可播放音频',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
currentAudio.value = {
|
||||
id: selection.media.id,
|
||||
name: selection.track?.title || selection.exhibit.name,
|
||||
audioUrl: selection.media.url,
|
||||
image: selection.exhibit.image,
|
||||
duration: selection.media.duration
|
||||
}
|
||||
showAudioPlayer.value = true
|
||||
}
|
||||
|
||||
const handleFloatingButtonClick = () => {
|
||||
if (showAudioPlayer.value) {
|
||||
showAudioPlayer.value = false
|
||||
} else if (currentAudio.value) {
|
||||
showAudioPlayer.value = true
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '请先选择一个讲解内容',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const clearAudioState = () => {
|
||||
showAudioPlayer.value = false
|
||||
isAudioPlaying.value = false
|
||||
currentAudio.value = null
|
||||
}
|
||||
|
||||
const handleAudioVisibleChange = (visible: boolean) => {
|
||||
showAudioPlayer.value = visible
|
||||
if (!visible) {
|
||||
isAudioPlaying.value = false
|
||||
currentAudio.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const handleAudioPlay = (audio: AudioItem) => {
|
||||
console.log('开始播放:', audio)
|
||||
isAudioPlaying.value = true
|
||||
}
|
||||
|
||||
const handleAudioPause = (audio: AudioItem) => {
|
||||
console.log('暂停播放:', audio)
|
||||
isAudioPlaying.value = false
|
||||
}
|
||||
|
||||
const handleAudioEnded = (audio: AudioItem) => {
|
||||
console.log('播放结束:', audio)
|
||||
clearAudioState()
|
||||
}
|
||||
|
||||
const handleAudioError = (_audio: AudioItem | null, message: string) => {
|
||||
console.warn('讲解音频不可播放:', message)
|
||||
clearAudioState()
|
||||
}
|
||||
|
||||
// 讲解页面处理
|
||||
const handleExplainExhibitClick = (exhibit: Exhibit) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/exhibit/detail?id=${exhibit.id}&tab=explain`
|
||||
})
|
||||
}
|
||||
|
||||
const handleExplainFeaturedClick = async (exhibit: Exhibit) => {
|
||||
await handleAudioClick(exhibit)
|
||||
}
|
||||
|
||||
const handleExplainHallClick = (hallId: string) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/hall/detail?id=${encodeURIComponent(hallId)}&tab=explain`
|
||||
})
|
||||
}
|
||||
|
||||
const handleExplainBrowseAll = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/explain/list'
|
||||
})
|
||||
}
|
||||
|
||||
const handleExplainLocationClick = async (exhibit: Exhibit) => {
|
||||
if (!exhibit.poiId) {
|
||||
uni.showToast({
|
||||
title: '该讲解暂无三维位置数据',
|
||||
icon: 'none'
|
||||
})
|
||||
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`
|
||||
})
|
||||
const handleExplainBack = () => {
|
||||
currentTab.value = 'guide'
|
||||
syncTopTabToUrl('guide')
|
||||
loadTopTabData('guide')
|
||||
showGuideMoreMenu.value = false
|
||||
showRoutePlanner.value = false
|
||||
isSimulatingRoute.value = false
|
||||
isPoiCardCollapsed.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1187,7 +1114,7 @@ const handleExplainLocationClick = async (exhibit: Exhibit) => {
|
||||
|
||||
.route-sim-top-pill {
|
||||
position: absolute;
|
||||
top: 154px;
|
||||
top: 126px;
|
||||
left: 50%;
|
||||
max-width: calc(100% - 40px);
|
||||
height: 34px;
|
||||
@@ -1263,7 +1190,7 @@ const handleExplainLocationClick = async (exhibit: Exhibit) => {
|
||||
|
||||
.entrance-tip {
|
||||
position: absolute;
|
||||
top: 154px;
|
||||
top: 126px;
|
||||
left: 30px;
|
||||
width: 168px;
|
||||
height: 42px;
|
||||
|
||||
Reference in New Issue
Block a user