@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user