This commit is contained in:
@@ -119,6 +119,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
import type { AudioLanguage as RepositoryAudioLanguage } from '@/repositories/AudioPlayInfoRepository'
|
||||||
export interface AudioItem {
|
export interface AudioItem {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -129,7 +130,7 @@ export interface AudioItem {
|
|||||||
supportedLanguages?: AudioLanguage[]
|
supportedLanguages?: AudioLanguage[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AudioLanguage = 'zh-CN' | 'en-US'
|
export type AudioLanguage = RepositoryAudioLanguage
|
||||||
export type AudioDisplayMode = 'expanded' | 'mini' | 'floating'
|
export type AudioDisplayMode = 'expanded' | 'mini' | 'floating'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -202,7 +203,11 @@ const displayMode = computed<AudioDisplayMode>(() => props.mode || 'mini')
|
|||||||
const closable = computed(() => props.closable !== false)
|
const closable = computed(() => props.closable !== false)
|
||||||
const expandable = computed(() => props.expandable !== false)
|
const expandable = computed(() => props.expandable !== false)
|
||||||
const displayAudioLanguage = computed<AudioLanguage>(() => (
|
const displayAudioLanguage = computed<AudioLanguage>(() => (
|
||||||
displayAudio.value?.language === 'en-US' ? 'en-US' : 'zh-CN'
|
displayAudio.value?.language === 'en-US'
|
||||||
|
? 'en-US'
|
||||||
|
: displayAudio.value?.language === 'yue-HK'
|
||||||
|
? 'yue-HK'
|
||||||
|
: 'zh-CN'
|
||||||
))
|
))
|
||||||
const formattedCurrentTime = computed(() => formatTime(displayCurrentTime.value))
|
const formattedCurrentTime = computed(() => formatTime(displayCurrentTime.value))
|
||||||
const formattedDuration = computed(() => formatTime(resolvedDuration.value))
|
const formattedDuration = computed(() => formatTime(resolvedDuration.value))
|
||||||
@@ -210,7 +215,9 @@ const expandedSubtitle = computed(() => {
|
|||||||
if (displayPlaybackError.value) return displayPlaybackError.value
|
if (displayPlaybackError.value) return displayPlaybackError.value
|
||||||
if (displayLoading.value) return '音频加载中'
|
if (displayLoading.value) return '音频加载中'
|
||||||
if (!displayAudio.value?.audioUrl) return '当前语言暂无语音讲解'
|
if (!displayAudio.value?.audioUrl) return '当前语言暂无语音讲解'
|
||||||
return displayAudioLanguage.value === 'en-US' ? '英文语音讲解' : '中文语音讲解'
|
if (displayAudioLanguage.value === 'en-US') return '英文语音讲解'
|
||||||
|
if (displayAudioLanguage.value === 'yue-HK') return '粤语语音讲解'
|
||||||
|
return '中文语音讲解'
|
||||||
})
|
})
|
||||||
|
|
||||||
// 格式化时间
|
// 格式化时间
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
} from '@/components/audio/AudioPlayer.vue'
|
} from '@/components/audio/AudioPlayer.vue'
|
||||||
import {
|
import {
|
||||||
audioPlayInfoRepository,
|
audioPlayInfoRepository,
|
||||||
|
audioReasonToText,
|
||||||
type AudioLanguage
|
type AudioLanguage
|
||||||
} from '@/repositories/AudioPlayInfoRepository'
|
} from '@/repositories/AudioPlayInfoRepository'
|
||||||
import type { AudioPlayTargetType } from '@/domain/museum'
|
import type { AudioPlayTargetType } from '@/domain/museum'
|
||||||
@@ -276,6 +277,10 @@ const switchLanguage = async (lang: AudioLanguage) => {
|
|||||||
|
|
||||||
const preservedMode = displayMode.value
|
const preservedMode = displayMode.value
|
||||||
pendingLanguage.value = lang
|
pendingLanguage.value = lang
|
||||||
|
stopAudioElement()
|
||||||
|
playing.value = false
|
||||||
|
currentTime.value = 0
|
||||||
|
duration.value = 0
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = ''
|
error.value = ''
|
||||||
|
|
||||||
@@ -311,7 +316,7 @@ const switchLanguage = async (lang: AudioLanguage) => {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
currentTime.value = 0
|
currentTime.value = 0
|
||||||
duration.value = 0
|
duration.value = 0
|
||||||
error.value = '当前语言暂无语音讲解'
|
error.value = audioReasonToText(playInfo.reason || 'NO_PUBLISHED_AUDIO')
|
||||||
showToast(error.value)
|
showToast(error.value)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -341,7 +346,7 @@ const switchLanguage = async (lang: AudioLanguage) => {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
currentTime.value = 0
|
currentTime.value = 0
|
||||||
duration.value = 0
|
duration.value = 0
|
||||||
error.value = '当前语言暂无语音讲解'
|
error.value = '语音服务暂不可用,请稍后重试'
|
||||||
showToast(error.value)
|
showToast(error.value)
|
||||||
return false
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export type DataSourceMode = 'static' | 'api' | 'sdk'
|
export type DataSourceMode = 'static' | 'api' | 'sdk'
|
||||||
export type GuideContentDataSourceMode = 'static' | 'remote' | 'mock'
|
export type GuideContentDataSourceMode = 'static' | 'remote' | 'mock'
|
||||||
|
export type ConfiguredAudioLanguage = 'zh-CN' | 'yue-HK' | 'en-US'
|
||||||
|
|
||||||
const allowedModes = new Set<DataSourceMode>(['static', 'api', 'sdk'])
|
const allowedModes = new Set<DataSourceMode>(['static', 'api', 'sdk'])
|
||||||
const allowedGuideContentModes = new Set<GuideContentDataSourceMode>(['static', 'remote', 'mock'])
|
const allowedGuideContentModes = new Set<GuideContentDataSourceMode>(['static', 'remote', 'mock'])
|
||||||
@@ -27,6 +28,11 @@ const normalizeGuideContentMode = (mode: string | undefined): GuideContentDataSo
|
|||||||
return 'static'
|
return 'static'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeAudioLanguage = (language: string | undefined): ConfiguredAudioLanguage => {
|
||||||
|
if (language === 'en-US' || language === 'yue-HK') return language
|
||||||
|
return 'zh-CN'
|
||||||
|
}
|
||||||
|
|
||||||
const normalizeBoolean = (value: string | undefined) => (
|
const normalizeBoolean = (value: string | undefined) => (
|
||||||
['1', 'true', 'yes', 'on'].includes(value?.trim().toLowerCase() || '')
|
['1', 'true', 'yes', 'on'].includes(value?.trim().toLowerCase() || '')
|
||||||
)
|
)
|
||||||
@@ -74,7 +80,7 @@ export const dataSourceConfig = {
|
|||||||
defaultAudioApiBaseUrl
|
defaultAudioApiBaseUrl
|
||||||
),
|
),
|
||||||
apiBaseUrl: normalizeUrl(import.meta.env.VITE_API_BASE_URL, defaultApiBaseUrl),
|
apiBaseUrl: normalizeUrl(import.meta.env.VITE_API_BASE_URL, defaultApiBaseUrl),
|
||||||
audioLanguage: import.meta.env.VITE_AUDIO_LANGUAGE === 'en-US' ? 'en-US' : 'zh-CN',
|
audioLanguage: normalizeAudioLanguage(import.meta.env.VITE_AUDIO_LANGUAGE),
|
||||||
sgsApiBaseUrl: normalizeUrl(
|
sgsApiBaseUrl: normalizeUrl(
|
||||||
import.meta.env.VITE_SGS_API_BASE_URL || import.meta.env.VITE_API_BASE_URL,
|
import.meta.env.VITE_SGS_API_BASE_URL || import.meta.env.VITE_API_BASE_URL,
|
||||||
defaultApiBaseUrl
|
defaultApiBaseUrl
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ import {
|
|||||||
EXHIBIT_PLACEHOLDER_IMAGE,
|
EXHIBIT_PLACEHOLDER_IMAGE,
|
||||||
HALL_PLACEHOLDER_IMAGE
|
HALL_PLACEHOLDER_IMAGE
|
||||||
} from '@/utils/placeholders'
|
} from '@/utils/placeholders'
|
||||||
|
import {
|
||||||
|
normalizeGuideAudioLanguage,
|
||||||
|
type GuideAudioLanguage
|
||||||
|
} from '@/data/adapters/guideStopInfoAdapter'
|
||||||
|
|
||||||
export interface BackendGuideContent {
|
export interface BackendGuideContent {
|
||||||
id?: string | number | null
|
id?: string | number | null
|
||||||
@@ -251,8 +255,12 @@ const buildTags = (exhibit: BackendExhibit) => Array.from(new Set([
|
|||||||
firstText(exhibit.origin)
|
firstText(exhibit.origin)
|
||||||
].filter(Boolean)))
|
].filter(Boolean)))
|
||||||
|
|
||||||
const normalizeSupportedLanguages = (languages: string[] | null | undefined) => (
|
const normalizeSupportedLanguages = (languages: string[] | null | undefined): GuideAudioLanguage[] => (
|
||||||
Array.isArray(languages) ? languages.map(String).filter(Boolean) : []
|
Array.from(new Set((languages || [])
|
||||||
|
.map(String)
|
||||||
|
.filter((language) => ['zh', 'zh-cn', 'en', 'en-us', 'yue', 'yue-cn', 'yue-hk']
|
||||||
|
.includes(language.trim().toLowerCase()))
|
||||||
|
.map(normalizeGuideAudioLanguage)))
|
||||||
)
|
)
|
||||||
|
|
||||||
const normalizeCatalogAudioStatus = (status?: string | null) => (
|
const normalizeCatalogAudioStatus = (status?: string | null) => (
|
||||||
@@ -496,7 +504,8 @@ export const toBackendMuseumExhibit = (
|
|||||||
const audioUrl = options.includeDetail ? resolveGuideAudioUrl(guide, source) : normalizeSameOriginPublicUrl(firstText(source.audioUrl))
|
const audioUrl = options.includeDetail ? resolveGuideAudioUrl(guide, source) : normalizeSameOriginPublicUrl(firstText(source.audioUrl))
|
||||||
const targetType = normalizeAudioTargetType(guide?.targetType || source.playTargetType)
|
const targetType = normalizeAudioTargetType(guide?.targetType || source.playTargetType)
|
||||||
const targetId = stringifyId(guide?.targetId || source.playTargetId)
|
const targetId = stringifyId(guide?.targetId || source.playTargetId)
|
||||||
const supportedLanguage = source.supportedLanguages?.find((language) => language === 'zh-CN' || language === 'en-US')
|
const supportedLanguages = normalizeSupportedLanguages(source.supportedLanguages)
|
||||||
|
const supportedLanguage = supportedLanguages[0]
|
||||||
const metadataSuggestsAudio = source.hasAudio === true && isAudioReady(source.audioStatus)
|
const metadataSuggestsAudio = source.hasAudio === true && isAudioReady(source.audioStatus)
|
||||||
const audioAvailable = Boolean(audioUrl)
|
const audioAvailable = Boolean(audioUrl)
|
||||||
const sourcePoiId = stringifyId(source.poiId)
|
const sourcePoiId = stringifyId(source.poiId)
|
||||||
@@ -528,7 +537,7 @@ export const toBackendMuseumExhibit = (
|
|||||||
audioHasText: options.includeDetail ? Boolean(guideText) : undefined,
|
audioHasText: options.includeDetail ? Boolean(guideText) : undefined,
|
||||||
audioAvailable: audioAvailable || metadataSuggestsAudio,
|
audioAvailable: audioAvailable || metadataSuggestsAudio,
|
||||||
audioStatus: source.audioStatus || undefined,
|
audioStatus: source.audioStatus || undefined,
|
||||||
supportedLanguages: source.supportedLanguages || undefined,
|
supportedLanguages: supportedLanguages.length ? supportedLanguages : undefined,
|
||||||
audioUnavailableReason: audioAvailable || metadataSuggestsAudio
|
audioUnavailableReason: audioAvailable || metadataSuggestsAudio
|
||||||
? undefined
|
? undefined
|
||||||
: '该讲解暂无已发布音频,当前提供图文讲解。',
|
: '该讲解暂无已发布音频,当前提供图文讲解。',
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
normalizeSameOriginPublicUrl
|
normalizeSameOriginPublicUrl
|
||||||
} from '@/utils/publicUrl'
|
} from '@/utils/publicUrl'
|
||||||
|
|
||||||
|
export type GuideAudioLanguage = 'zh-CN' | 'yue-HK' | 'en-US'
|
||||||
|
|
||||||
export interface BackendGuideStopLinkedExhibit {
|
export interface BackendGuideStopLinkedExhibit {
|
||||||
id?: string | number | null
|
id?: string | number | null
|
||||||
name?: string | null
|
name?: string | null
|
||||||
@@ -97,7 +99,7 @@ export interface GuideStopInfo {
|
|||||||
floorId?: string
|
floorId?: string
|
||||||
mapX?: number
|
mapX?: number
|
||||||
mapY?: number
|
mapY?: number
|
||||||
lang: 'zh-CN' | 'en-US'
|
lang: GuideAudioLanguage
|
||||||
title: string
|
title: string
|
||||||
description?: string
|
description?: string
|
||||||
coverImageUrl?: string
|
coverImageUrl?: string
|
||||||
@@ -109,7 +111,7 @@ export interface GuideStopInfo {
|
|||||||
playTargetId: string
|
playTargetId: string
|
||||||
hasAudio: boolean
|
hasAudio: boolean
|
||||||
hasText: boolean
|
hasText: boolean
|
||||||
supportedLanguages: string[]
|
supportedLanguages: GuideAudioLanguage[]
|
||||||
audioStatus: 'READY' | 'MISSING' | string
|
audioStatus: 'READY' | 'MISSING' | string
|
||||||
reason?: string
|
reason?: string
|
||||||
linkedExhibitCount?: number
|
linkedExhibitCount?: number
|
||||||
@@ -120,7 +122,7 @@ export interface GuideAudioPlayInfo {
|
|||||||
playable: boolean
|
playable: boolean
|
||||||
targetType: AudioPlayTargetType
|
targetType: AudioPlayTargetType
|
||||||
targetId: string
|
targetId: string
|
||||||
lang: 'zh-CN' | 'en-US'
|
lang: GuideAudioLanguage
|
||||||
narrationTier?: 'STANDARD' | 'EXTENDED'
|
narrationTier?: 'STANDARD' | 'EXTENDED'
|
||||||
audioId?: string
|
audioId?: string
|
||||||
title?: string
|
title?: string
|
||||||
@@ -139,7 +141,7 @@ export interface GuideAudioTextInfo {
|
|||||||
available: boolean
|
available: boolean
|
||||||
targetType: AudioPlayTargetType
|
targetType: AudioPlayTargetType
|
||||||
targetId: string
|
targetId: string
|
||||||
lang: 'zh-CN' | 'en-US'
|
lang: GuideAudioLanguage
|
||||||
narrationTier?: 'STANDARD' | 'EXTENDED'
|
narrationTier?: 'STANDARD' | 'EXTENDED'
|
||||||
title?: string
|
title?: string
|
||||||
text?: string
|
text?: string
|
||||||
@@ -166,8 +168,25 @@ const normalizeTargetType = (value: string | null | undefined, fallback: AudioPl
|
|||||||
return normalized === 'ITEM' || normalized === 'STOP' ? normalized : fallback
|
return normalized === 'ITEM' || normalized === 'STOP' ? normalized : fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeLanguage = (value: string | null | undefined): 'zh-CN' | 'en-US' => (
|
export const normalizeGuideAudioLanguage = (
|
||||||
value === 'en-US' ? 'en-US' : 'zh-CN'
|
value: string | null | undefined
|
||||||
|
): GuideAudioLanguage => {
|
||||||
|
const normalized = value?.trim().toLowerCase()
|
||||||
|
if (normalized === 'en' || normalized === 'en-us') return 'en-US'
|
||||||
|
if (normalized === 'yue' || normalized === 'yue-cn' || normalized === 'yue-hk') return 'yue-HK'
|
||||||
|
return 'zh-CN'
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeSupportedLanguages = (languages: string[] | null | undefined): GuideAudioLanguage[] => (
|
||||||
|
Array.from(new Set((languages || [])
|
||||||
|
.map((language) => {
|
||||||
|
const normalized = language?.trim().toLowerCase()
|
||||||
|
if (!['zh', 'zh-cn', 'en', 'en-us', 'yue', 'yue-cn', 'yue-hk'].includes(normalized)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return normalizeGuideAudioLanguage(language)
|
||||||
|
})
|
||||||
|
.filter(Boolean))) as GuideAudioLanguage[]
|
||||||
)
|
)
|
||||||
|
|
||||||
const parseGalleryUrls = (value: BackendGuideStopInfo['galleryUrls'] | BackendGuideStopLinkedExhibit['galleryUrls']) => {
|
const parseGalleryUrls = (value: BackendGuideStopInfo['galleryUrls'] | BackendGuideStopLinkedExhibit['galleryUrls']) => {
|
||||||
@@ -223,7 +242,7 @@ export const toGuideStopInfo = (
|
|||||||
fallback: {
|
fallback: {
|
||||||
targetType: AudioPlayTargetType
|
targetType: AudioPlayTargetType
|
||||||
targetId: string
|
targetId: string
|
||||||
lang: 'zh-CN' | 'en-US'
|
lang: GuideAudioLanguage
|
||||||
}
|
}
|
||||||
): GuideStopInfo => {
|
): GuideStopInfo => {
|
||||||
const targetType = normalizeTargetType(source.targetType, fallback.targetType)
|
const targetType = normalizeTargetType(source.targetType, fallback.targetType)
|
||||||
@@ -246,7 +265,7 @@ export const toGuideStopInfo = (
|
|||||||
floorId: stringifyId(source.floorId) || undefined,
|
floorId: stringifyId(source.floorId) || undefined,
|
||||||
mapX: normalizeNumber(source.mapX),
|
mapX: normalizeNumber(source.mapX),
|
||||||
mapY: normalizeNumber(source.mapY),
|
mapY: normalizeNumber(source.mapY),
|
||||||
lang: normalizeLanguage(source.lang || fallback.lang),
|
lang: normalizeGuideAudioLanguage(source.lang || fallback.lang),
|
||||||
title: source.title?.trim() || '讲解内容',
|
title: source.title?.trim() || '讲解内容',
|
||||||
description: source.description?.trim() || undefined,
|
description: source.description?.trim() || undefined,
|
||||||
coverImageUrl,
|
coverImageUrl,
|
||||||
@@ -258,7 +277,7 @@ export const toGuideStopInfo = (
|
|||||||
playTargetId,
|
playTargetId,
|
||||||
hasAudio: source.hasAudio === true,
|
hasAudio: source.hasAudio === true,
|
||||||
hasText: source.hasText === true,
|
hasText: source.hasText === true,
|
||||||
supportedLanguages: source.supportedLanguages || [],
|
supportedLanguages: normalizeSupportedLanguages(source.supportedLanguages),
|
||||||
audioStatus: source.audioStatus || 'MISSING',
|
audioStatus: source.audioStatus || 'MISSING',
|
||||||
reason: source.reason || undefined,
|
reason: source.reason || undefined,
|
||||||
linkedExhibitCount: normalizeNumber(source.linkedExhibitCount),
|
linkedExhibitCount: normalizeNumber(source.linkedExhibitCount),
|
||||||
@@ -271,7 +290,7 @@ export const toGuideAudioPlayInfo = (
|
|||||||
fallback: {
|
fallback: {
|
||||||
targetType: AudioPlayTargetType
|
targetType: AudioPlayTargetType
|
||||||
targetId: string
|
targetId: string
|
||||||
lang: 'zh-CN' | 'en-US'
|
lang: GuideAudioLanguage
|
||||||
}
|
}
|
||||||
): GuideAudioPlayInfo => {
|
): GuideAudioPlayInfo => {
|
||||||
const targetType = normalizeTargetType(source.targetType, fallback.targetType)
|
const targetType = normalizeTargetType(source.targetType, fallback.targetType)
|
||||||
@@ -281,7 +300,7 @@ export const toGuideAudioPlayInfo = (
|
|||||||
playable: source.playable === true,
|
playable: source.playable === true,
|
||||||
targetType,
|
targetType,
|
||||||
targetId,
|
targetId,
|
||||||
lang: normalizeLanguage(source.lang || fallback.lang),
|
lang: normalizeGuideAudioLanguage(source.lang || fallback.lang),
|
||||||
narrationTier: normalizeNarrationTier(source.narrationTier),
|
narrationTier: normalizeNarrationTier(source.narrationTier),
|
||||||
audioId: stringifyId(source.audioId) || undefined,
|
audioId: stringifyId(source.audioId) || undefined,
|
||||||
title: source.title?.trim() || undefined,
|
title: source.title?.trim() || undefined,
|
||||||
@@ -302,7 +321,7 @@ export const toGuideAudioTextInfo = (
|
|||||||
fallback: {
|
fallback: {
|
||||||
targetType: AudioPlayTargetType
|
targetType: AudioPlayTargetType
|
||||||
targetId: string
|
targetId: string
|
||||||
lang: 'zh-CN' | 'en-US'
|
lang: GuideAudioLanguage
|
||||||
}
|
}
|
||||||
): GuideAudioTextInfo => {
|
): GuideAudioTextInfo => {
|
||||||
const targetType = normalizeTargetType(source.targetType, fallback.targetType)
|
const targetType = normalizeTargetType(source.targetType, fallback.targetType)
|
||||||
@@ -312,7 +331,7 @@ export const toGuideAudioTextInfo = (
|
|||||||
available: source.available === true,
|
available: source.available === true,
|
||||||
targetType,
|
targetType,
|
||||||
targetId,
|
targetId,
|
||||||
lang: normalizeLanguage(source.lang || fallback.lang),
|
lang: normalizeGuideAudioLanguage(source.lang || fallback.lang),
|
||||||
narrationTier: normalizeNarrationTier(source.narrationTier),
|
narrationTier: normalizeNarrationTier(source.narrationTier),
|
||||||
title: source.title?.trim() || undefined,
|
title: source.title?.trim() || undefined,
|
||||||
text: source.text || undefined,
|
text: source.text || undefined,
|
||||||
|
|||||||
@@ -73,9 +73,7 @@ const requestJson = <T>(url: string): Promise<T> => new Promise((resolve, reject
|
|||||||
|
|
||||||
const normalizeKeyword = (keyword = '') => keyword.trim()
|
const normalizeKeyword = (keyword = '') => keyword.trim()
|
||||||
|
|
||||||
const catalogLang = () => (
|
const catalogLang = () => dataSourceConfig.audioLanguage
|
||||||
dataSourceConfig.audioLanguage === 'en-US' ? 'en-US' : 'zh-CN'
|
|
||||||
)
|
|
||||||
|
|
||||||
const cacheKey = (...parts: string[]) => [catalogLang(), ...parts].join(':')
|
const cacheKey = (...parts: string[]) => [catalogLang(), ...parts].join(':')
|
||||||
|
|
||||||
|
|||||||
2
src/env.d.ts
vendored
2
src/env.d.ts
vendored
@@ -15,7 +15,7 @@ interface ImportMetaEnv {
|
|||||||
readonly VITE_GUIDE_STATIC_DATA_BASE_URL?: string
|
readonly VITE_GUIDE_STATIC_DATA_BASE_URL?: string
|
||||||
readonly VITE_API_BASE_URL?: string
|
readonly VITE_API_BASE_URL?: string
|
||||||
readonly VITE_AUDIO_API_BASE_URL?: string
|
readonly VITE_AUDIO_API_BASE_URL?: string
|
||||||
readonly VITE_AUDIO_LANGUAGE?: 'zh-CN' | 'en-US'
|
readonly VITE_AUDIO_LANGUAGE?: 'zh-CN' | 'yue-HK' | 'en-US'
|
||||||
readonly VITE_SGS_API_BASE_URL?: string
|
readonly VITE_SGS_API_BASE_URL?: string
|
||||||
readonly VITE_SGS_MAP_ID?: string
|
readonly VITE_SGS_MAP_ID?: string
|
||||||
readonly VITE_SGS_SDK_SCRIPT_URL?: string
|
readonly VITE_SGS_SDK_SCRIPT_URL?: string
|
||||||
|
|||||||
@@ -32,7 +32,10 @@
|
|||||||
v-for="option in contentLanguageOptions"
|
v-for="option in contentLanguageOptions"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
class="language-anchor-option"
|
class="language-anchor-option"
|
||||||
:class="{ active: selectedAudioLanguage === option.value }"
|
:class="{
|
||||||
|
active: selectedAudioLanguage === option.value,
|
||||||
|
disabled: languageSwitchLoading
|
||||||
|
}"
|
||||||
@tap="handleLanguageChange(option.value)"
|
@tap="handleLanguageChange(option.value)"
|
||||||
>
|
>
|
||||||
<text class="language-anchor-text">{{ option.label }}</text>
|
<text class="language-anchor-text">{{ option.label }}</text>
|
||||||
@@ -52,7 +55,10 @@
|
|||||||
{{ paragraph }}
|
{{ paragraph }}
|
||||||
</text>
|
</text>
|
||||||
</template>
|
</template>
|
||||||
<text v-if="currentDetailTextLoading" class="section-hint">正在加载讲解正文</text>
|
<text v-if="currentDetailTextLoading" class="section-hint">{{ detailLoadingMessage }}</text>
|
||||||
|
<text v-else-if="audioAvailabilityMessage" class="section-hint audio-status-hint">
|
||||||
|
{{ audioAvailabilityMessage }}
|
||||||
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
@@ -129,6 +135,7 @@ import {
|
|||||||
EXPLAIN_DETAIL_PLACEHOLDER_IMAGE
|
EXPLAIN_DETAIL_PLACEHOLDER_IMAGE
|
||||||
} from '@/utils/placeholders'
|
} from '@/utils/placeholders'
|
||||||
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'
|
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'
|
||||||
|
import { normalizeGuideAudioLanguage } from '@/data/adapters/guideStopInfoAdapter'
|
||||||
|
|
||||||
const defaultDetail: ExplainDetailPageViewModel = {
|
const defaultDetail: ExplainDetailPageViewModel = {
|
||||||
id: '',
|
id: '',
|
||||||
@@ -151,7 +158,9 @@ const globalAudioPlayer = useGlobalAudioPlayer()
|
|||||||
const activeTopTab = ref<GuideTopTab>('explain')
|
const activeTopTab = ref<GuideTopTab>('explain')
|
||||||
const resolvedHallId = ref('')
|
const resolvedHallId = ref('')
|
||||||
const retryingAudio = ref(false)
|
const retryingAudio = ref(false)
|
||||||
|
const languageSwitchLoading = ref(false)
|
||||||
const selectedAudioLanguage = ref<AudioLanguage>('zh-CN')
|
const selectedAudioLanguage = ref<AudioLanguage>('zh-CN')
|
||||||
|
let languageSwitchSequence = 0
|
||||||
const detailEntryRequest = ref<{
|
const detailEntryRequest = ref<{
|
||||||
exhibitId: string
|
exhibitId: string
|
||||||
targetType?: AudioPlayTargetType
|
targetType?: AudioPlayTargetType
|
||||||
@@ -159,14 +168,17 @@ const detailEntryRequest = ref<{
|
|||||||
} | null>(null)
|
} | null>(null)
|
||||||
const detailTextByLanguage = ref<Record<AudioLanguage, string>>({
|
const detailTextByLanguage = ref<Record<AudioLanguage, string>>({
|
||||||
'zh-CN': defaultDetail.body,
|
'zh-CN': defaultDetail.body,
|
||||||
|
'yue-HK': '当前语言暂无讲解词。',
|
||||||
'en-US': 'English narration text is not available yet.'
|
'en-US': 'English narration text is not available yet.'
|
||||||
})
|
})
|
||||||
const detailTextLoadingByLanguage = ref<Record<AudioLanguage, boolean>>({
|
const detailTextLoadingByLanguage = ref<Record<AudioLanguage, boolean>>({
|
||||||
'zh-CN': false,
|
'zh-CN': false,
|
||||||
|
'yue-HK': false,
|
||||||
'en-US': false
|
'en-US': false
|
||||||
})
|
})
|
||||||
const detailTextLoadedByLanguage = ref<Record<AudioLanguage, boolean>>({
|
const detailTextLoadedByLanguage = ref<Record<AudioLanguage, boolean>>({
|
||||||
'zh-CN': false,
|
'zh-CN': false,
|
||||||
|
'yue-HK': false,
|
||||||
'en-US': false
|
'en-US': false
|
||||||
})
|
})
|
||||||
// 讲解详情页位置入口暂未开放,避免误导用户进入位置预览流程。
|
// 讲解详情页位置入口暂未开放,避免误导用户进入位置预览流程。
|
||||||
@@ -181,6 +193,10 @@ const contentLanguageOptions: Array<{
|
|||||||
value: 'zh-CN',
|
value: 'zh-CN',
|
||||||
label: '中文'
|
label: '中文'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'yue-HK',
|
||||||
|
label: '粤语'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: 'en-US',
|
value: 'en-US',
|
||||||
label: 'English'
|
label: 'English'
|
||||||
@@ -188,9 +204,15 @@ const contentLanguageOptions: Array<{
|
|||||||
]
|
]
|
||||||
|
|
||||||
const isAudioLanguage = (value: unknown): value is AudioLanguage => (
|
const isAudioLanguage = (value: unknown): value is AudioLanguage => (
|
||||||
value === 'zh-CN' || value === 'en-US'
|
value === 'zh-CN' || value === 'yue-HK' || value === 'en-US'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const audioLanguageLabel = (lang: AudioLanguage) => {
|
||||||
|
if (lang === 'en-US') return 'English'
|
||||||
|
if (lang === 'yue-HK') return '粤语'
|
||||||
|
return '中文'
|
||||||
|
}
|
||||||
|
|
||||||
const isRealHeroImage = (image?: string) => (
|
const isRealHeroImage = (image?: string) => (
|
||||||
Boolean(image && image !== EXPLAIN_DETAIL_PLACEHOLDER_IMAGE)
|
Boolean(image && image !== EXPLAIN_DETAIL_PLACEHOLDER_IMAGE)
|
||||||
)
|
)
|
||||||
@@ -222,7 +244,7 @@ const isCurrentDetailAudioTarget = computed(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
const isDetailAudioDockVisible = computed(() => (
|
const isDetailAudioDockVisible = computed(() => (
|
||||||
exhibit.value.audio.status === 'playable'
|
!languageSwitchLoading.value && exhibit.value.audio.status === 'playable'
|
||||||
))
|
))
|
||||||
const audioDockTitle = computed(() => exhibit.value.title || '讲解内容')
|
const audioDockTitle = computed(() => exhibit.value.title || '讲解内容')
|
||||||
const formatDetailAudioTime = (seconds?: number) => {
|
const formatDetailAudioTime = (seconds?: number) => {
|
||||||
@@ -249,7 +271,7 @@ const detailAudioTimeLabel = computed(() => (
|
|||||||
`${formatDetailAudioTime(detailAudioCurrentSeconds.value)} / ${formatDetailAudioTime(detailAudioDurationSeconds.value)}`
|
`${formatDetailAudioTime(detailAudioCurrentSeconds.value)} / ${formatDetailAudioTime(detailAudioDurationSeconds.value)}`
|
||||||
))
|
))
|
||||||
const audioDockSubtitle = computed(() => [
|
const audioDockSubtitle = computed(() => [
|
||||||
selectedAudioLanguage.value === 'en-US' ? 'English' : '中文',
|
audioLanguageLabel(selectedAudioLanguage.value),
|
||||||
detailAudioTimeLabel.value
|
detailAudioTimeLabel.value
|
||||||
].filter(Boolean).join(' · ') || '讲解')
|
].filter(Boolean).join(' · ') || '讲解')
|
||||||
const detailAudioPlaying = computed(() => (
|
const detailAudioPlaying = computed(() => (
|
||||||
@@ -346,26 +368,6 @@ const loadFullDetailTextForLanguage = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadBilingualDetailText = async (
|
|
||||||
request: NonNullable<typeof detailEntryRequest.value>,
|
|
||||||
currentLang: AudioLanguage,
|
|
||||||
currentViewModel: ExplainDetailPageViewModel
|
|
||||||
) => {
|
|
||||||
await loadFullDetailTextForLanguage(request, currentLang, currentViewModel)
|
|
||||||
const nextLang: AudioLanguage = currentLang === 'zh-CN' ? 'en-US' : 'zh-CN'
|
|
||||||
void loadFullDetailTextForLanguage(request, nextLang)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ensureDetailTextForLanguage = async (lang: AudioLanguage) => {
|
|
||||||
if (detailTextLoadedByLanguage.value[lang] || detailTextLoadingByLanguage.value[lang]) return
|
|
||||||
if (!detailEntryRequest.value) {
|
|
||||||
applyDetailText(lang)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadFullDetailTextForLanguage(detailEntryRequest.value, lang)
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadExplainDetail = async (
|
const loadExplainDetail = async (
|
||||||
request: NonNullable<typeof detailEntryRequest.value>,
|
request: NonNullable<typeof detailEntryRequest.value>,
|
||||||
lang: AudioLanguage
|
lang: AudioLanguage
|
||||||
@@ -382,7 +384,7 @@ const loadExplainDetail = async (
|
|||||||
resolvedHallId.value = exhibitData.hallId
|
resolvedHallId.value = exhibitData.hallId
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadBilingualDetailText(request, lang, exhibit.value)
|
void loadFullDetailTextForLanguage(request, lang, exhibit.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoad(async (options: any = {}) => {
|
onLoad(async (options: any = {}) => {
|
||||||
@@ -405,7 +407,9 @@ onLoad(async (options: any = {}) => {
|
|||||||
? String(exhibitId)
|
? String(exhibitId)
|
||||||
: rawTargetId ? String(rawTargetId) : undefined
|
: rawTargetId ? String(rawTargetId) : undefined
|
||||||
const rawLang = Array.isArray(options.lang) ? options.lang[0] : options.lang
|
const rawLang = Array.isArray(options.lang) ? options.lang[0] : options.lang
|
||||||
const lang = isAudioLanguage(rawLang) ? rawLang : 'zh-CN'
|
const lang = typeof rawLang === 'string'
|
||||||
|
? normalizeGuideAudioLanguage(rawLang)
|
||||||
|
: 'zh-CN'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
detailEntryRequest.value = {
|
detailEntryRequest.value = {
|
||||||
@@ -481,7 +485,19 @@ const splitDetailTextIntoParagraphs = (text: string): string[] => {
|
|||||||
|
|
||||||
const currentDetailText = computed(() => detailTextFor(selectedAudioLanguage.value))
|
const currentDetailText = computed(() => detailTextFor(selectedAudioLanguage.value))
|
||||||
const currentDetailParagraphs = computed(() => splitDetailTextIntoParagraphs(currentDetailText.value))
|
const currentDetailParagraphs = computed(() => splitDetailTextIntoParagraphs(currentDetailText.value))
|
||||||
const currentDetailTextLoading = computed(() => detailTextLoadingByLanguage.value[selectedAudioLanguage.value])
|
const currentDetailTextLoading = computed(() => (
|
||||||
|
languageSwitchLoading.value || detailTextLoadingByLanguage.value[selectedAudioLanguage.value]
|
||||||
|
))
|
||||||
|
const detailLoadingMessage = computed(() => (
|
||||||
|
languageSwitchLoading.value
|
||||||
|
? `正在切换到${audioLanguageLabel(selectedAudioLanguage.value)}`
|
||||||
|
: '正在加载讲解正文'
|
||||||
|
))
|
||||||
|
const audioAvailabilityMessage = computed(() => (
|
||||||
|
exhibit.value.audio.status === 'playable'
|
||||||
|
? ''
|
||||||
|
: exhibit.value.audio.unavailableReason || '当前语言暂无语音讲解'
|
||||||
|
))
|
||||||
|
|
||||||
const replaceDetailRouteLanguage = (lang: AudioLanguage) => {
|
const replaceDetailRouteLanguage = (lang: AudioLanguage) => {
|
||||||
if (typeof window === 'undefined') return
|
if (typeof window === 'undefined') return
|
||||||
@@ -493,15 +509,64 @@ const replaceDetailRouteLanguage = (lang: AudioLanguage) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleLanguageChange = async (lang: AudioLanguage, options: { syncAudio?: boolean } = {}) => {
|
const handleLanguageChange = async (lang: AudioLanguage, options: { syncAudio?: boolean } = {}) => {
|
||||||
if (lang === selectedAudioLanguage.value) return
|
if (lang === selectedAudioLanguage.value || languageSwitchLoading.value) return
|
||||||
|
|
||||||
const shouldSyncAudio = options.syncAudio !== false && isCurrentDetailAudioTarget.value
|
const shouldSyncAudio = options.syncAudio !== false && isCurrentDetailAudioTarget.value
|
||||||
|
const request = detailEntryRequest.value
|
||||||
|
const requestSequence = ++languageSwitchSequence
|
||||||
selectedAudioLanguage.value = lang
|
selectedAudioLanguage.value = lang
|
||||||
replaceDetailRouteLanguage(lang)
|
replaceDetailRouteLanguage(lang)
|
||||||
void ensureDetailTextForLanguage(lang)
|
languageSwitchLoading.value = true
|
||||||
|
|
||||||
if (shouldSyncAudio && globalAudioPlayer.currentSource.value?.lang !== lang) {
|
const audioSwitch = shouldSyncAudio && globalAudioPlayer.currentSource.value?.lang !== lang
|
||||||
await globalAudioPlayer.switchLanguage(lang)
|
? globalAudioPlayer.switchLanguage(lang)
|
||||||
|
: Promise.resolve(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!request) {
|
||||||
|
applyDetailText(lang)
|
||||||
|
await audioSwitch
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const exhibitData = await explainUseCase.enterExplainDetail({
|
||||||
|
...request,
|
||||||
|
lang
|
||||||
|
})
|
||||||
|
if (requestSequence !== languageSwitchSequence) return
|
||||||
|
|
||||||
|
const nextViewModel = toExplainDetailPageViewModel(exhibitData)
|
||||||
|
exhibit.value = nextViewModel
|
||||||
|
if (exhibitData.hallId) {
|
||||||
|
resolvedHallId.value = exhibitData.hallId
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadFullDetailTextForLanguage(request, lang, nextViewModel)
|
||||||
|
await audioSwitch
|
||||||
|
} catch (error) {
|
||||||
|
if (requestSequence !== languageSwitchSequence) return
|
||||||
|
|
||||||
|
console.warn('讲解语言切换失败:', lang, error)
|
||||||
|
exhibit.value = {
|
||||||
|
...exhibit.value,
|
||||||
|
audio: {
|
||||||
|
...exhibit.value.audio,
|
||||||
|
status: 'unavailable',
|
||||||
|
url: undefined,
|
||||||
|
duration: undefined,
|
||||||
|
language: lang,
|
||||||
|
unavailableReason: '讲解服务暂不可用,请稍后重试'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applyDetailText(lang)
|
||||||
|
uni.showToast({
|
||||||
|
title: '讲解语言切换失败,请稍后重试',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
if (requestSequence === languageSwitchSequence) {
|
||||||
|
languageSwitchLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,9 +582,7 @@ watch(
|
|||||||
&& source?.targetType === targetType
|
&& source?.targetType === targetType
|
||||||
&& source.targetId === targetId
|
&& source.targetId === targetId
|
||||||
) {
|
) {
|
||||||
selectedAudioLanguage.value = lang
|
void handleLanguageChange(lang, { syncAudio: false })
|
||||||
replaceDetailRouteLanguage(lang)
|
|
||||||
void ensureDetailTextForLanguage(lang)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1178,6 +1241,15 @@ const handleBack = () => {
|
|||||||
color: #0f140d;
|
color: #0f140d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.language-anchor-option.disabled {
|
||||||
|
opacity: 0.62;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-anchor-option.active.disabled {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.language-anchor-text {
|
.language-anchor-text {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
@@ -1261,9 +1333,17 @@ const handleBack = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section-hint {
|
.section-hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 22px;
|
||||||
color: #6d7568;
|
color: #6d7568;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.audio-status-hint {
|
||||||
|
color: #7a5a22;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-audio-dock {
|
.detail-audio-dock {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ import {
|
|||||||
type BackendGuideStopInfo,
|
type BackendGuideStopInfo,
|
||||||
type GuideAudioPlayInfo,
|
type GuideAudioPlayInfo,
|
||||||
type GuideAudioTextInfo,
|
type GuideAudioTextInfo,
|
||||||
|
type GuideAudioLanguage,
|
||||||
type GuideStopInfo
|
type GuideStopInfo
|
||||||
} from '@/data/adapters/guideStopInfoAdapter'
|
} from '@/data/adapters/guideStopInfoAdapter'
|
||||||
|
|
||||||
export type AudioLanguage = 'zh-CN' | 'en-US'
|
export type AudioLanguage = GuideAudioLanguage
|
||||||
|
|
||||||
export interface AudioPlayInfoRequest {
|
export interface AudioPlayInfoRequest {
|
||||||
targetType: AudioPlayTargetType
|
targetType: AudioPlayTargetType
|
||||||
|
|||||||
@@ -144,24 +144,32 @@ export class ExplainUseCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private languageLabel(language: AudioLanguage) {
|
private languageLabel(language: AudioLanguage) {
|
||||||
return language === 'en-US' ? '英文' : '中文'
|
if (language === 'en-US') return '英文'
|
||||||
|
if (language === 'yue-HK') return '粤语'
|
||||||
|
return '中文'
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyLanguageVariant(exhibit: MuseumExhibit, language: AudioLanguage): MuseumExhibit {
|
private applyLanguageVariant(exhibit: MuseumExhibit, language: AudioLanguage): MuseumExhibit {
|
||||||
const variant = exhibit.audioVariants?.[language]
|
const variant = exhibit.audioVariants?.[language]
|
||||||
|
const mandarinText = exhibit.audioVariants?.['zh-CN']?.text
|
||||||
|
|| exhibit.guideText
|
||||||
|
|| exhibit.description
|
||||||
const supportedLanguages = exhibit.supportedLanguages?.length
|
const supportedLanguages = exhibit.supportedLanguages?.length
|
||||||
? exhibit.supportedLanguages
|
? exhibit.supportedLanguages
|
||||||
: exhibit.audioVariants ? Object.keys(exhibit.audioVariants) : [language]
|
: exhibit.audioVariants ? Object.keys(exhibit.audioVariants) : [language]
|
||||||
|
|
||||||
if (!variant) {
|
if (!variant) {
|
||||||
const label = this.languageLabel(language)
|
const label = this.languageLabel(language)
|
||||||
|
const guideText = language === 'yue-HK'
|
||||||
|
? mandarinText || '当前讲解词暂未配置。'
|
||||||
|
: `${label}讲解词暂未配置。`
|
||||||
return {
|
return {
|
||||||
...exhibit,
|
...exhibit,
|
||||||
guideText: `${label}讲解词暂未配置。`,
|
guideText,
|
||||||
audioUrl: undefined,
|
audioUrl: undefined,
|
||||||
audioDuration: undefined,
|
audioDuration: undefined,
|
||||||
audioLanguage: language,
|
audioLanguage: language,
|
||||||
audioHasText: false,
|
audioHasText: language === 'yue-HK' && Boolean(mandarinText),
|
||||||
audioAvailable: false,
|
audioAvailable: false,
|
||||||
audioStatus: 'MISSING',
|
audioStatus: 'MISSING',
|
||||||
audioUnavailableReason: `当前讲解暂无${label}音频`,
|
audioUnavailableReason: `当前讲解暂无${label}音频`,
|
||||||
@@ -172,11 +180,13 @@ export class ExplainUseCase {
|
|||||||
return {
|
return {
|
||||||
...exhibit,
|
...exhibit,
|
||||||
guideTitle: variant.title || exhibit.guideTitle,
|
guideTitle: variant.title || exhibit.guideTitle,
|
||||||
guideText: variant.text || `${this.languageLabel(language)}讲解词暂未配置。`,
|
guideText: language === 'yue-HK'
|
||||||
|
? mandarinText || '当前讲解词暂未配置。'
|
||||||
|
: variant.text || `${this.languageLabel(language)}讲解词暂未配置。`,
|
||||||
audioUrl: variant.audioUrl,
|
audioUrl: variant.audioUrl,
|
||||||
audioDuration: variant.audioDuration,
|
audioDuration: variant.audioDuration,
|
||||||
audioLanguage: language,
|
audioLanguage: language,
|
||||||
audioHasText: variant.hasText === true,
|
audioHasText: language === 'yue-HK' ? Boolean(mandarinText) : variant.hasText === true,
|
||||||
audioAvailable: variant.available === true && Boolean(variant.audioUrl),
|
audioAvailable: variant.available === true && Boolean(variant.audioUrl),
|
||||||
audioStatus: variant.available && variant.audioUrl ? 'READY' : 'MISSING',
|
audioStatus: variant.available && variant.audioUrl ? 'READY' : 'MISSING',
|
||||||
audioUnavailableReason: variant.available && variant.audioUrl
|
audioUnavailableReason: variant.available && variant.audioUrl
|
||||||
@@ -413,12 +423,13 @@ export class ExplainUseCase {
|
|||||||
async loadExplainDetailText(exhibit: MuseumExhibit): Promise<ExplainTextSelection> {
|
async loadExplainDetailText(exhibit: MuseumExhibit): Promise<ExplainTextSelection> {
|
||||||
const targetType = exhibit.playTargetType || 'ITEM'
|
const targetType = exhibit.playTargetType || 'ITEM'
|
||||||
const targetId = exhibit.playTargetId || exhibit.id
|
const targetId = exhibit.playTargetId || exhibit.id
|
||||||
|
const lang = (exhibit.audioLanguage as AudioLanguage) || (dataSourceConfig.audioLanguage as AudioLanguage)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const textInfo = await this.audioPlayInfo.getTextInfo({
|
const textInfo = await this.audioPlayInfo.getTextInfo({
|
||||||
targetType,
|
targetType,
|
||||||
targetId,
|
targetId,
|
||||||
lang: (exhibit.audioLanguage as AudioLanguage) || (dataSourceConfig.audioLanguage as AudioLanguage)
|
lang
|
||||||
})
|
})
|
||||||
const nextExhibit = this.applyTextInfo(exhibit, textInfo)
|
const nextExhibit = this.applyTextInfo(exhibit, textInfo)
|
||||||
|
|
||||||
@@ -436,7 +447,7 @@ export class ExplainUseCase {
|
|||||||
available: false,
|
available: false,
|
||||||
targetType,
|
targetType,
|
||||||
targetId,
|
targetId,
|
||||||
lang: dataSourceConfig.audioLanguage as AudioLanguage,
|
lang,
|
||||||
reason: 'SERVICE_ERROR'
|
reason: 'SERVICE_ERROR'
|
||||||
},
|
},
|
||||||
available: false,
|
available: false,
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ const formatDuration = (duration?: number) => (
|
|||||||
|
|
||||||
const languageLabelFor = (language?: string) => {
|
const languageLabelFor = (language?: string) => {
|
||||||
if (language === 'en-US') return '英文'
|
if (language === 'en-US') return '英文'
|
||||||
|
if (language === 'yue-HK') return '粤语'
|
||||||
if (language === 'zh-CN') return '中文'
|
if (language === 'zh-CN') return '中文'
|
||||||
return language || undefined
|
return language || undefined
|
||||||
}
|
}
|
||||||
|
|||||||
84
tests/unit/GlobalAudioPlayerLanguage.spec.ts
Normal file
84
tests/unit/GlobalAudioPlayerLanguage.spec.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
// @vitest-environment happy-dom
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
const repositoryMocks = vi.hoisted(() => ({
|
||||||
|
getPlayInfo: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/repositories/AudioPlayInfoRepository', () => ({
|
||||||
|
audioPlayInfoRepository: {
|
||||||
|
getPlayInfo: repositoryMocks.getPlayInfo
|
||||||
|
},
|
||||||
|
audioReasonToText: () => '当前语言暂无语音讲解'
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { useGlobalAudioPlayer } from '@/composables/useGlobalAudioPlayer'
|
||||||
|
|
||||||
|
describe('全局讲解播放器语言切换', () => {
|
||||||
|
const player = useGlobalAudioPlayer()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
player.close()
|
||||||
|
repositoryMocks.getPlayInfo.mockReset()
|
||||||
|
vi.stubGlobal('uni', {
|
||||||
|
showToast: vi.fn()
|
||||||
|
})
|
||||||
|
vi.spyOn(window.HTMLMediaElement.prototype, 'play').mockResolvedValue(undefined)
|
||||||
|
vi.spyOn(window.HTMLMediaElement.prototype, 'pause').mockImplementation(() => undefined)
|
||||||
|
vi.spyOn(window.HTMLMediaElement.prototype, 'load').mockImplementation(() => undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
player.close()
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('切换到粤语时立即中断旧音频,并使用 yue-HK 播放地址', async () => {
|
||||||
|
repositoryMocks.getPlayInfo.mockResolvedValue({
|
||||||
|
playable: true,
|
||||||
|
targetType: 'STOP',
|
||||||
|
targetId: '1823450596800289',
|
||||||
|
lang: 'yue-HK',
|
||||||
|
audioId: 'cantonese-audio',
|
||||||
|
playUrl: '/museum-assets/audio/cantonese.mp3',
|
||||||
|
hasText: true,
|
||||||
|
fallback: false
|
||||||
|
})
|
||||||
|
|
||||||
|
await player.play({
|
||||||
|
id: 'mandarin-audio',
|
||||||
|
name: '测试讲解',
|
||||||
|
audioUrl: '/museum-assets/audio/mandarin.mp3',
|
||||||
|
language: 'zh-CN'
|
||||||
|
}, {
|
||||||
|
source: {
|
||||||
|
targetType: 'STOP',
|
||||||
|
targetId: '1823450596800289',
|
||||||
|
lang: 'zh-CN'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const pauseSpy = vi.mocked(window.HTMLMediaElement.prototype.pause)
|
||||||
|
pauseSpy.mockClear()
|
||||||
|
const switching = player.switchLanguage('yue-HK')
|
||||||
|
|
||||||
|
expect(pauseSpy).toHaveBeenCalledTimes(1)
|
||||||
|
expect(player.playing.value).toBe(false)
|
||||||
|
expect(player.currentTime.value).toBe(0)
|
||||||
|
expect(repositoryMocks.getPlayInfo).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
targetType: 'STOP',
|
||||||
|
targetId: '1823450596800289',
|
||||||
|
lang: 'yue-HK',
|
||||||
|
refresh: true
|
||||||
|
}))
|
||||||
|
|
||||||
|
await expect(switching).resolves.toBe(true)
|
||||||
|
expect(player.currentSource.value?.lang).toBe('yue-HK')
|
||||||
|
expect(player.currentAudio.value).toEqual(expect.objectContaining({
|
||||||
|
language: 'yue-HK',
|
||||||
|
audioUrl: '/museum-assets/audio/cantonese.mp3'
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user