@@ -172,10 +172,7 @@ import {
|
||||
toExplainDetailPageViewModel,
|
||||
type ExplainDetailPageViewModel
|
||||
} from '@/view-models/explainViewModels'
|
||||
import type {
|
||||
AudioPlayTargetType,
|
||||
GuideAudioGender
|
||||
} from '@/domain/museum'
|
||||
import type { GuideAudioGender } from '@/domain/museum'
|
||||
import type {
|
||||
AudioLanguage
|
||||
} from '@/repositories/AudioPlayInfoRepository'
|
||||
@@ -222,12 +219,11 @@ const activeTopTab = ref<GuideTopTab>('explain')
|
||||
const retryingAudio = ref(false)
|
||||
const languageSwitchLoading = ref(false)
|
||||
const selectedAudioLanguage = ref<AudioLanguage>('zh-CN')
|
||||
const selectedAudioGender = ref<GuideAudioGender>('male')
|
||||
const selectedAudioGender = ref<GuideAudioGender>('female')
|
||||
let languageSwitchSequence = 0
|
||||
const detailEntryRequest = ref<{
|
||||
exhibitId: string
|
||||
targetType?: AudioPlayTargetType
|
||||
targetId?: string
|
||||
stopId: string
|
||||
hallId?: string
|
||||
hallName?: string
|
||||
floorId?: string
|
||||
@@ -255,9 +251,17 @@ const isAudioLanguage = (value: unknown): value is AudioLanguage => (
|
||||
value === 'zh-CN' || value === 'yue-HK' || value === 'en-US'
|
||||
)
|
||||
|
||||
const detailAudioVersion = computed<'standard' | 'extended'>(() => (
|
||||
exhibit.value.audio.version === 'extended' ? 'extended' : 'standard'
|
||||
))
|
||||
const activeDetailAudioOptions = computed(() => (
|
||||
(exhibit.value.audio.audioOptions || []).filter((option) => (
|
||||
!option.version || option.version === detailAudioVersion.value
|
||||
))
|
||||
))
|
||||
const supportedDetailLanguages = computed(() => (
|
||||
resolveGuideAudioLanguages(
|
||||
exhibit.value.audio.audioOptions,
|
||||
activeDetailAudioOptions.value,
|
||||
exhibit.value.audio.supportedLanguages
|
||||
).filter(isAudioLanguage)
|
||||
))
|
||||
@@ -316,7 +320,7 @@ const heroSubtitle = computed(() => [
|
||||
detailMeta.value
|
||||
].filter(Boolean).join(' · '))
|
||||
const selectedDetailAudioOption = computed(() => resolveGuideAudioOption(
|
||||
exhibit.value.audio.audioOptions,
|
||||
activeDetailAudioOptions.value,
|
||||
selectedAudioLanguage.value,
|
||||
selectedAudioGender.value
|
||||
))
|
||||
@@ -325,7 +329,7 @@ const currentDetailAudioGender = computed(() => (
|
||||
|| resolveGuideAudioGender(selectedAudioLanguage.value, selectedAudioGender.value)
|
||||
))
|
||||
const canSwitchDetailAudioVoice = computed(() => canToggleGuideAudioGender(
|
||||
exhibit.value.audio.audioOptions,
|
||||
activeDetailAudioOptions.value,
|
||||
selectedAudioLanguage.value
|
||||
))
|
||||
const detailAudioVoiceLabel = computed(() => {
|
||||
@@ -337,21 +341,18 @@ const selectedDetailAudioAvailable = computed(() => (
|
||||
|| exhibit.value.audio.status === 'playable'
|
||||
))
|
||||
const currentAudioTarget = computed(() => ({
|
||||
targetType: exhibit.value.audio.playTargetType || 'ITEM',
|
||||
targetId: exhibit.value.audio.playTargetId || exhibit.value.id,
|
||||
stopId: exhibit.value.audio.stopId || exhibit.value.id,
|
||||
lang: selectedAudioLanguage.value,
|
||||
channelCode: selectedDetailAudioOption.value?.channelCode
|
||||
}))
|
||||
const isCurrentDetailAudio = computed(() => globalAudioPlayer.isCurrentSource(currentAudioTarget.value))
|
||||
const isCurrentDetailAudioTarget = computed(() => {
|
||||
const source = globalAudioPlayer.currentSource.value
|
||||
const targetType = exhibit.value.audio.playTargetType || 'ITEM'
|
||||
const targetId = exhibit.value.audio.playTargetId || exhibit.value.id
|
||||
const stopId = exhibit.value.audio.stopId || exhibit.value.id
|
||||
|
||||
return Boolean(
|
||||
globalAudioPlayer.currentAudio.value
|
||||
&& source?.targetType === targetType
|
||||
&& source.targetId === targetId
|
||||
&& (source?.stopId === stopId || (source?.targetType === 'STOP' && source.targetId === stopId))
|
||||
)
|
||||
})
|
||||
let detailAudioClosedOnExit = false
|
||||
@@ -418,8 +419,8 @@ const isCurrentDetailAudioError = computed(() => {
|
||||
const source = globalAudioPlayer.currentSource.value
|
||||
return Boolean(
|
||||
globalAudioPlayer.error.value
|
||||
&& source?.targetType === currentAudioTarget.value.targetType
|
||||
&& source.targetId === currentAudioTarget.value.targetId
|
||||
&& (source?.stopId === currentAudioTarget.value.stopId
|
||||
|| (source?.targetType === 'STOP' && source.targetId === currentAudioTarget.value.stopId))
|
||||
)
|
||||
})
|
||||
const isDetailAudioLoading = computed(() => (
|
||||
@@ -451,25 +452,8 @@ const applyDetailText = (lang: AudioLanguage, text?: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const buildTextSourceFromViewModel = (viewModel: ExplainDetailPageViewModel) => ({
|
||||
id: viewModel.id,
|
||||
name: viewModel.title,
|
||||
hallId: viewModel.hallId,
|
||||
hallName: viewModel.hallName,
|
||||
floorId: viewModel.floorId,
|
||||
floorLabel: viewModel.floorLabel,
|
||||
image: viewModel.coverImages[0],
|
||||
description: viewModel.summary,
|
||||
guideText: viewModel.summary,
|
||||
audioLanguage: viewModel.audio.language,
|
||||
audioHasText: viewModel.audio.hasText,
|
||||
audioOptions: viewModel.audio.audioOptions,
|
||||
playTargetType: viewModel.audio.playTargetType,
|
||||
playTargetId: viewModel.audio.playTargetId
|
||||
})
|
||||
|
||||
const loadFullDetailTextForLanguage = async (
|
||||
request: NonNullable<typeof detailEntryRequest.value>,
|
||||
_request: NonNullable<typeof detailEntryRequest.value>,
|
||||
lang: AudioLanguage,
|
||||
preferredViewModel?: ExplainDetailPageViewModel
|
||||
) => {
|
||||
@@ -479,21 +463,15 @@ const loadFullDetailTextForLanguage = async (
|
||||
}
|
||||
|
||||
try {
|
||||
const viewModel = preferredViewModel || toExplainDetailPageViewModel(await explainUseCase.enterExplainDetail({
|
||||
...request,
|
||||
lang
|
||||
}))
|
||||
|
||||
if (viewModel.audio.hasText) {
|
||||
const selection = await explainUseCase.loadExplainDetailText(buildTextSourceFromViewModel(viewModel))
|
||||
if (selection.available) {
|
||||
const nextViewModel = toExplainDetailPageViewModel(selection.exhibit)
|
||||
applyDetailText(lang, nextViewModel.body || nextViewModel.summary)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
applyDetailText(lang, viewModel.body || viewModel.summary)
|
||||
const viewModel = preferredViewModel || exhibit.value
|
||||
const variants = viewModel.audio.textVariants || []
|
||||
const version = viewModel.audio.version || 'standard'
|
||||
const detailText = variants.find((variant) => (
|
||||
variant.version === version && variant.languageCode === lang
|
||||
))?.text || (lang === 'yue-HK'
|
||||
? variants.find((variant) => variant.version === version && variant.languageCode === 'zh-CN')?.text
|
||||
: undefined)
|
||||
applyDetailText(lang, detailText || viewModel.summary)
|
||||
} catch (error) {
|
||||
console.warn('讲解正文加载失败:', lang, error)
|
||||
applyDetailText(lang)
|
||||
@@ -517,8 +495,12 @@ const loadExplainDetail = async (
|
||||
|
||||
heroImageIndex.value = 0
|
||||
exhibit.value = toExplainDetailPageViewModel(exhibitData)
|
||||
const resolvedLanguage = resolveSupportedDetailLanguage(lang)
|
||||
const recommended = exhibit.value.audio.audioOptions?.find((option) => (
|
||||
option.channelCode === exhibit.value.audio.recommendedTrackCode && option.version === detailAudioVersion.value
|
||||
))
|
||||
const resolvedLanguage = resolveSupportedDetailLanguage(recommended?.languageCode as AudioLanguage || lang)
|
||||
selectedAudioLanguage.value = resolvedLanguage
|
||||
selectedAudioGender.value = recommended?.gender || resolveGuideAudioGender(resolvedLanguage, 'female')
|
||||
|
||||
if (resolvedLanguage !== lang) {
|
||||
replaceDetailRouteLanguage(resolvedLanguage)
|
||||
@@ -532,28 +514,22 @@ const loadExplainDetail = async (
|
||||
|
||||
onLoad(async (options: any = {}) => {
|
||||
detailAudioClosedOnExit = false
|
||||
selectedAudioGender.value = 'male'
|
||||
selectedAudioGender.value = 'female'
|
||||
const tab = Array.isArray(options.tab) ? options.tab[0] : options.tab
|
||||
if (isGuideTopTab(tab)) {
|
||||
activeTopTab.value = tab
|
||||
}
|
||||
|
||||
const exhibitId = Array.isArray(options.id) ? options.id[0] : options.id
|
||||
if (!exhibitId) {
|
||||
const rawStopId = Array.isArray(options.stopId) ? options.stopId[0] : options.stopId
|
||||
const rawExhibitId = Array.isArray(options.id) ? options.id[0] : options.id
|
||||
// New detail links are keyed by stopId. id remains optional so bookmarked
|
||||
// historical routes and surrounding page metadata can still be preserved.
|
||||
const stopId = rawStopId ? String(rawStopId) : String(rawExhibitId || '')
|
||||
const exhibitId = rawExhibitId ? String(rawExhibitId) : stopId
|
||||
if (!stopId) {
|
||||
detailState.value = 'missing'
|
||||
return
|
||||
}
|
||||
|
||||
const rawTargetType = Array.isArray(options.targetType) ? options.targetType[0] : options.targetType
|
||||
const targetType: AudioPlayTargetType | undefined = rawTargetType === 'ITEM'
|
||||
? 'ITEM'
|
||||
: rawTargetType === 'STOP' || rawTargetType === 'GUIDE_STOP'
|
||||
? 'STOP'
|
||||
: undefined
|
||||
const rawTargetId = Array.isArray(options.targetId) ? options.targetId[0] : options.targetId
|
||||
const targetId = rawTargetType === 'GUIDE_STOP'
|
||||
? String(exhibitId)
|
||||
: rawTargetId ? String(rawTargetId) : undefined
|
||||
const rawLang = Array.isArray(options.lang) ? options.lang[0] : options.lang
|
||||
const lang = typeof rawLang === 'string'
|
||||
? normalizeGuideAudioLanguage(rawLang)
|
||||
@@ -562,8 +538,7 @@ onLoad(async (options: any = {}) => {
|
||||
try {
|
||||
detailEntryRequest.value = {
|
||||
exhibitId,
|
||||
targetType,
|
||||
targetId,
|
||||
stopId,
|
||||
hallId: Array.isArray(options.hallId) ? options.hallId[0] : options.hallId,
|
||||
hallName: Array.isArray(options.hallName) ? options.hallName[0] : options.hallName,
|
||||
floorId: Array.isArray(options.floorId) ? options.floorId[0] : options.floorId,
|
||||
@@ -684,17 +659,27 @@ const handleLanguageChange = async (lang: AudioLanguage, options: { syncAudio?:
|
||||
applyDetailText(lang)
|
||||
return
|
||||
}
|
||||
|
||||
const exhibitData = await explainUseCase.enterExplainDetail({
|
||||
...request,
|
||||
lang
|
||||
})
|
||||
if (requestSequence !== languageSwitchSequence) return
|
||||
|
||||
heroImageIndex.value = 0
|
||||
const nextViewModel = toExplainDetailPageViewModel(exhibitData)
|
||||
exhibit.value = nextViewModel
|
||||
void loadFullDetailTextForLanguage(request, lang, nextViewModel)
|
||||
const audioOption = resolveGuideAudioOption(
|
||||
activeDetailAudioOptions.value,
|
||||
lang,
|
||||
selectedAudioGender.value
|
||||
)
|
||||
exhibit.value = {
|
||||
...exhibit.value,
|
||||
audio: {
|
||||
...exhibit.value.audio,
|
||||
language: lang,
|
||||
url: audioOption?.playUrl,
|
||||
duration: audioOption?.duration,
|
||||
status: audioOption?.playUrl ? 'playable' : 'unavailable',
|
||||
unavailableReason: audioOption?.playUrl ? undefined : '当前语言暂无语音讲解',
|
||||
hasText: Boolean(exhibit.value.audio.textVariants?.some((variant) => (
|
||||
variant.version === detailAudioVersion.value && (variant.languageCode === lang || (lang === 'yue-HK' && variant.languageCode === 'zh-CN'))
|
||||
)))
|
||||
}
|
||||
}
|
||||
void loadFullDetailTextForLanguage(request, lang, exhibit.value)
|
||||
if (shouldContinueAudio) {
|
||||
// The player must become actionable before resuming with the new language track.
|
||||
languageSwitchLoading.value = false
|
||||
@@ -742,13 +727,11 @@ watch(
|
||||
() => globalAudioPlayer.currentSource.value,
|
||||
(source) => {
|
||||
const lang = source?.lang
|
||||
const targetType = exhibit.value.audio.playTargetType || 'ITEM'
|
||||
const targetId = exhibit.value.audio.playTargetId || exhibit.value.id
|
||||
const stopId = exhibit.value.audio.stopId || exhibit.value.id
|
||||
|
||||
if (
|
||||
isAudioLanguage(lang)
|
||||
&& source?.targetType === targetType
|
||||
&& source.targetId === targetId
|
||||
&& (source?.stopId === stopId || (source?.targetType === 'STOP' && source.targetId === stopId))
|
||||
) {
|
||||
void handleLanguageChange(lang, { syncAudio: false })
|
||||
}
|
||||
@@ -761,12 +744,7 @@ function buildDetailRoute(lang: AudioLanguage = selectedAudioLanguage.value) {
|
||||
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)
|
||||
}
|
||||
params.set('stopId', request?.stopId || exhibit.value.audio.stopId || exhibit.value.id)
|
||||
if (lang) {
|
||||
params.set('lang', lang)
|
||||
}
|
||||
@@ -782,7 +760,7 @@ const toAudioItem = (
|
||||
|
||||
return {
|
||||
id: media.id,
|
||||
name: selection.playInfo?.title || selection.exhibit.name,
|
||||
name: selection.exhibit.name,
|
||||
audioUrl,
|
||||
image: heroImage.value,
|
||||
duration: media.duration,
|
||||
@@ -806,11 +784,10 @@ const refreshCurrentAudioOnce = async (message: string) => {
|
||||
audioDuration: exhibit.value.audio.duration,
|
||||
audioStatus: 'READY',
|
||||
audioLanguage: selectedAudioLanguage.value,
|
||||
audioOptions: exhibit.value.audio.audioOptions,
|
||||
audioOptions: activeDetailAudioOptions.value,
|
||||
guideVersion: detailAudioVersion.value,
|
||||
playTargetType: exhibit.value.audio.playTargetType,
|
||||
playTargetId: exhibit.value.audio.playTargetId
|
||||
}, {
|
||||
refreshPlayInfo: true
|
||||
})
|
||||
|
||||
retryingAudio.value = false
|
||||
@@ -833,11 +810,11 @@ const playDetailAudio = async (audio: AudioItem) => {
|
||||
await globalAudioPlayer.play(audio, {
|
||||
source: {
|
||||
exhibitId: exhibit.value.id,
|
||||
targetType: currentAudioTarget.value.targetType,
|
||||
targetId: currentAudioTarget.value.targetId,
|
||||
stopId: currentAudioTarget.value.stopId,
|
||||
lang: selectedAudioLanguage.value,
|
||||
channelCode: selectedDetailAudioOption.value?.channelCode,
|
||||
voiceGender: currentDetailAudioGender.value,
|
||||
audioOptions: activeDetailAudioOptions.value,
|
||||
title: audio.name,
|
||||
detailRoute: buildDetailRoute()
|
||||
},
|
||||
@@ -883,7 +860,8 @@ const handlePlayAudio = async (options: { forceRefresh?: boolean } = {}) => {
|
||||
audioStatus: exhibit.value.audio.status === 'playable' ? 'READY' : 'MISSING',
|
||||
audioLanguage: selectedAudioLanguage.value,
|
||||
audioUnavailableReason: exhibit.value.audio.unavailableReason,
|
||||
audioOptions: exhibit.value.audio.audioOptions,
|
||||
audioOptions: activeDetailAudioOptions.value,
|
||||
guideVersion: detailAudioVersion.value,
|
||||
playTargetType: exhibit.value.audio.playTargetType,
|
||||
playTargetId: exhibit.value.audio.playTargetId
|
||||
}, {
|
||||
@@ -945,12 +923,31 @@ const fallbackToExplainObjectList = () => {
|
||||
})
|
||||
}
|
||||
|
||||
type ExplainDetailPageStackEntry = {
|
||||
route?: string
|
||||
}
|
||||
|
||||
const returnToExplainObjectList = () => {
|
||||
closeDetailAudioOnExit()
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: fallbackToExplainObjectList
|
||||
})
|
||||
|
||||
// H5 history can omit intermediate uni-app pages after a refresh or deep link.
|
||||
if (typeof window !== 'undefined' && window.location.hash.startsWith('#/')) {
|
||||
fallbackToExplainObjectList()
|
||||
return
|
||||
}
|
||||
|
||||
const pages = getCurrentPages()
|
||||
const previousPage = pages[pages.length - 2] as ExplainDetailPageStackEntry | undefined
|
||||
|
||||
if (previousPage?.route === 'pages/explain/guide-stop-list') {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: fallbackToExplainObjectList
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
fallbackToExplainObjectList()
|
||||
}
|
||||
|
||||
const handleBack = returnToExplainObjectList
|
||||
|
||||
Reference in New Issue
Block a user