适配讲解详情多语言声线播放
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-25 01:04:42 +08:00
parent cb6da6590d
commit 7001ec355a
8 changed files with 567 additions and 31 deletions

View File

@@ -104,6 +104,16 @@
<text class="detail-audio-title">{{ audioDockTitle }}</text>
<text class="detail-audio-subtitle">{{ audioDockSubtitle }}</text>
</view>
<button
v-if="voiceOptionsForCurrentLanguage.length"
class="detail-audio-voice"
:class="{ disabled: voiceSwitchLoading || !canSelectAudioVoice }"
:disabled="voiceSwitchLoading || !canSelectAudioVoice"
:aria-label="canSelectAudioVoice ? `选择讲解声线当前${currentVoiceLabel}` : `当前仅支持${currentVoiceLabel}`"
@tap.stop="handleVoiceSelection"
>
<text>{{ currentVoiceLabel }}</text>
</button>
<button
v-if="audioDockState === 'playable'"
class="detail-audio-rate"
@@ -226,6 +236,7 @@ const activeTopTab = ref<GuideTopTab>('explain')
const resolvedHallId = ref('')
const retryingAudio = ref(false)
const languageSwitchLoading = ref(false)
const voiceSwitchLoading = ref(false)
const selectedAudioLanguage = ref<AudioLanguage>('zh-CN')
const detailEntryRequest = ref<{
exhibitId: string
@@ -277,6 +288,20 @@ const contentLanguageOptions = computed(() => {
return languageOptionDefinitions.filter((option) => supportedLanguages.includes(option.value))
})
const voiceOptionsForCurrentLanguage = computed(() => (
(detailSource.value?.audioOptions || []).filter((option) => (
option.languageCode === selectedAudioLanguage.value && Boolean(option.audioUrl?.trim())
))
))
const canSelectAudioVoice = computed(() => (
new Set(voiceOptionsForCurrentLanguage.value.map((option) => option.gender)).size > 1
))
const currentVoiceLabel = computed(() => (
detailSource.value?.audioVoiceGender === 'male' ? '男声' : '女声'
))
const resolveSupportedDetailLanguage = (requestedLanguage: AudioLanguage) => {
const supportedLanguages = supportedDetailLanguages.value
if (!supportedLanguages.length || supportedLanguages.includes(requestedLanguage)) {
@@ -410,6 +435,7 @@ const isCurrentDetailAudioError = computed(() => {
})
const isDetailAudioLoading = computed(() => (
languageSwitchLoading.value
|| voiceSwitchLoading.value
|| retryingAudio.value
|| (isCurrentDetailAudioTarget.value && globalAudioPlayer.loading.value)
))
@@ -671,6 +697,51 @@ const handleLanguageChange = async (lang: AudioLanguage, options: { syncAudio?:
}
}
const selectVoiceOption = async (channelCode: string) => {
if (voiceSwitchLoading.value) return
const source = detailSource.value
if (!source) return
const localizedExhibit = explainUseCase.selectExplainDetailAudioOption(source, channelCode)
if (localizedExhibit === source) return
const shouldRestartAudio = isCurrentDetailAudio.value && globalAudioPlayer.playing.value
const shouldCloseAudio = isCurrentDetailAudioTarget.value
voiceSwitchLoading.value = true
try {
if (shouldCloseAudio) {
globalAudioPlayer.close()
}
detailSource.value = localizedExhibit
exhibit.value = toExplainDetailPageViewModel(localizedExhibit)
if (shouldRestartAudio) {
voiceSwitchLoading.value = false
await handlePlayAudio()
}
} finally {
voiceSwitchLoading.value = false
}
}
const handleVoiceSelection = () => {
const voiceOptions = voiceOptionsForCurrentLanguage.value
if (!canSelectAudioVoice.value || voiceSwitchLoading.value) return
uni.showActionSheet({
itemList: voiceOptions.map((option) => option.displayName),
success: ({ tapIndex }) => {
const selectedOption = voiceOptions[tapIndex]
if (selectedOption) {
void selectVoiceOption(selectedOption.channelCode)
}
}
})
}
watch(
() => globalAudioPlayer.currentSource.value,
(source) => {
@@ -1270,6 +1341,7 @@ const handleBack = returnToExplainObjectList
border: 0;
}
.detail-audio-voice,
.detail-audio-rate,
.detail-audio-mute {
position: absolute;
@@ -1285,6 +1357,36 @@ const handleBack = returnToExplainObjectList
color: #192d83;
}
.detail-audio-voice {
right: 94px;
width: 46px;
color: #31524a;
}
.detail-audio-voice text {
width: 42px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border: 1px solid #d9ddd6;
border-radius: 4px;
background: #ffffff;
font-size: 12px;
line-height: 16px;
font-weight: 700;
}
.detail-audio-voice.disabled {
color: #8b918c;
}
.detail-audio-voice.disabled text {
border-color: #e6e7e4;
background: #f8f8f7;
}
.detail-audio-rate {
right: 50px;
width: 40px;
@@ -1313,6 +1415,7 @@ const handleBack = returnToExplainObjectList
color: #7a807a;
}
.detail-audio-voice::after,
.detail-audio-rate::after,
.detail-audio-mute::after {
border: 0;
@@ -1372,7 +1475,7 @@ const handleBack = returnToExplainObjectList
}
.detail-audio-dock.is-playable .detail-audio-progress-hit {
right: 88px;
right: 140px;
}
.detail-audio-progress.is-indeterminate::after {