修复:首页点位讲解入口展示逻辑

This commit is contained in:
cxk
2026-07-20 01:50:14 +08:00
parent e2bf84aec6
commit 5331d6e0af
2 changed files with 95 additions and 62 deletions

View File

@@ -99,15 +99,12 @@
</view> </view>
</view> </view>
<view <view
v-if="selectedGuidePoiIsHall" v-if="selectedGuidePoiExplainHall"
class="poi-card-actions" class="poi-card-actions"
> >
<view class="poi-action primary" @tap="handleViewSelectedHall"> <view class="poi-action primary" @tap="handleViewSelectedHall">
<text class="poi-action-text">查看展厅</text> <text class="poi-action-text">查看展厅</text>
</view> </view>
<view class="poi-action" @tap="handleSelectedPoiRelated">
<text class="poi-action-text">相关讲解</text>
</view>
</view> </view>
</template> </template>
</view> </view>
@@ -290,7 +287,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue' import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app' import { onLoad, onShow } from '@dcloudio/uni-app'
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue' import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
import GuideMapShell from '@/components/navigation/GuideMapShell.vue' import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
@@ -400,6 +397,8 @@ const loadingFloorId = ref('')
const renderedFloorId = ref('') const renderedFloorId = ref('')
const failedFloorId = ref('') const failedFloorId = ref('')
const selectedGuidePoi = ref<GuideRenderPoi | null>(null) const selectedGuidePoi = ref<GuideRenderPoi | null>(null)
const selectedGuidePoiExplainHall = ref<MuseumHall | null>(null)
let selectedGuidePoiExplainHallRequestId = 0
// 状态 // 状态
const currentTab = ref<GuideTopTab>(initialTopTab()) const currentTab = ref<GuideTopTab>(initialTopTab())
@@ -714,21 +713,20 @@ const selectedGuidePoiSubtitle = computed(() => {
return `${floorLabel} · ${poi.primaryCategoryZh || '位置预览'}` return `${floorLabel} · ${poi.primaryCategoryZh || '位置预览'}`
}) })
const selectedGuidePoiIsHall = computed(() => { const isGuidePoiHallLike = (poi: GuideRenderPoi | null) => Boolean(
const poi = selectedGuidePoi.value poi
return Boolean( && (
poi poi.kind === 'hall'
&& ( || poi.primaryCategory === 'touring_poi'
poi.kind === 'hall' || poi.primaryCategory === 'exhibition_hall'
|| poi.primaryCategory === 'touring_poi' || poi.primaryCategory === 'exhibition_hall_entrance'
|| poi.primaryCategory === 'exhibition_hall'
|| poi.primaryCategory === 'exhibition_hall_entrance'
)
) )
}) )
const selectedGuidePoiIsHall = computed(() => isGuidePoiHallLike(selectedGuidePoi.value))
const selectedGuidePoiCollapsedSubtitle = computed(() => ( const selectedGuidePoiCollapsedSubtitle = computed(() => (
selectedGuidePoiIsHall.value ? '点按展开查看展厅' : selectedGuidePoiSubtitle.value selectedGuidePoiExplainHall.value ? '点按展开查看展厅' : selectedGuidePoiSubtitle.value
)) ))
const routeSimulationStepText = computed(() => { const routeSimulationStepText = computed(() => {
@@ -1132,12 +1130,10 @@ const togglePoiCardCollapsed = () => {
const normalizeHallMatchText = (value?: string) => (value || '') const normalizeHallMatchText = (value?: string) => (value || '')
.trim() .trim()
.replace(/[\s()【】[\]_-]/g, '') .replace(/[\s()【】[\]_-]/g, '')
.replace(/^展厅\d+/, '')
.toLowerCase() .toLowerCase()
const resolveSelectedPoiHall = async (): Promise<MuseumHall | null> => { const resolvePoiExplainHall = async (poi: GuideRenderPoi): Promise<MuseumHall | null> => {
const poi = selectedGuidePoi.value
if (!poi) return null
const halls = await explainUseCase.listHalls() const halls = await explainUseCase.listHalls()
const matchedByPoiId = halls.find((hall) => ( const matchedByPoiId = halls.find((hall) => (
hall.poiId === poi.id hall.poiId === poi.id
@@ -1162,10 +1158,30 @@ const resolveSelectedPoiHall = async (): Promise<MuseumHall | null> => {
const poiHallName = normalizeHallMatchText(poi.hallName || poi.name) const poiHallName = normalizeHallMatchText(poi.hallName || poi.name)
return halls.find((hall) => { return halls.find((hall) => {
const hallName = normalizeHallMatchText(hall.name) const hallName = normalizeHallMatchText(hall.name)
return hallName && poiHallName && (hallName.includes(poiHallName) || poiHallName.includes(hallName)) return hallName && poiHallName && hallName === poiHallName
}) || null }) || null
} }
watch(selectedGuidePoi, (poi) => {
const requestId = ++selectedGuidePoiExplainHallRequestId
selectedGuidePoiExplainHall.value = null
if (!poi || !isGuidePoiHallLike(poi)) return
void resolvePoiExplainHall(poi)
.then((hall) => {
if (
requestId !== selectedGuidePoiExplainHallRequestId
|| selectedGuidePoi.value?.id !== poi.id
) return
selectedGuidePoiExplainHall.value = hall
})
.catch((error) => {
if (requestId !== selectedGuidePoiExplainHallRequestId) return
console.warn('点位讲解展厅关联解析失败:', error)
})
}, { flush: 'sync' })
const navigateToPoiDetail = ({ const navigateToPoiDetail = ({
url, url,
floorId, floorId,
@@ -1209,43 +1225,12 @@ const navigateToHallExplainObjects = (hallId: string, hallName = '讲解') => {
) )
} }
const handleViewSelectedHall = async () => { const handleViewSelectedHall = () => {
const hall = await resolveSelectedPoiHall() const hall = selectedGuidePoiExplainHall.value
if (!hall) { if (!hall) return
uni.showToast({
title: '该展厅详情暂未接入',
icon: 'none'
})
return
}
navigateToHallExplainObjects(hall.id, hall.name) navigateToHallExplainObjects(hall.id, hall.name)
} }
const handleSelectedPoiRelated = async () => {
if (!selectedGuidePoi.value) return
const hall = await resolveSelectedPoiHall()
if (hall) {
navigateToHallExplainObjects(hall.id, hall.name)
return
}
const keyword = selectedGuidePoi.value.hallName || selectedGuidePoi.value.name
const results = await explainUseCase.searchExplain(keyword)
const firstHall = results.find((item) => item.type === 'hall' && item.hallId)
if (firstHall?.hallId) {
navigateToHallExplainObjects(firstHall.hallId, selectedGuidePoi.value.hallName || '讲解')
return
}
uni.showToast({
title: '该展厅暂无对应讲解',
icon: 'none'
})
}
const loadRouteTargets = async (keyword = '') => { const loadRouteTargets = async (keyword = '') => {
routeTargetsLoading.value = true routeTargetsLoading.value = true
routeTargetsError.value = '' routeTargetsError.value = ''

View File

@@ -528,7 +528,7 @@ describe('首页搜索与地图闭环', () => {
expect(testState.resetToViewBaselineCalls).toHaveLength(1) expect(testState.resetToViewBaselineCalls).toHaveLength(1)
}) })
it('模型点选展厅的相关讲解进入该展厅的讲解对象列表,并在返回时复位原楼层', async () => { it('模型点选展厅只显示一个讲解入口,并在返回时复位原楼层', async () => {
testState.halls = [{ id: 'hall-l2', poiId: poi.id, name: '二层展厅' }] testState.halls = [{ id: 'hall-l2', poiId: poi.id, name: '二层展厅' }]
const wrapper = await mountIndex() const wrapper = await mountIndex()
const map = wrapper.getComponent(GuideMapShellStub) const map = wrapper.getComponent(GuideMapShellStub)
@@ -541,9 +541,10 @@ describe('首页搜索与地图闭环', () => {
}) })
await flushPromises() await flushPromises()
const relatedActions = wrapper.findAll('.poi-action') const actions = wrapper.findAll('.poi-action')
expect(relatedActions).toHaveLength(2) expect(actions).toHaveLength(1)
await relatedActions[1]!.trigger('tap') expect(wrapper.text()).not.toContain('相关讲解')
await wrapper.find('.poi-action.primary').trigger('tap')
await flushPromises() await flushPromises()
expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain('/pages/explain/guide-stop-list?hallId=hall-l2') expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain('/pages/explain/guide-stop-list?hallId=hall-l2')
@@ -559,7 +560,54 @@ describe('首页搜索与地图闭环', () => {
expect(wrapper.find('.guide-poi-card').exists()).toBe(false) expect(wrapper.find('.guide-poi-card').exists()).toBe(false)
}) })
it('模型点选展厅的相关讲解优先进入讲解对象列表并保留一次性返回上下文', async () => { it('展厅类型空间没有讲解展厅关联时不显示展厅与讲解按钮', async () => {
testState.halls = [{ id: 'hall-biology', poiId: 'another-poi', name: '生物厅' }]
const wrapper = await mountIndex()
const map = wrapper.getComponent(GuideMapShellStub)
map.vm.$emit('poi-click', {
...poi,
id: 'space-l2-temporary',
name: '生物厅临展空间',
kind: 'space',
hallId: 'space-l2-temporary',
primaryCategory: 'exhibition_hall',
primaryCategoryZh: '展厅'
})
await flushPromises()
expect(wrapper.find('.guide-poi-card').exists()).toBe(true)
expect(wrapper.find('.poi-card-actions').exists()).toBe(false)
expect(wrapper.text()).not.toContain('查看展厅')
expect(wrapper.text()).not.toContain('相关讲解')
})
it('展厅编号名称规范化后仍能关联真实讲解展厅', async () => {
testState.halls = [{ id: 'hall-biology', name: '生物厅' }]
const wrapper = await mountIndex()
const map = wrapper.getComponent(GuideMapShellStub)
map.vm.$emit('poi-click', {
...poi,
id: 'hall-space-biology',
name: '展厅_6生物厅',
kind: 'space',
hallId: 'space-biology',
primaryCategory: 'exhibition_hall',
primaryCategoryZh: '展厅'
})
await flushPromises()
const actions = wrapper.findAll('.poi-action')
expect(actions).toHaveLength(1)
await actions[0]!.trigger('tap')
await flushPromises()
expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain(
'/pages/explain/guide-stop-list?hallId=hall-biology'
)
})
it('模型点选展厅的唯一讲解入口进入讲解对象列表并保留一次性返回上下文', async () => {
testState.halls = [{ id: 'hall-l2', poiId: poi.id, name: '二层展厅' }] testState.halls = [{ id: 'hall-l2', poiId: poi.id, name: '二层展厅' }]
const wrapper = await mountIndex() const wrapper = await mountIndex()
const map = wrapper.getComponent(GuideMapShellStub) const map = wrapper.getComponent(GuideMapShellStub)
@@ -571,7 +619,7 @@ describe('首页搜索与地图闭环', () => {
primaryCategoryZh: '展厅' primaryCategoryZh: '展厅'
}) })
await flushPromises() await flushPromises()
await wrapper.findAll('.poi-action')[1]!.trigger('tap') await wrapper.find('.poi-action.primary').trigger('tap')
await flushPromises() await flushPromises()
expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain('/pages/explain/guide-stop-list?hallId=hall-l2') expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain('/pages/explain/guide-stop-list?hallId=hall-l2')