实现讲解全局播放器

This commit is contained in:
lyf
2026-07-07 23:35:03 +08:00
parent 1d930fa63a
commit 07beae95d0
7 changed files with 599 additions and 159 deletions

View File

@@ -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'
})
}
})
}