diff --git a/src/pages/exhibit/detail.vue b/src/pages/exhibit/detail.vue index ba8beeb..7f25712 100644 --- a/src/pages/exhibit/detail.vue +++ b/src/pages/exhibit/detail.vue @@ -218,7 +218,7 @@ const detailTextLoadedByLanguage = ref>({ const isLocationActionVisible = false const shouldUseHostNavigation = computed(() => isEmbeddedInWechatMiniProgram()) -const contentLanguageOptions: Array<{ +const languageOptionDefinitions: Array<{ value: AudioLanguage label: string }> = [ @@ -240,6 +240,24 @@ const isAudioLanguage = (value: unknown): value is AudioLanguage => ( value === 'zh-CN' || value === 'yue-HK' || value === 'en-US' ) +const supportedDetailLanguages = computed(() => ( + (exhibit.value.audio.supportedLanguages || []).filter(isAudioLanguage) +)) + +const contentLanguageOptions = computed(() => { + const supportedLanguages = supportedDetailLanguages.value + return languageOptionDefinitions.filter((option) => supportedLanguages.includes(option.value)) +}) + +const resolveSupportedDetailLanguage = (requestedLanguage: AudioLanguage) => { + const supportedLanguages = supportedDetailLanguages.value + if (!supportedLanguages.length || supportedLanguages.includes(requestedLanguage)) { + return requestedLanguage + } + + return supportedLanguages.includes('zh-CN') ? 'zh-CN' : supportedLanguages[0] +} + const audioLanguageLabel = (lang: AudioLanguage) => { if (lang === 'en-US') return 'English' if (lang === 'yue-HK') return '粤语' @@ -446,13 +464,20 @@ const loadExplainDetail = async ( }) exhibit.value = toExplainDetailPageViewModel(exhibitData) - selectedAudioLanguage.value = lang + const resolvedLanguage = resolveSupportedDetailLanguage(lang) + selectedAudioLanguage.value = resolvedLanguage if (exhibitData.hallId) { resolvedHallId.value = exhibitData.hallId } - void loadFullDetailTextForLanguage(request, lang, exhibit.value) + if (resolvedLanguage !== lang) { + replaceDetailRouteLanguage(resolvedLanguage) + await loadExplainDetail(request, resolvedLanguage) + return + } + + void loadFullDetailTextForLanguage(request, resolvedLanguage, exhibit.value) } onLoad(async (options: any = {}) => { @@ -581,7 +606,11 @@ const replaceDetailRouteLanguage = (lang: AudioLanguage) => { } const handleLanguageChange = async (lang: AudioLanguage, options: { syncAudio?: boolean } = {}) => { - if (lang === selectedAudioLanguage.value || languageSwitchLoading.value) return + if ( + lang === selectedAudioLanguage.value + || languageSwitchLoading.value + || (supportedDetailLanguages.value.length > 0 && !supportedDetailLanguages.value.includes(lang)) + ) return const shouldSyncAudio = options.syncAudio !== false && isCurrentDetailAudioTarget.value const request = detailEntryRequest.value diff --git a/tests/unit/ExhibitDetailAudioFirst.spec.ts b/tests/unit/ExhibitDetailAudioFirst.spec.ts index cfd12f6..3a2f2e8 100644 --- a/tests/unit/ExhibitDetailAudioFirst.spec.ts +++ b/tests/unit/ExhibitDetailAudioFirst.spec.ts @@ -86,7 +86,8 @@ const exhibit = { audioLanguage: 'en-US', audioHasText: false, playTargetType: 'STOP', - playTargetId: 'stop-1' + playTargetId: 'stop-1', + supportedLanguages: ['zh-CN', 'yue-HK', 'en-US'] } describe('讲解详情音频优先布局', () => { @@ -148,6 +149,59 @@ describe('讲解详情音频优先布局', () => { expect(wrapper.get('.section-text').text()).toContain('detailed English narration') }) + it('仅展示 stop-info 声明支持的语言按钮', async () => { + mocks.enterExplainDetail.mockResolvedValue({ + ...exhibit, + supportedLanguages: ['zh-CN', 'en-US'] + }) + const wrapper = mount(ExhibitDetail, { + global: { stubs: { GuidePageFrame: GuidePageFrameStub } } + }) + + await mocks.onLoadHandler?.({ id: exhibit.id, lang: 'zh-CN', targetType: 'STOP', targetId: 'stop-1' }) + await flushPromises() + + const labels = wrapper.findAll('.language-option-text').map((node) => node.text()) + expect(labels).toEqual(['中文', 'English']) + expect(labels).not.toContain('粤语') + }) + + it('无支持语言时不显示语言栏', async () => { + mocks.enterExplainDetail.mockResolvedValue({ + ...exhibit, + supportedLanguages: [] + }) + const wrapper = mount(ExhibitDetail, { + global: { stubs: { GuidePageFrame: GuidePageFrameStub } } + }) + + await mocks.onLoadHandler?.({ id: exhibit.id, lang: 'zh-CN', targetType: 'STOP', targetId: 'stop-1' }) + await flushPromises() + + expect(wrapper.find('.detail-language-bar').exists()).toBe(false) + }) + + it('深链语言不受支持时回退至中文并规范化地址', async () => { + mocks.enterExplainDetail.mockImplementation(async (request: { lang: string }) => ({ + ...exhibit, + audioLanguage: request.lang, + supportedLanguages: ['zh-CN'] + })) + const replaceState = vi.spyOn(window.history, 'replaceState') + const wrapper = mount(ExhibitDetail, { + global: { stubs: { GuidePageFrame: GuidePageFrameStub } } + }) + + await mocks.onLoadHandler?.({ id: exhibit.id, lang: 'en-US', targetType: 'STOP', targetId: 'stop-1' }) + await flushPromises() + + expect(mocks.enterExplainDetail).toHaveBeenNthCalledWith(1, expect.objectContaining({ lang: 'en-US' })) + expect(mocks.enterExplainDetail).toHaveBeenNthCalledWith(2, expect.objectContaining({ lang: 'zh-CN' })) + expect(wrapper.get('.language-option').classes()).toContain('active') + expect(wrapper.get('.language-option-text').text()).toBe('中文') + expect(replaceState).toHaveBeenCalledWith(null, '', expect.stringContaining('lang=zh-CN')) + }) + it('通过同一全局音频播放器开始播放,不创建页面播放器', async () => { const wrapper = mount(ExhibitDetail, { global: { stubs: { GuidePageFrame: GuidePageFrameStub } }