This commit is contained in:
@@ -32,7 +32,10 @@
|
||||
v-for="option in contentLanguageOptions"
|
||||
:key="option.value"
|
||||
class="language-anchor-option"
|
||||
:class="{ active: selectedAudioLanguage === option.value }"
|
||||
:class="{
|
||||
active: selectedAudioLanguage === option.value,
|
||||
disabled: languageSwitchLoading
|
||||
}"
|
||||
@tap="handleLanguageChange(option.value)"
|
||||
>
|
||||
<text class="language-anchor-text">{{ option.label }}</text>
|
||||
@@ -52,7 +55,10 @@
|
||||
{{ paragraph }}
|
||||
</text>
|
||||
</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>
|
||||
</scroll-view>
|
||||
@@ -129,6 +135,7 @@ import {
|
||||
EXPLAIN_DETAIL_PLACEHOLDER_IMAGE
|
||||
} from '@/utils/placeholders'
|
||||
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'
|
||||
import { normalizeGuideAudioLanguage } from '@/data/adapters/guideStopInfoAdapter'
|
||||
|
||||
const defaultDetail: ExplainDetailPageViewModel = {
|
||||
id: '',
|
||||
@@ -151,7 +158,9 @@ const globalAudioPlayer = useGlobalAudioPlayer()
|
||||
const activeTopTab = ref<GuideTopTab>('explain')
|
||||
const resolvedHallId = ref('')
|
||||
const retryingAudio = ref(false)
|
||||
const languageSwitchLoading = ref(false)
|
||||
const selectedAudioLanguage = ref<AudioLanguage>('zh-CN')
|
||||
let languageSwitchSequence = 0
|
||||
const detailEntryRequest = ref<{
|
||||
exhibitId: string
|
||||
targetType?: AudioPlayTargetType
|
||||
@@ -159,14 +168,17 @@ const detailEntryRequest = ref<{
|
||||
} | null>(null)
|
||||
const detailTextByLanguage = ref<Record<AudioLanguage, string>>({
|
||||
'zh-CN': defaultDetail.body,
|
||||
'yue-HK': '当前语言暂无讲解词。',
|
||||
'en-US': 'English narration text is not available yet.'
|
||||
})
|
||||
const detailTextLoadingByLanguage = ref<Record<AudioLanguage, boolean>>({
|
||||
'zh-CN': false,
|
||||
'yue-HK': false,
|
||||
'en-US': false
|
||||
})
|
||||
const detailTextLoadedByLanguage = ref<Record<AudioLanguage, boolean>>({
|
||||
'zh-CN': false,
|
||||
'yue-HK': false,
|
||||
'en-US': false
|
||||
})
|
||||
// 讲解详情页位置入口暂未开放,避免误导用户进入位置预览流程。
|
||||
@@ -181,6 +193,10 @@ const contentLanguageOptions: Array<{
|
||||
value: 'zh-CN',
|
||||
label: '中文'
|
||||
},
|
||||
{
|
||||
value: 'yue-HK',
|
||||
label: '粤语'
|
||||
},
|
||||
{
|
||||
value: 'en-US',
|
||||
label: 'English'
|
||||
@@ -188,9 +204,15 @@ const contentLanguageOptions: Array<{
|
||||
]
|
||||
|
||||
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) => (
|
||||
Boolean(image && image !== EXPLAIN_DETAIL_PLACEHOLDER_IMAGE)
|
||||
)
|
||||
@@ -222,7 +244,7 @@ const isCurrentDetailAudioTarget = computed(() => {
|
||||
)
|
||||
})
|
||||
const isDetailAudioDockVisible = computed(() => (
|
||||
exhibit.value.audio.status === 'playable'
|
||||
!languageSwitchLoading.value && exhibit.value.audio.status === 'playable'
|
||||
))
|
||||
const audioDockTitle = computed(() => exhibit.value.title || '讲解内容')
|
||||
const formatDetailAudioTime = (seconds?: number) => {
|
||||
@@ -249,7 +271,7 @@ const detailAudioTimeLabel = computed(() => (
|
||||
`${formatDetailAudioTime(detailAudioCurrentSeconds.value)} / ${formatDetailAudioTime(detailAudioDurationSeconds.value)}`
|
||||
))
|
||||
const audioDockSubtitle = computed(() => [
|
||||
selectedAudioLanguage.value === 'en-US' ? 'English' : '中文',
|
||||
audioLanguageLabel(selectedAudioLanguage.value),
|
||||
detailAudioTimeLabel.value
|
||||
].filter(Boolean).join(' · ') || '讲解')
|
||||
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 (
|
||||
request: NonNullable<typeof detailEntryRequest.value>,
|
||||
lang: AudioLanguage
|
||||
@@ -382,7 +384,7 @@ const loadExplainDetail = async (
|
||||
resolvedHallId.value = exhibitData.hallId
|
||||
}
|
||||
|
||||
void loadBilingualDetailText(request, lang, exhibit.value)
|
||||
void loadFullDetailTextForLanguage(request, lang, exhibit.value)
|
||||
}
|
||||
|
||||
onLoad(async (options: any = {}) => {
|
||||
@@ -405,7 +407,9 @@ onLoad(async (options: any = {}) => {
|
||||
? String(exhibitId)
|
||||
: rawTargetId ? String(rawTargetId) : undefined
|
||||
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 {
|
||||
detailEntryRequest.value = {
|
||||
@@ -481,7 +485,19 @@ const splitDetailTextIntoParagraphs = (text: string): string[] => {
|
||||
|
||||
const currentDetailText = computed(() => detailTextFor(selectedAudioLanguage.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) => {
|
||||
if (typeof window === 'undefined') return
|
||||
@@ -493,15 +509,64 @@ const replaceDetailRouteLanguage = (lang: AudioLanguage) => {
|
||||
}
|
||||
|
||||
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 request = detailEntryRequest.value
|
||||
const requestSequence = ++languageSwitchSequence
|
||||
selectedAudioLanguage.value = lang
|
||||
replaceDetailRouteLanguage(lang)
|
||||
void ensureDetailTextForLanguage(lang)
|
||||
languageSwitchLoading.value = true
|
||||
|
||||
if (shouldSyncAudio && globalAudioPlayer.currentSource.value?.lang !== lang) {
|
||||
await globalAudioPlayer.switchLanguage(lang)
|
||||
const audioSwitch = shouldSyncAudio && globalAudioPlayer.currentSource.value?.lang !== 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.targetId === targetId
|
||||
) {
|
||||
selectedAudioLanguage.value = lang
|
||||
replaceDetailRouteLanguage(lang)
|
||||
void ensureDetailTextForLanguage(lang)
|
||||
void handleLanguageChange(lang, { syncAudio: false })
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1178,6 +1241,15 @@ const handleBack = () => {
|
||||
color: #0f140d;
|
||||
}
|
||||
|
||||
.language-anchor-option.disabled {
|
||||
opacity: 0.62;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.language-anchor-option.active.disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.language-anchor-text {
|
||||
max-width: 100%;
|
||||
font-size: 18px;
|
||||
@@ -1261,9 +1333,17 @@ const handleBack = () => {
|
||||
}
|
||||
|
||||
.section-hint {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
color: #6d7568;
|
||||
}
|
||||
|
||||
.audio-status-hint {
|
||||
color: #7a5a22;
|
||||
}
|
||||
|
||||
.detail-audio-dock {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
|
||||
Reference in New Issue
Block a user