实现讲解全局播放器
This commit is contained in:
@@ -128,27 +128,16 @@
|
||||
<text class="location-text">查看位置</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AudioPlayer
|
||||
ref="audioPlayerRef"
|
||||
:visible="showAudioPlayer"
|
||||
:audio="currentAudio"
|
||||
:auto-play="false"
|
||||
@update:visible="handleAudioVisibleChange"
|
||||
@play="handleAudioPlay"
|
||||
@pause="handleAudioPause"
|
||||
@ended="handleAudioEnded"
|
||||
@error="handleAudioError"
|
||||
/>
|
||||
</view>
|
||||
</GuidePageFrame>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||||
import AudioPlayer, { type AudioItem } from '@/components/audio/AudioPlayer.vue'
|
||||
import type { AudioItem } from '@/components/audio/AudioPlayer.vue'
|
||||
import { useGlobalAudioPlayer } from '@/composables/useGlobalAudioPlayer'
|
||||
import {
|
||||
explainUseCase
|
||||
} from '@/usecases/explainUseCase'
|
||||
@@ -186,16 +175,8 @@ const defaultDetail: ExplainDetailPageViewModel = {
|
||||
relatedItems: []
|
||||
}
|
||||
|
||||
interface AudioPlayerExpose {
|
||||
play: (audio: AudioItem) => void
|
||||
pause: () => void
|
||||
}
|
||||
|
||||
const exhibit = ref<ExplainDetailPageViewModel>(defaultDetail)
|
||||
const audioPlayerRef = ref<AudioPlayerExpose | null>(null)
|
||||
const showAudioPlayer = ref(false)
|
||||
const currentAudio = ref<AudioItem | null>(null)
|
||||
const isPlaying = ref(false)
|
||||
const globalAudioPlayer = useGlobalAudioPlayer()
|
||||
const activeTopTab = ref<GuideTopTab>('explain')
|
||||
const resolvedHallId = ref('')
|
||||
const retryingAudio = ref(false)
|
||||
@@ -231,6 +212,18 @@ const visibleLinkedExhibits = computed(() => (
|
||||
[...(exhibit.value.linkedExhibits || [])]
|
||||
.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
|
||||
))
|
||||
const currentAudioTarget = computed(() => ({
|
||||
targetType: exhibit.value.audio.playTargetType || 'ITEM',
|
||||
targetId: exhibit.value.audio.playTargetId || exhibit.value.id,
|
||||
lang: exhibit.value.audio.language
|
||||
}))
|
||||
const isCurrentDetailAudio = computed(() => globalAudioPlayer.isCurrentSource(currentAudioTarget.value))
|
||||
const isPlaying = computed(() => (
|
||||
isCurrentDetailAudio.value && globalAudioPlayer.playing.value
|
||||
))
|
||||
const showAudioPlayer = computed(() => (
|
||||
globalAudioPlayer.visible.value && globalAudioPlayer.hasAudio.value
|
||||
))
|
||||
const supportedDetailLanguages = computed(() => new Set(
|
||||
(exhibit.value.audio.supportedLanguages || [selectedAudioLanguage.value])
|
||||
.filter(isAudioLanguage)
|
||||
@@ -284,10 +277,9 @@ const resolveHallGuidePoi = async () => {
|
||||
}
|
||||
|
||||
const resetAudioPlayer = () => {
|
||||
audioPlayerRef.value?.pause()
|
||||
showAudioPlayer.value = false
|
||||
currentAudio.value = null
|
||||
isPlaying.value = false
|
||||
if (isCurrentDetailAudio.value) {
|
||||
globalAudioPlayer.close()
|
||||
}
|
||||
retryingAudio.value = false
|
||||
}
|
||||
|
||||
@@ -413,12 +405,92 @@ const handleLoadDetailText = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const buildDetailRoute = () => {
|
||||
const request = detailEntryRequest.value
|
||||
const params = new URLSearchParams({
|
||||
id: request?.exhibitId || exhibit.value.id,
|
||||
tab: activeTopTab.value
|
||||
})
|
||||
if (currentAudioTarget.value.targetType) {
|
||||
params.set('targetType', currentAudioTarget.value.targetType)
|
||||
}
|
||||
if (currentAudioTarget.value.targetId) {
|
||||
params.set('targetId', currentAudioTarget.value.targetId)
|
||||
}
|
||||
if (selectedAudioLanguage.value) {
|
||||
params.set('lang', selectedAudioLanguage.value)
|
||||
}
|
||||
return `/pages/exhibit/detail?${params.toString()}`
|
||||
}
|
||||
|
||||
const toAudioItem = (
|
||||
selection: NonNullable<Awaited<ReturnType<typeof explainUseCase.selectAudioForExplainDetail>>>
|
||||
): AudioItem | null => {
|
||||
if (!selection.playable || !selection.media?.url) return null
|
||||
|
||||
return {
|
||||
id: selection.media.id,
|
||||
name: selection.playInfo?.title || selection.exhibit.name,
|
||||
audioUrl: selection.media.url,
|
||||
image: heroImage.value,
|
||||
duration: selection.media.duration
|
||||
}
|
||||
}
|
||||
|
||||
const refreshCurrentAudioOnce = async (message: string) => {
|
||||
if (retryingAudio.value || exhibit.value.audio.status !== 'playable') return null
|
||||
|
||||
retryingAudio.value = true
|
||||
const selection = await explainUseCase.selectAudioForExplainDetail({
|
||||
id: exhibit.value.id,
|
||||
name: exhibit.value.title,
|
||||
image: heroImage.value,
|
||||
description: exhibit.value.summary,
|
||||
guideText: exhibit.value.summary,
|
||||
audioUrl: exhibit.value.audio.url,
|
||||
audioDuration: exhibit.value.audio.duration,
|
||||
audioStatus: 'READY',
|
||||
audioLanguage: exhibit.value.audio.language,
|
||||
playTargetType: exhibit.value.audio.playTargetType,
|
||||
playTargetId: exhibit.value.audio.playTargetId
|
||||
}, {
|
||||
refreshPlayInfo: true
|
||||
})
|
||||
|
||||
retryingAudio.value = false
|
||||
const retryAudio = toAudioItem(selection)
|
||||
if (retryAudio) return retryAudio
|
||||
|
||||
exhibit.value = {
|
||||
...exhibit.value,
|
||||
audio: {
|
||||
...exhibit.value.audio,
|
||||
status: 'unavailable',
|
||||
url: undefined,
|
||||
unavailableReason: selection.unavailableMessage || message || '音频加载失败,当前提供图文讲解。'
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const handlePlayAudio = async () => {
|
||||
if (isCurrentDetailAudio.value) {
|
||||
if (globalAudioPlayer.playing.value) {
|
||||
globalAudioPlayer.pause()
|
||||
return
|
||||
}
|
||||
|
||||
void globalAudioPlayer.resume()
|
||||
return
|
||||
}
|
||||
|
||||
const selection = exhibit.value.id
|
||||
? await explainUseCase.selectAudioForExplainDetail({
|
||||
id: exhibit.value.id,
|
||||
name: exhibit.value.title,
|
||||
image: heroImage.value,
|
||||
description: exhibit.value.summary,
|
||||
guideText: exhibit.value.summary,
|
||||
audioUrl: exhibit.value.audio.url,
|
||||
audioDuration: exhibit.value.audio.duration,
|
||||
audioStatus: exhibit.value.audio.status === 'playable' ? 'READY' : 'MISSING',
|
||||
@@ -437,99 +509,20 @@ const handlePlayAudio = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
const audio: AudioItem = {
|
||||
id: selection.media.id,
|
||||
name: selection.playInfo?.title || selection.exhibit.name,
|
||||
audioUrl: selection.media.url,
|
||||
image: heroImage.value,
|
||||
duration: selection.media.duration
|
||||
}
|
||||
const audio = toAudioItem(selection)
|
||||
if (!audio) return
|
||||
|
||||
const isSameAudio = currentAudio.value?.id === audio.id
|
||||
if (showAudioPlayer.value && isSameAudio && isPlaying.value) {
|
||||
audioPlayerRef.value?.pause()
|
||||
return
|
||||
}
|
||||
|
||||
showAudioPlayer.value = true
|
||||
currentAudio.value = audio
|
||||
await nextTick()
|
||||
audioPlayerRef.value?.play(audio)
|
||||
}
|
||||
|
||||
const handleAudioVisibleChange = (visible: boolean) => {
|
||||
showAudioPlayer.value = visible
|
||||
if (!visible) {
|
||||
isPlaying.value = false
|
||||
currentAudio.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const handleAudioPlay = () => {
|
||||
retryingAudio.value = false
|
||||
isPlaying.value = true
|
||||
}
|
||||
|
||||
const handleAudioPause = () => {
|
||||
isPlaying.value = false
|
||||
}
|
||||
|
||||
const handleAudioEnded = () => {
|
||||
isPlaying.value = false
|
||||
showAudioPlayer.value = false
|
||||
currentAudio.value = null
|
||||
retryingAudio.value = false
|
||||
}
|
||||
|
||||
const handleAudioError = async (_audio: AudioItem | null, message: string) => {
|
||||
console.warn('详情音频不可播放:', message)
|
||||
isPlaying.value = false
|
||||
|
||||
if (!retryingAudio.value && exhibit.value.audio.status === 'playable') {
|
||||
retryingAudio.value = true
|
||||
const selection = await explainUseCase.selectAudioForExplainDetail({
|
||||
id: exhibit.value.id,
|
||||
name: exhibit.value.title,
|
||||
image: heroImage.value,
|
||||
audioUrl: exhibit.value.audio.url,
|
||||
audioDuration: exhibit.value.audio.duration,
|
||||
audioStatus: 'READY',
|
||||
audioLanguage: exhibit.value.audio.language,
|
||||
playTargetType: exhibit.value.audio.playTargetType,
|
||||
playTargetId: exhibit.value.audio.playTargetId
|
||||
}, {
|
||||
refreshPlayInfo: true
|
||||
})
|
||||
|
||||
if (selection.playable && selection.media?.url) {
|
||||
const retryAudio: AudioItem = {
|
||||
id: selection.media.id,
|
||||
name: selection.playInfo?.title || selection.exhibit.name,
|
||||
audioUrl: selection.media.url,
|
||||
image: heroImage.value,
|
||||
duration: selection.media.duration
|
||||
}
|
||||
|
||||
currentAudio.value = retryAudio
|
||||
showAudioPlayer.value = true
|
||||
await nextTick()
|
||||
audioPlayerRef.value?.play(retryAudio)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
retryingAudio.value = false
|
||||
showAudioPlayer.value = false
|
||||
currentAudio.value = null
|
||||
exhibit.value = {
|
||||
...exhibit.value,
|
||||
audio: {
|
||||
...exhibit.value.audio,
|
||||
status: 'unavailable',
|
||||
url: undefined,
|
||||
unavailableReason: message || '音频加载失败,当前提供图文讲解。'
|
||||
}
|
||||
}
|
||||
await globalAudioPlayer.play(audio, {
|
||||
source: {
|
||||
exhibitId: exhibit.value.id,
|
||||
targetType: currentAudioTarget.value.targetType,
|
||||
targetId: currentAudioTarget.value.targetId,
|
||||
lang: selectedAudioLanguage.value,
|
||||
title: audio.name,
|
||||
detailRoute: buildDetailRoute()
|
||||
},
|
||||
retryOnError: refreshCurrentAudioOnce
|
||||
})
|
||||
}
|
||||
|
||||
const handleNavigate = async () => {
|
||||
@@ -565,8 +558,13 @@ const handleNavigate = async () => {
|
||||
}
|
||||
|
||||
const fallbackToExplainHome = () => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index?tab=explain'
|
||||
uni.redirectTo({
|
||||
url: '/pages/index/index?tab=explain',
|
||||
fail: () => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index?tab=explain'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -153,8 +153,13 @@ const handleBack = () => {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: () => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index?tab=explain'
|
||||
uni.redirectTo({
|
||||
url: '/pages/index/index?tab=explain',
|
||||
fail: () => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index?tab=explain'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -240,8 +240,13 @@ const handleBack = () => {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: () => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index?tab=explain'
|
||||
uni.redirectTo({
|
||||
url: '/pages/index/index?tab=explain',
|
||||
fail: () => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index?tab=explain'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user