245 lines
6.0 KiB
Vue
245 lines
6.0 KiB
Vue
<template>
|
|
<GuidePageFrame
|
|
active-tab="explain"
|
|
variant="static"
|
|
:show-top-tabs="false"
|
|
show-bottom-nav
|
|
show-back
|
|
back-label="讲解首页"
|
|
@tab-change="handleTopTabChange"
|
|
@back="handleBack"
|
|
>
|
|
<view class="explain-all-page">
|
|
<ExplainList
|
|
:cards="explainExhibits"
|
|
:loading="explainLoading"
|
|
:error="explainError"
|
|
list-title="全部讲解"
|
|
page-mode="all"
|
|
:initial-render-limit="48"
|
|
:page-size="48"
|
|
@exhibit-click="handleExplainExhibitClick"
|
|
@featured-click="handleExplainFeaturedClick"
|
|
@audio-click="handleAudioClick"
|
|
@location-click="handleExplainLocationClick"
|
|
@hall-click="handleExplainHallClick"
|
|
/>
|
|
|
|
<AudioPlayer
|
|
ref="audioPlayerRef"
|
|
:visible="showAudioPlayer"
|
|
:audio="currentAudio"
|
|
:auto-play="false"
|
|
avoid-bottom-nav
|
|
@update:visible="handleAudioVisibleChange"
|
|
@play="handleAudioPlay"
|
|
@pause="handleAudioPause"
|
|
@ended="handleAudioEnded"
|
|
@error="handleAudioError"
|
|
/>
|
|
</view>
|
|
</GuidePageFrame>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref } from 'vue'
|
|
import { onLoad } from '@dcloudio/uni-app'
|
|
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
|
import ExplainList, { type Exhibit } from '@/components/explain/ExplainList.vue'
|
|
import AudioPlayer, { type AudioItem } from '@/components/audio/AudioPlayer.vue'
|
|
import {
|
|
explainUseCase
|
|
} from '@/usecases/explainUseCase'
|
|
import {
|
|
guideUseCase
|
|
} from '@/usecases/guideUseCase'
|
|
import {
|
|
toExplainCardViewModel
|
|
} from '@/view-models/explainViewModels'
|
|
import {
|
|
navigateToGuideTopTab,
|
|
type GuideTopTab
|
|
} from '@/utils/guideTopTabs'
|
|
|
|
const explainExhibits = ref<Exhibit[]>([])
|
|
const explainLoading = ref(false)
|
|
const explainError = ref('')
|
|
const showAudioPlayer = ref(false)
|
|
const isAudioPlaying = ref(false)
|
|
const currentAudio = ref<AudioItem | null>(null)
|
|
const audioPlayerRef = ref<AudioPlayerExpose | null>(null)
|
|
let explainLoadPromise: Promise<void> | null = null
|
|
|
|
interface AudioPlayerExpose {
|
|
play: (audio: AudioItem) => void
|
|
pause: () => void
|
|
}
|
|
|
|
const loadExplainExhibits = async () => {
|
|
if (explainExhibits.value.length) return
|
|
if (explainLoadPromise) return explainLoadPromise
|
|
|
|
explainLoading.value = true
|
|
explainError.value = ''
|
|
|
|
explainLoadPromise = (async () => {
|
|
try {
|
|
const exhibits = await explainUseCase.listExplainExhibits()
|
|
explainExhibits.value = exhibits.map(toExplainCardViewModel)
|
|
} catch (error) {
|
|
console.error('加载全部讲解内容失败:', error)
|
|
explainError.value = '讲解内容加载失败,请稍后重试'
|
|
explainExhibits.value = []
|
|
} finally {
|
|
explainLoading.value = false
|
|
explainLoadPromise = null
|
|
}
|
|
})()
|
|
|
|
return explainLoadPromise
|
|
}
|
|
|
|
onLoad(() => {
|
|
void loadExplainExhibits()
|
|
})
|
|
|
|
const handleBack = () => {
|
|
uni.navigateBack({
|
|
delta: 1,
|
|
fail: () => {
|
|
uni.reLaunch({
|
|
url: '/pages/index/index?tab=explain'
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
const handleTopTabChange = (tab: GuideTopTab) => {
|
|
navigateToGuideTopTab(tab)
|
|
}
|
|
|
|
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 handleAudioClick = async (exhibit: Exhibit) => {
|
|
if (exhibit.audioUrl) {
|
|
const audio: AudioItem = {
|
|
id: `media-${exhibit.id}`,
|
|
name: exhibit.title,
|
|
audioUrl: exhibit.audioUrl,
|
|
image: exhibit.coverImage,
|
|
duration: exhibit.audioDuration
|
|
}
|
|
|
|
const isSameAudio = currentAudio.value?.id === audio.id
|
|
if (showAudioPlayer.value && isSameAudio && isAudioPlaying.value) {
|
|
audioPlayerRef.value?.pause()
|
|
return
|
|
}
|
|
|
|
currentAudio.value = audio
|
|
showAudioPlayer.value = true
|
|
audioPlayerRef.value?.play(audio)
|
|
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
|
|
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 = () => {
|
|
isAudioPlaying.value = true
|
|
}
|
|
|
|
const handleAudioPause = () => {
|
|
isAudioPlaying.value = false
|
|
}
|
|
|
|
const handleAudioEnded = () => {
|
|
clearAudioState()
|
|
}
|
|
|
|
const handleAudioError = (_audio: AudioItem | null, message: string) => {
|
|
console.warn('全部讲解音频不可播放:', message)
|
|
clearAudioState()
|
|
}
|
|
|
|
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`
|
|
})
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.explain-all-page {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: #edf0ea;
|
|
}
|
|
</style>
|