实现讲解全局播放器

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

@@ -3,7 +3,7 @@
<view
class="audio-player"
:class="{ 'avoid-bottom-nav': avoidBottomNav }"
v-if="visible && currentAudio"
v-if="visible && displayAudio"
>
<!-- 迷你播放器 (底部固定) -->
<view class="mini-player" @tap="handleExpand">
@@ -11,21 +11,21 @@
<view class="player-cover">
<image
class="cover-image"
:src="currentAudio?.image || EXHIBIT_PLACEHOLDER_IMAGE"
:src="displayAudio?.image || EXHIBIT_PLACEHOLDER_IMAGE"
mode="aspectFill"
/>
</view>
<!-- 播放信息 -->
<view class="player-info">
<text class="player-title">{{ currentAudio?.name || '讲解音频' }}</text>
<text class="player-subtitle" :class="{ error: playbackError }">{{ playerSubtitle }}</text>
<text class="player-title">{{ displayAudio?.name || '讲解音频' }}</text>
<text class="player-subtitle" :class="{ error: displayPlaybackError }">{{ playerSubtitle }}</text>
</view>
<!-- 播放/暂停按钮 -->
<view class="player-controls">
<view class="control-btn play-btn" @tap.stop="togglePlay">
<svg v-if="isPlaying" width="24" height="24" viewBox="0 0 24 24" fill="none">
<svg v-if="displayPlaying" width="24" height="24" viewBox="0 0 24 24" fill="none">
<rect x="6" y="5" width="4" height="14" rx="1" fill="currentColor"/>
<rect x="14" y="5" width="4" height="14" rx="1" fill="currentColor"/>
</svg>
@@ -73,6 +73,12 @@ const props = defineProps<{
audio?: AudioItem | null
autoPlay?: boolean
avoidBottomNav?: boolean
controlled?: boolean
playing?: boolean
loading?: boolean
playbackError?: string
currentTime?: number
durationValue?: number
}>()
const emit = defineEmits<{
@@ -82,15 +88,19 @@ const emit = defineEmits<{
ended: [audio: AudioItem]
timeUpdate: [currentTime: number]
error: [audio: AudioItem | null, message: string]
toggle: []
close: []
expand: []
seek: [percent: number]
}>()
// 状态
const currentAudio = ref<AudioItem | null>(null)
const isPlaying = ref(false)
const currentTime = ref(0)
const localCurrentTime = ref(0)
const duration = ref(0)
const isLoading = ref(false)
const playbackError = ref('')
const localPlaybackError = ref('')
const h5AudioElement = ref<HTMLAudioElement | null>(null)
// 内部音频上下文
@@ -102,22 +112,32 @@ let cleanupH5Audio: (() => void) | null = null
const progressPercent = computed(() => {
const totalDuration = resolvedDuration.value
if (totalDuration === 0) return 0
return (currentTime.value / totalDuration) * 100
return (displayCurrentTime.value / totalDuration) * 100
})
const resolvedDuration = computed(() => {
if (props.controlled) {
if (props.durationValue && props.durationValue > 0) return props.durationValue
return props.audio?.duration || 0
}
if (duration.value > 0) return duration.value
return currentAudio.value?.duration || 0
})
const displayAudio = computed(() => props.controlled ? props.audio || null : currentAudio.value)
const displayPlaying = computed(() => props.controlled ? props.playing === true : isPlaying.value)
const displayLoading = computed(() => props.controlled ? props.loading === true : isLoading.value)
const displayPlaybackError = computed(() => props.controlled ? props.playbackError || '' : localPlaybackError.value)
const displayCurrentTime = computed(() => props.controlled ? props.currentTime || 0 : localCurrentTime.value)
const playerSubtitle = computed(() => {
if (playbackError.value) return playbackError.value
if (isLoading.value) return '音频加载中'
if (!currentAudio.value) return '请选择讲解'
if (displayPlaybackError.value) return displayPlaybackError.value
if (displayLoading.value) return '音频加载中'
if (!displayAudio.value) return '请选择讲解'
if (resolvedDuration.value > 0) {
return `${formatTime(currentTime.value)} / ${formatTime(resolvedDuration.value)}`
return `${formatTime(displayCurrentTime.value)} / ${formatTime(resolvedDuration.value)}`
}
return isPlaying.value ? '正在播放' : '点击播放按钮开始'
return displayPlaying.value ? '正在播放' : '点击播放按钮开始'
})
// 格式化时间
@@ -137,7 +157,7 @@ const initAudio = () => {
innerAudioContext.onPlay(() => {
isPlaying.value = true
isLoading.value = false
playbackError.value = ''
localPlaybackError.value = ''
if (currentAudio.value) {
emit('play', currentAudio.value)
}
@@ -149,21 +169,21 @@ const initAudio = () => {
innerAudioContext.onStop(() => {
isPlaying.value = false
currentTime.value = 0
localCurrentTime.value = 0
})
innerAudioContext.onEnded(() => {
isPlaying.value = false
currentTime.value = 0
localCurrentTime.value = 0
if (currentAudio.value) {
emit('ended', currentAudio.value)
}
})
innerAudioContext.onTimeUpdate(() => {
currentTime.value = innerAudioContext.currentTime
localCurrentTime.value = innerAudioContext.currentTime
duration.value = innerAudioContext.duration || 0
emit('timeUpdate', currentTime.value)
emit('timeUpdate', localCurrentTime.value)
})
innerAudioContext.onError((err: any) => {
@@ -229,7 +249,7 @@ const showToast = (title: string) => {
const handlePlaybackError = (message: string, shouldClose = false) => {
isPlaying.value = false
isLoading.value = false
playbackError.value = message
localPlaybackError.value = message
emit('error', currentAudio.value, message)
showToast(message)
@@ -241,9 +261,9 @@ const handlePlaybackError = (message: string, shouldClose = false) => {
const resetPlaybackState = (clearAudio = true) => {
isPlaying.value = false
isLoading.value = false
currentTime.value = 0
localCurrentTime.value = 0
duration.value = 0
playbackError.value = ''
localPlaybackError.value = ''
if (clearAudio) {
currentAudio.value = null
@@ -260,11 +280,11 @@ const playAudio = async (audio: AudioItem) => {
const isNewAudio = currentAudio.value?.id !== audio.id
currentAudio.value = audio
playbackError.value = ''
localPlaybackError.value = ''
isLoading.value = true
if (isNewAudio) {
currentTime.value = 0
localCurrentTime.value = 0
duration.value = audio.duration || 0
}
@@ -284,7 +304,7 @@ const playAudio = async (audio: AudioItem) => {
isPlaying.value = false
if (isAutoplayBlocked(error)) {
playbackError.value = '点击播放按钮开始播放'
localPlaybackError.value = '点击播放按钮开始播放'
return false
}
@@ -356,6 +376,11 @@ const stopAudio = () => {
// 切换播放/暂停
const togglePlay = () => {
if (props.controlled) {
emit('toggle')
return
}
if (isPlaying.value) {
pauseAudio()
} else {
@@ -374,13 +399,13 @@ const seekTo = (percent: number) => {
if (h5AudioElement.value) {
h5AudioElement.value.currentTime = targetTime
currentTime.value = targetTime
localCurrentTime.value = targetTime
return
}
if (innerAudioContext) {
innerAudioContext.seek(targetTime)
currentTime.value = targetTime
localCurrentTime.value = targetTime
}
}
@@ -389,19 +414,30 @@ const handleSeek = (e: any) => {
const rect = e.currentTarget.getBoundingClientRect()
const x = e.detail.x || (e.touches && e.touches[0]?.clientX - rect.left) || 0
const percent = (x / rect.width) * 100
seekTo(Math.max(0, Math.min(100, percent)))
const nextPercent = Math.max(0, Math.min(100, percent))
if (props.controlled) {
emit('seek', nextPercent)
return
}
seekTo(nextPercent)
}
// 展开播放器
const handleExpand = () => {
// 展开逻辑 (如果需要全屏播放器)
console.log('展开播放器')
emit('expand')
}
// 关闭播放器
const handleClose = () => {
if (props.controlled) {
emit('close')
emit('update:visible', false)
return
}
stopAudio()
emit('update:visible', false)
emit('close')
}
const handleH5LoadedMetadata = () => {
@@ -413,7 +449,7 @@ const handleH5LoadedMetadata = () => {
const handleH5Play = () => {
isPlaying.value = true
isLoading.value = false
playbackError.value = ''
localPlaybackError.value = ''
if (currentAudio.value) {
emit('play', currentAudio.value)
}
@@ -429,7 +465,7 @@ const handleH5Pause = () => {
const handleH5Ended = () => {
isPlaying.value = false
isLoading.value = false
currentTime.value = 0
localCurrentTime.value = 0
if (currentAudio.value) {
emit('ended', currentAudio.value)
}
@@ -438,9 +474,9 @@ const handleH5Ended = () => {
const handleH5TimeUpdate = () => {
const audio = h5AudioElement.value
if (!audio) return
currentTime.value = audio.currentTime || 0
localCurrentTime.value = audio.currentTime || 0
duration.value = Number.isFinite(audio.duration) ? audio.duration : duration.value
emit('timeUpdate', currentTime.value)
emit('timeUpdate', localCurrentTime.value)
}
const handleH5Waiting = () => {
@@ -458,6 +494,8 @@ const handleH5AudioError = () => {
// 监听外部 audio 属性变化
watch(() => props.audio, (newAudio) => {
if (props.controlled) return
if (newAudio && props.visible && props.autoPlay && currentAudio.value?.id !== newAudio.id) {
void playAudio(newAudio)
} else if (newAudio && currentAudio.value?.id !== newAudio.id) {
@@ -468,17 +506,23 @@ watch(() => props.audio, (newAudio) => {
// 监听外部 visible 属性变化
watch(() => props.visible, (visible) => {
if (props.controlled) return
if (!visible) {
stopAudio()
}
})
onMounted(() => {
if (props.controlled) return
initH5Audio()
initAudio()
})
onUnmounted(() => {
if (props.controlled) return
cleanupH5Audio?.()
cleanupH5Audio = null
@@ -495,7 +539,7 @@ defineExpose({
stop: stopAudio,
seekTo: seekTo,
isPlaying: () => isPlaying.value,
currentTime: () => currentTime.value,
currentTime: () => localCurrentTime.value,
duration: () => duration.value
})
</script>

View File

@@ -0,0 +1,82 @@
<template>
<AudioPlayer
v-if="isHostVisible"
controlled
:visible="visible"
:audio="currentAudio"
:playing="playing"
:loading="loading"
:playback-error="error"
:current-time="currentTime"
:duration-value="duration"
avoid-bottom-nav
@toggle="handleToggle"
@close="player.close"
@seek="player.seekToPercent"
@expand="handleExpand"
@update:visible="handleVisibleChange"
/>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import AudioPlayer from '@/components/audio/AudioPlayer.vue'
import { useGlobalAudioPlayer } from '@/composables/useGlobalAudioPlayer'
const player = useGlobalAudioPlayer()
const {
currentAudio,
currentSource,
visible,
playing,
loading,
error,
currentTime,
duration,
activeHostId,
hasAudio
} = player
const hostId = player.registerHost()
const isHostVisible = computed(() => (
visible.value
&& hasAudio.value
&& activeHostId.value === hostId
))
const handleToggle = () => {
if (playing.value) {
player.pause()
return
}
void player.resume()
}
const handleVisibleChange = (visible: boolean) => {
if (!visible) {
player.close()
}
}
const handleExpand = () => {
const route = currentSource.value?.detailRoute
if (!route) return
uni.navigateTo({
url: route
})
}
onMounted(() => {
player.activateHost(hostId)
})
onShow(() => {
player.activateHost(hostId)
})
onUnmounted(() => {
player.unregisterHost(hostId)
})
</script>

View File

@@ -32,11 +32,13 @@
<view class="guide-page-frame-body">
<slot></slot>
</view>
<GlobalAudioPlayerHost />
</view>
</template>
<script setup lang="ts">
import GuideTopTabs from '@/components/navigation/GuideTopTabs.vue'
import GlobalAudioPlayerHost from '@/components/audio/GlobalAudioPlayerHost.vue'
import type { GuideTopTab } from '@/utils/guideTopTabs'
withDefaults(defineProps<{

View File

@@ -0,0 +1,304 @@
import { computed, ref } from 'vue'
import type { AudioItem } from '@/components/audio/AudioPlayer.vue'
import type { AudioLanguage } from '@/repositories/AudioPlayInfoRepository'
import type { AudioPlayTargetType } from '@/domain/museum'
export interface GlobalAudioSource {
exhibitId?: string
targetType?: AudioPlayTargetType
targetId?: string
lang?: AudioLanguage | string
detailRoute?: string
title?: string
}
export type GlobalAudioRetryHandler = (message: string) => Promise<AudioItem | null>
export interface GlobalAudioPlayOptions {
source?: GlobalAudioSource
retryOnError?: GlobalAudioRetryHandler
retryAttempt?: boolean
}
const currentAudio = ref<AudioItem | null>(null)
const currentSource = ref<GlobalAudioSource | null>(null)
const visible = ref(false)
const playing = ref(false)
const loading = ref(false)
const error = ref('')
const currentTime = ref(0)
const duration = ref(0)
const activeHostId = ref('')
let audioElement: HTMLAudioElement | null = null
let retryOnError: GlobalAudioRetryHandler | null = null
let retrying = false
let retryUsed = false
let stopping = false
let hostSequence = 0
const showToast = (title: string) => {
if (typeof uni !== 'undefined') {
uni.showToast({
title,
icon: 'none'
})
}
}
const isAutoplayBlocked = (err: unknown) => {
const name = typeof err === 'object' && err && 'name' in err
? String((err as { name?: unknown }).name)
: ''
const message = err instanceof Error ? err.message : String(err || '')
return name === 'NotAllowedError' || /user didn't interact|not allowed|gesture/i.test(message)
}
const syncAudioDuration = () => {
if (!audioElement) return
duration.value = Number.isFinite(audioElement.duration)
? audioElement.duration
: currentAudio.value?.duration || 0
}
const ensureAudioElement = () => {
if (audioElement || typeof window === 'undefined' || !window.Audio) return audioElement
const audio = new window.Audio()
audio.preload = 'metadata'
audio.addEventListener('loadedmetadata', syncAudioDuration)
audio.addEventListener('play', () => {
playing.value = true
loading.value = false
error.value = ''
})
audio.addEventListener('pause', () => {
if (!stopping) {
playing.value = false
}
})
audio.addEventListener('ended', () => {
handleEnded()
})
audio.addEventListener('timeupdate', () => {
currentTime.value = audio.currentTime || 0
syncAudioDuration()
})
audio.addEventListener('waiting', () => {
loading.value = true
})
audio.addEventListener('canplay', () => {
loading.value = false
})
audio.addEventListener('error', () => {
if (!stopping && currentAudio.value?.audioUrl) {
void handleError('音频加载失败,当前提供图文讲解。')
}
})
audioElement = audio
return audioElement
}
const resetState = () => {
currentAudio.value = null
currentSource.value = null
visible.value = false
playing.value = false
loading.value = false
error.value = ''
currentTime.value = 0
duration.value = 0
retryOnError = null
retrying = false
retryUsed = false
}
const stopAudioElement = () => {
if (!audioElement) return
stopping = true
audioElement.pause()
audioElement.removeAttribute('src')
audioElement.load()
const resetStopping = () => {
stopping = false
}
if (typeof window !== 'undefined') {
window.setTimeout(resetStopping, 0)
return
}
resetStopping()
}
const play = async (audio: AudioItem, options: GlobalAudioPlayOptions = {}) => {
if (!audio.audioUrl) {
handleError('当前语言暂无语音讲解。')
return false
}
const element = ensureAudioElement()
if (!element) {
handleError('当前环境暂不支持音频播放。')
return false
}
const isNewAudio = currentAudio.value?.id !== audio.id
if (!options.retryAttempt) {
retryUsed = false
}
currentAudio.value = audio
currentSource.value = options.source || currentSource.value
retryOnError = options.retryOnError || retryOnError
visible.value = true
loading.value = true
error.value = ''
if (isNewAudio) {
currentTime.value = 0
duration.value = audio.duration || 0
}
if (element.getAttribute('src') !== audio.audioUrl) {
element.src = audio.audioUrl
element.load()
}
try {
await element.play()
return true
} catch (err) {
loading.value = false
playing.value = false
if (isAutoplayBlocked(err)) {
error.value = '点击播放按钮开始播放'
return false
}
await handleError('音频暂时无法播放,当前提供图文讲解。')
return false
}
}
const pause = () => {
audioElement?.pause()
playing.value = false
}
const resume = () => {
if (!currentAudio.value) return Promise.resolve(false)
return play(currentAudio.value, {
source: currentSource.value || undefined,
retryOnError: retryOnError || undefined
})
}
const stop = () => {
stopAudioElement()
resetState()
}
const close = () => {
stop()
}
function handleEnded() {
stopAudioElement()
resetState()
}
async function handleError(message: string) {
error.value = message
playing.value = false
loading.value = false
if (retryOnError && !retrying && !retryUsed) {
retryUsed = true
retrying = true
const retryAudio = await retryOnError(message).catch((err) => {
console.warn('讲解音频刷新播放信息失败:', err)
return null
})
retrying = false
if (retryAudio?.audioUrl) {
return play(retryAudio, {
source: currentSource.value || undefined,
retryOnError: retryOnError || undefined,
retryAttempt: true
})
}
}
showToast(message || '讲解音频播放失败')
stop()
return false
}
const seekToPercent = (percent: number) => {
const totalDuration = duration.value || currentAudio.value?.duration || 0
if (!audioElement || totalDuration <= 0) return
const targetTime = Math.max(0, Math.min(100, percent)) / 100 * totalDuration
audioElement.currentTime = targetTime
currentTime.value = targetTime
}
const registerHost = () => {
hostSequence += 1
const hostId = `global-audio-host-${hostSequence}`
activeHostId.value = hostId
return hostId
}
const activateHost = (hostId: string) => {
activeHostId.value = hostId
}
const unregisterHost = (hostId: string) => {
if (activeHostId.value === hostId) {
activeHostId.value = ''
}
}
const isCurrentSource = (source: Pick<GlobalAudioSource, 'targetType' | 'targetId' | 'lang'>) => {
if (!currentAudio.value || !currentSource.value) return false
return Boolean(
source.targetType
&& source.targetId
&& currentSource.value.targetType === source.targetType
&& currentSource.value.targetId === source.targetId
&& (!source.lang || !currentSource.value.lang || currentSource.value.lang === source.lang)
)
}
export const useGlobalAudioPlayer = () => ({
currentAudio,
currentSource,
visible,
playing,
loading,
error,
currentTime,
duration,
activeHostId,
hasAudio: computed(() => Boolean(currentAudio.value)),
play,
pause,
resume,
stop,
close,
handleEnded,
handleError,
seekToPercent,
registerHost,
activateHost,
unregisterHost,
isCurrentSource
})

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

View File

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

View File

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