同步 sgs-frontend-mobile 源码
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-27 11:26:01 +08:00
parent 575dca430f
commit 9ea1bfab71
181 changed files with 24395 additions and 5212 deletions

View File

@@ -4,8 +4,8 @@ import type {
ExplainGuideStop,
ExplainGuideStopPage,
ExplainTrack,
GuideAudioGender,
MediaAsset,
MuseumAudioOption,
MuseumExhibit,
MuseumHall,
SearchIndexItem
@@ -32,6 +32,10 @@ import {
import {
normalizeExplainDetailTargetFromGuideStop
} from '@/domain/explainDetailTarget'
import {
resolveGuideAudioLanguages,
resolveGuideAudioOption
} from '@/domain/guideAudioOptions'
export interface ExplainAudioSelection {
exhibit: MuseumExhibit
@@ -116,14 +120,16 @@ export class ExplainUseCase {
return {
targetType: request.targetType,
targetId: request.targetId,
lang
lang,
lightweight: true
}
}
return {
targetType: 'ITEM',
targetId: request.exhibitId,
lang
lang,
lightweight: true
}
}
@@ -156,36 +162,14 @@ export class ExplainUseCase {
return '中文'
}
private audioOptionForLanguage(
exhibit: MuseumExhibit,
language: AudioLanguage
): MuseumAudioOption | undefined {
const options = exhibit.audioOptions?.filter((option) => (
option.languageCode === language && Boolean(option.audioUrl?.trim())
)) || []
return options.find((option) => option.isDefault) || options[0]
}
private applyLanguageVariant(exhibit: MuseumExhibit, language: AudioLanguage): MuseumExhibit {
const variant = exhibit.audioVariants?.[language]
const audioOption = this.audioOptionForLanguage(exhibit, language)
const hasAudioOptions = Boolean(exhibit.audioOptions?.length)
const mandarinText = exhibit.audioVariants?.['zh-CN']?.text
|| exhibit.guideText
|| exhibit.description
const supportedLanguages = exhibit.supportedLanguages?.length
? exhibit.supportedLanguages
: exhibit.audioOptions?.length
? Array.from(new Set(exhibit.audioOptions.map((option) => option.languageCode)))
: exhibit.audioVariants ? Object.keys(exhibit.audioVariants) : [language]
const legacyPlayable = (variant?.playable ?? variant?.available) === true && Boolean(variant?.audioUrl)
const playable = hasAudioOptions ? Boolean(audioOption?.audioUrl) : legacyPlayable
const audioUrl = hasAudioOptions ? audioOption?.audioUrl : variant?.audioUrl
const audioDuration = hasAudioOptions ? audioOption?.duration : variant?.audioDuration
const audioStatus = hasAudioOptions
? playable ? 'READY' : 'MISSING'
: variant?.audioStatus || (playable ? 'READY' : 'MISSING')
: exhibit.audioVariants ? Object.keys(exhibit.audioVariants) : [language]
if (!variant) {
const label = this.languageLabel(language)
@@ -195,45 +179,32 @@ export class ExplainUseCase {
return {
...exhibit,
guideText,
audioUrl,
audioDuration,
audioUrl: undefined,
audioDuration: undefined,
audioLanguage: language,
audioHasText: language === 'yue-HK' && Boolean(mandarinText),
audioAvailable: playable,
audioStatus,
audioUnavailableReason: playable ? undefined : `当前讲解暂无${label}音频`,
audioChannelCode: audioOption?.channelCode,
audioVoiceGender: audioOption?.gender,
audioVoiceDisplayName: audioOption?.displayName,
audioAvailable: false,
audioStatus: 'MISSING',
audioUnavailableReason: `当前讲解暂无${label}音频`,
supportedLanguages
}
}
const textAvailable = variant.textAvailable ?? (variant.hasText === true || Boolean(variant.text))
const guideText = variant.text
|| (language === 'yue-HK' ? mandarinText : undefined)
|| `${this.languageLabel(language)}讲解词暂未配置。`
return {
...exhibit,
guideTitle: variant.title || exhibit.guideTitle,
guideText,
audioUrl: playable ? audioUrl : undefined,
audioDuration,
guideText: language === 'yue-HK'
? mandarinText || '当前讲解词暂未配置。'
: variant.text || `${this.languageLabel(language)}讲解词暂未配置。`,
audioUrl: variant.audioUrl,
audioDuration: variant.audioDuration,
audioLanguage: language,
audioHasText: textAvailable,
audioText: variant.text || undefined,
audioTextLength: variant.textLength,
audioTextHash: variant.textHash,
audioNarrationTier: variant.narrationTier,
audioAvailable: playable,
audioStatus,
audioUnavailableReason: playable
audioHasText: language === 'yue-HK' ? Boolean(mandarinText) : variant.hasText === true,
audioAvailable: variant.available === true && Boolean(variant.audioUrl),
audioStatus: variant.available && variant.audioUrl ? 'READY' : 'MISSING',
audioUnavailableReason: variant.available && variant.audioUrl
? undefined
: audioReasonToText(variant.reason || 'NO_PUBLISHED_AUDIO'),
audioChannelCode: audioOption?.channelCode,
audioVoiceGender: audioOption?.gender,
audioVoiceDisplayName: audioOption?.displayName,
: `当前讲解暂无${this.languageLabel(language)}音频`,
supportedLanguages
}
}
@@ -249,39 +220,15 @@ export class ExplainUseCase {
: undefined
const description = stopInfo.description || fallback?.description || '该讲解暂无简介。'
const audioTarget = this.resolveStopInfoAudioTarget(stopInfo)
const audioVariants = Object.fromEntries(Object.values(stopInfo.languageVariants).map((variant) => [variant.lang, {
title: stopInfo.title,
text: variant.text,
textAvailable: variant.textAvailable,
textLength: variant.textLength,
textHash: variant.textHash,
audioUrl: variant.playUrl,
audioDuration: variant.duration,
format: variant.format,
audioId: variant.audioId,
narrationTier: variant.narrationTier,
hasText: variant.hasText,
enabled: variant.enabled,
playable: variant.playable,
available: variant.playable,
audioStatus: variant.audioStatus,
fallback: variant.fallback,
reason: variant.reason
}]))
const audioOptions: MuseumAudioOption[] = (stopInfo.audioOptions || []).map((option) => ({
channelCode: option.channelCode,
displayName: option.displayName,
languageCode: option.languageCode,
languageName: option.languageName,
gender: option.gender,
audioUrl: option.playUrl,
duration: option.duration,
format: option.format,
isDefault: option.isDefault,
sortOrder: option.sortOrder
}))
// Product defaults Chinese and English to male even if the management default is female.
const audioOption = resolveGuideAudioOption(stopInfo.audioOptions, stopInfo.lang, 'male')
const audioAvailable = Boolean(audioOption?.playUrl) || stopInfo.audioStatus === 'READY'
const supportedLanguages = resolveGuideAudioLanguages(
stopInfo.audioOptions,
stopInfo.supportedLanguages
)
const exhibit: MuseumExhibit = {
return {
...(fallback || {}),
id: fallback?.id || linkedPrimary?.id || stopInfo.targetId,
name: stopInfo.title || fallback?.name || linkedPrimary?.name || '讲解内容',
@@ -302,15 +249,21 @@ export class ExplainUseCase {
tags: fallback?.tags,
guideTitle: stopInfo.title || fallback?.guideTitle,
guideText: stopInfo.description || fallback?.guideText || fallback?.description,
audioUrl: undefined,
audioDuration: undefined,
audioUrl: audioOption?.playUrl,
audioDuration: audioOption?.duration,
audioLanguage: stopInfo.lang,
audioHasText: stopInfo.hasText,
audioAvailable: false,
audioStatus: stopInfo.audioStatus,
supportedLanguages: stopInfo.supportedLanguages,
audioVariants,
audioOptions,
audioText: undefined,
audioTextLength: undefined,
audioTextHash: undefined,
audioNarrationTier: undefined,
audioUnavailableReason: audioAvailable
? undefined
: audioReasonToText(stopInfo.reason || (stopInfo.audioStatus === 'MISSING' ? 'NO_PUBLISHED_AUDIO' : undefined)),
audioAvailable,
audioStatus: audioAvailable ? 'READY' : stopInfo.audioStatus,
supportedLanguages,
audioOptions: stopInfo.audioOptions,
imageStatus: stopInfo.imageStatus,
imageSource: stopInfo.imageSource,
galleryUrls: stopInfo.imageStatus === 'READY' ? stopInfo.galleryUrls : [],
@@ -323,8 +276,6 @@ export class ExplainUseCase {
playTargetType: audioTarget.targetType,
playTargetId: audioTarget.targetId
}
return this.applyLanguageVariant(exhibit, stopInfo.lang)
}
private applyPlayInfo(
@@ -497,33 +448,6 @@ export class ExplainUseCase {
throw stopInfoResult.error || new Error('讲解详情加载失败')
}
selectExplainDetailLanguage(exhibit: MuseumExhibit, language: AudioLanguage) {
return this.applyLanguageVariant(exhibit, language)
}
selectExplainDetailAudioOption(exhibit: MuseumExhibit, channelCode: string): MuseumExhibit {
const language = exhibit.audioLanguage as AudioLanguage | undefined
const audioOption = exhibit.audioOptions?.find((option) => (
option.channelCode === channelCode
&& option.languageCode === language
&& Boolean(option.audioUrl?.trim())
))
if (!audioOption) return exhibit
return {
...exhibit,
audioUrl: audioOption.audioUrl,
audioDuration: audioOption.duration,
audioLanguage: audioOption.languageCode,
audioAvailable: true,
audioStatus: 'READY',
audioUnavailableReason: undefined,
audioChannelCode: audioOption.channelCode,
audioVoiceGender: audioOption.gender,
audioVoiceDisplayName: audioOption.displayName
}
}
async loadExplainDetailText(exhibit: MuseumExhibit): Promise<ExplainTextSelection> {
const targetType = exhibit.playTargetType || 'ITEM'
const targetId = exhibit.playTargetId || exhibit.id
@@ -723,17 +647,48 @@ export class ExplainUseCase {
async selectAudioForExplainDetail(
exhibit: MuseumExhibit,
options: { refreshPlayInfo?: boolean } = {}
options: {
refreshPlayInfo?: boolean
voiceGender?: GuideAudioGender
} = {}
): Promise<ExplainAudioSelection> {
const targetType = exhibit.playTargetType || 'ITEM'
const targetId = exhibit.playTargetId || exhibit.id
const language = (exhibit.audioLanguage as AudioLanguage) || (dataSourceConfig.audioLanguage as AudioLanguage)
const audioOption = options.refreshPlayInfo
? undefined
: resolveGuideAudioOption(exhibit.audioOptions, language, options.voiceGender || 'male')
if ((!options.refreshPlayInfo || exhibit.audioChannelCode) && exhibit.audioUrl?.trim()) {
if (audioOption) {
const audioExhibit: MuseumExhibit = {
...exhibit,
audioUrl: audioOption.playUrl,
audioDuration: audioOption.duration,
audioLanguage: language,
audioAvailable: true,
audioStatus: 'READY'
}
return {
exhibit: audioExhibit,
track: null,
media: {
id: `channel-${audioOption.channelCode}`,
type: 'audio',
url: audioOption.playUrl,
duration: audioOption.duration,
language,
available: true
},
playable: true
}
}
if (!options.refreshPlayInfo && exhibit.audioUrl?.trim()) {
return {
exhibit,
track: null,
media: {
id: `static-${exhibit.audioChannelCode || `${targetType}-${targetId}`}-${exhibit.audioLanguage || dataSourceConfig.audioLanguage}`,
id: `static-${targetType}-${targetId}-${exhibit.audioLanguage || dataSourceConfig.audioLanguage}`,
type: 'audio',
url: exhibit.audioUrl,
duration: exhibit.audioDuration,
@@ -758,7 +713,7 @@ export class ExplainUseCase {
const playInfo = await this.audioPlayInfo.getPlayInfo({
targetType,
targetId,
lang: (exhibit.audioLanguage as AudioLanguage) || (dataSourceConfig.audioLanguage as AudioLanguage),
lang: language,
refresh: options.refreshPlayInfo === true
})