diff --git a/src/pages/exhibit/detail.vue b/src/pages/exhibit/detail.vue index 3bfed39..9120af1 100644 --- a/src/pages/exhibit/detail.vue +++ b/src/pages/exhibit/detail.vue @@ -234,6 +234,8 @@ const detailEntryRequest = ref<{ floorId?: string floorLabel?: string poiId?: string + returnToGuideStopList?: boolean + returnToHallList?: boolean } | null>(null) const detailTextByLanguage = ref>({ 'zh-CN': defaultDetail.body, @@ -569,7 +571,13 @@ onLoad(async (options: any = {}) => { hallId: Array.isArray(options.hallId) ? options.hallId[0] : options.hallId, hallName: Array.isArray(options.hallName) ? options.hallName[0] : options.hallName, floorId: Array.isArray(options.floorId) ? options.floorId[0] : options.floorId, - poiId: Array.isArray(options.poiId) ? options.poiId[0] : options.poiId + poiId: Array.isArray(options.poiId) ? options.poiId[0] : options.poiId, + returnToGuideStopList: (Array.isArray(options.returnToGuideStopList) + ? options.returnToGuideStopList[0] + : options.returnToGuideStopList) === '1', + returnToHallList: (Array.isArray(options.returnToHallList) + ? options.returnToHallList[0] + : options.returnToHallList) === '1' } await loadExplainDetail(detailEntryRequest.value, lang) } catch (error) { @@ -772,6 +780,12 @@ function buildDetailRoute(lang: AudioLanguage = selectedAudioLanguage.value) { if (lang) { params.set('lang', lang) } + if (request?.hallId) params.set('hallId', request.hallId) + if (request?.hallName) params.set('hallName', request.hallName) + if (request?.floorId) params.set('floorId', request.floorId) + if (request?.poiId) params.set('poiId', request.poiId) + if (request?.returnToGuideStopList) params.set('returnToGuideStopList', '1') + if (request?.returnToHallList) params.set('returnToHallList', '1') return `/pages/exhibit/detail?${params.toString()}` } @@ -936,7 +950,9 @@ const fallbackToExplainObjectList = () => { const hallId = detailEntryRequest.value?.hallId || exhibit.value.hallId const hallName = decodeRouteParam(detailEntryRequest.value?.hallName || exhibit.value.hallName || '讲解') const url = hallId - ? explainGuideStopListUrl(hallId, hallName) + ? explainGuideStopListUrl(hallId, hallName, { + returnToHallList: detailEntryRequest.value?.returnToHallList + }) : '/pages/explain/list' uni.redirectTo({ @@ -963,6 +979,11 @@ const returnToExplainObjectList = async () => { return } + if (detailEntryRequest.value?.returnToGuideStopList) { + fallbackToExplainObjectList() + return + } + if (isEmbeddedInWechatMiniProgram()) { if (returningToMiniProgram) return returningToMiniProgram = true diff --git a/src/pages/explain/guide-stop-list.vue b/src/pages/explain/guide-stop-list.vue index 12dc57d..d8992ad 100644 --- a/src/pages/explain/guide-stop-list.vue +++ b/src/pages/explain/guide-stop-list.vue @@ -40,6 +40,7 @@ const explainError = ref('') const loadMoreError = ref('') const hasMore = ref(false) const nextPageNo = ref(1) +const shouldReturnToHallList = ref(false) let requestVersion = 0 let disposed = false let returningToMiniProgram = false @@ -119,6 +120,7 @@ onLoad((options: Record = {}) => { const first = (value: string | string[] | undefined) => Array.isArray(value) ? value[0] : value selectedExplainHallId.value = first(options.hallId) || '' selectedExplainHallName.value = first(options.hallName) || '讲解对象' + shouldReturnToHallList.value = first(options.returnToHallList) === '1' syncPageTitle('讲解对象') if (!selectedExplainHallId.value) { explainError.value = '缺少展厅参数,请返回展厅列表后重试' @@ -144,6 +146,8 @@ const handleExplainGuideStopClick = (stop: ExplainGuideStopCatalogItem) => { if (hallName) params.set('hallName', hallName) if (stop.floorId) params.set('floorId', stop.floorId) if (stop.poiId) params.set('poiId', stop.poiId) + params.set('returnToGuideStopList', '1') + if (shouldReturnToHallList.value) params.set('returnToHallList', '1') uni.navigateTo({ url: `/pages/exhibit/detail?${params.toString()}` }) } @@ -173,6 +177,12 @@ const returnToHallList = async () => { return } + // H5 web-view 有时不会暴露完整页面栈,保留入口上下文即可回到展厅列表。 + if (shouldReturnToHallList.value) { + fallbackToHallList() + return + } + if (isEmbeddedInWechatMiniProgram()) { if (returningToMiniProgram) return returningToMiniProgram = true diff --git a/src/pages/explain/list.vue b/src/pages/explain/list.vue index cc7262a..a164715 100644 --- a/src/pages/explain/list.vue +++ b/src/pages/explain/list.vue @@ -279,7 +279,7 @@ const handleTopTabChange = (tab: GuideTopTab) => { const handleExplainHallClick = (hallId: string) => { const hall = explainHallItems.value.find((item) => item.id === hallId) uni.navigateTo({ - url: explainGuideStopListUrl(hallId, hall?.name || '讲解') + url: explainGuideStopListUrl(hallId, hall?.name || '讲解', { returnToHallList: true }) }) } diff --git a/src/utils/explainNavigation.ts b/src/utils/explainNavigation.ts index 8d416f2..e28b19d 100644 --- a/src/utils/explainNavigation.ts +++ b/src/utils/explainNavigation.ts @@ -1,4 +1,13 @@ -export const explainGuideStopListUrl = (hallId: string, hallName = '讲解') => { +type ExplainGuideStopListNavigationOptions = { + returnToHallList?: boolean +} + +export const explainGuideStopListUrl = ( + hallId: string, + hallName = '讲解', + options: ExplainGuideStopListNavigationOptions = {} +) => { const params = new URLSearchParams({ hallId, hallName }) + if (options.returnToHallList) params.set('returnToHallList', '1') return `/pages/explain/guide-stop-list?${params.toString()}` } diff --git a/tests/unit/ExhibitDetailAudioFirst.spec.ts b/tests/unit/ExhibitDetailAudioFirst.spec.ts index 48aedd7..4c6813b 100644 --- a/tests/unit/ExhibitDetailAudioFirst.spec.ts +++ b/tests/unit/ExhibitDetailAudioFirst.spec.ts @@ -411,6 +411,34 @@ describe('讲解详情音频优先布局', () => { expect(uni.redirectTo).not.toHaveBeenCalled() }) + it('从讲解对象列表进入时,即使 H5 页面栈缺失也回到对象列表', async () => { + mocks.embeddedInWechatMiniProgram = true + mocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'returned' }) + const wrapper = mount(ExhibitDetail, { + global: { stubs: { GuidePageFrame: GuidePageFrameStub } } + }) + + await mocks.onLoadHandler?.({ + id: exhibit.id, + targetType: 'STOP', + targetId: 'stop-1', + hallId: 'hall-1', + hallName: '恐龙厅', + returnToGuideStopList: '1', + returnToHallList: '1' + }) + wrapper.getComponent(GuidePageFrameStub).vm.$emit('back') + await flushPromises() + + const url = vi.mocked(uni.redirectTo).mock.calls[0]?.[0]?.url || '' + const params = new URLSearchParams(url.split('?')[1]) + expect(url.split('?')[0]).toBe('/pages/explain/guide-stop-list') + expect(params.get('hallId')).toBe('hall-1') + expect(params.get('returnToHallList')).toBe('1') + expect(mocks.returnToWechatMiniProgram).not.toHaveBeenCalled() + expect(uni.navigateBack).not.toHaveBeenCalled() + }) + it('离开详情页时立即关闭当前讲解播放器', async () => { audioState.currentAudio.value = { id: 'audio-1', audioUrl: exhibit.audioUrl } audioState.currentSource.value = { targetType: 'STOP', targetId: 'stop-1', lang: 'en-US' } diff --git a/tests/unit/ExplainGuideStopListNavigation.spec.ts b/tests/unit/ExplainGuideStopListNavigation.spec.ts index d3ecca8..9a45d7f 100644 --- a/tests/unit/ExplainGuideStopListNavigation.spec.ts +++ b/tests/unit/ExplainGuideStopListNavigation.spec.ts @@ -136,6 +136,7 @@ describe('讲解对象列表返回', () => { expect(params.get('hallName')).toBe('恐龙厅') expect(params.get('targetType')).toBe('STOP') expect(params.get('targetId')).toBe('target-1') + expect(params.get('returnToGuideStopList')).toBe('1') wrapper.unmount() }) @@ -192,6 +193,25 @@ describe('讲解对象列表返回', () => { wrapper.unmount() }) + it('从展厅列表进入时,即使 H5 页面栈缺失也保留内部返回路径', async () => { + hostEnvironmentMocks.embeddedInWechatMiniProgram = true + miniProgramReturnMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'returned' }) + const wrapper = mountGuideStopList() + testState.onLoadHandler?.({ hallId: 'hall-1', hallName: '恐龙厅', returnToHallList: '1' }) + await flushPromises() + + await wrapper.get('[data-testid="detail-click"]').trigger('click') + const detailUrl = vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url || '' + expect(new URLSearchParams(detailUrl.split('?')[1]).get('returnToHallList')).toBe('1') + + await wrapper.get('[data-testid="list-back"]').trigger('click') + await flushPromises() + + expect(uni.reLaunch).toHaveBeenCalledWith({ url: '/pages/explain/list' }) + expect(miniProgramReturnMocks.returnToWechatMiniProgram).not.toHaveBeenCalled() + wrapper.unmount() + }) + it('页面栈返回失败时回退至独立展厅列表', async () => { vi.stubGlobal('getCurrentPages', vi.fn(() => [ { route: 'pages/explain/list', options: {} },