适配H5讲解统一详情接口
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-08-05 14:52:36 +08:00
parent c287ef3a2a
commit 2edb6bd9f2
25 changed files with 946 additions and 1148 deletions

View File

@@ -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