优化搜索点位详情页面切换体验
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-14 10:00:30 +08:00
parent abaeda189e
commit ef59570b0e
4 changed files with 95 additions and 51 deletions

View File

@@ -8,6 +8,7 @@
@back="handlePageBack" @back="handlePageBack"
> >
<GuideMapShell <GuideMapShell
v-if="mapContextReady"
:search-text="facility.name" :search-text="facility.name"
search-top="20px" search-top="20px"
floor-bottom="153px" floor-bottom="153px"
@@ -70,6 +71,9 @@
</view> </view>
</view> </view>
</GuideMapShell> </GuideMapShell>
<view v-else class="detail-map-loading" data-testid="facility-map-loading">
<text class="detail-map-loading-text">正在准备位置预览...</text>
</view>
</GuidePageFrame> </GuidePageFrame>
</template> </template>
@@ -117,6 +121,7 @@ const targetFocusRequest = ref<TargetPoiFocusRequestViewModel | null>(null)
const targetFocusRequestId = ref(0) const targetFocusRequestId = ref(0)
const locationErrorMessage = ref('') const locationErrorMessage = ref('')
const resolutionLoadFailed = ref(false) const resolutionLoadFailed = ref(false)
const mapContextReady = ref(false)
interface TargetPoiFocusResult { interface TargetPoiFocusResult {
requestId: number | string requestId: number | string
@@ -340,8 +345,10 @@ const tryResolveFacilityPoi = async () => {
} }
onLoad(async (options: Record<string, unknown> = {}) => { onLoad(async (options: Record<string, unknown> = {}) => {
mapContextReady.value = false
searchContext.value = parseFacilityDetailSearchContext(options) searchContext.value = parseFacilityDetailSearchContext(options)
resolutionLoadFailed.value = false resolutionLoadFailed.value = false
try {
await loadGuideFloors() await loadGuideFloors()
if (options.id) { if (options.id) {
@@ -372,7 +379,6 @@ onLoad(async (options: Record<string, unknown> = {}) => {
return return
} }
try {
const poi = await tryResolveFacilityPoi() const poi = await tryResolveFacilityPoi()
if (poi) { if (poi) {
facility.value = toFacilityDetailViewModel(poi) facility.value = toFacilityDetailViewModel(poi)
@@ -396,6 +402,9 @@ onLoad(async (options: Record<string, unknown> = {}) => {
} catch (error) { } catch (error) {
console.error('加载设施位置详情失败:', error) console.error('加载设施位置详情失败:', error)
locationErrorMessage.value = '位置数据加载失败,请稍后重试。' locationErrorMessage.value = '位置数据加载失败,请稍后重试。'
} finally {
// 目标楼层与聚焦请求准备完成后再挂载 ThreeMap避免默认楼层的中间帧。
mapContextReady.value = true
} }
}) })
@@ -443,6 +452,20 @@ const handlePageBack = () => {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.detail-map-loading {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: #f3f3f3;
}
.detail-map-loading-text {
color: #424754;
font-size: 14px;
}
.detail-sheet { .detail-sheet {
position: absolute; position: absolute;
left: 84px; left: 84px;

View File

@@ -428,6 +428,7 @@ const searchVisiblePoiIds = ref<string[] | null>(null)
const searchTargetFocusRequest = ref<TargetPoiFocusRequestViewModel | null>(null) const searchTargetFocusRequest = ref<TargetPoiFocusRequestViewModel | null>(null)
let searchTargetFocusRequestId = 0 let searchTargetFocusRequestId = 0
let pendingCollapseSearchAfterDetail = false let pendingCollapseSearchAfterDetail = false
let pendingSearchDetailFocusRequest: TargetPoiFocusRequestViewModel | null = null
let launchOverlayFallbackTimer: ReturnType<typeof setTimeout> | null = null let launchOverlayFallbackTimer: ReturnType<typeof setTimeout> | null = null
let launchOverlayFadeTimer: ReturnType<typeof setTimeout> | null = null let launchOverlayFadeTimer: ReturnType<typeof setTimeout> | null = null
@@ -1358,10 +1359,23 @@ onShow(async () => {
await nextTick() await nextTick()
if (pendingCollapseSearchAfterDetail) { if (pendingCollapseSearchAfterDetail) {
pendingCollapseSearchAfterDetail = false pendingCollapseSearchAfterDetail = false
const pendingFocusRequest = pendingSearchDetailFocusRequest
pendingSearchDetailFocusRequest = null
await homeSearchPanelRef.value?.returnToCollapsedAfterDetail?.() await homeSearchPanelRef.value?.returnToCollapsedAfterDetail?.()
homeSearchExpanded.value = false homeSearchExpanded.value = false
homeCategoryModeActive.value = false homeCategoryModeActive.value = false
searchVisiblePoiIds.value = null searchVisiblePoiIds.value = null
if (pendingFocusRequest) {
closeArrivalPanel()
showRoutePlanner.value = false
isSimulatingRoute.value = false
selectedGuidePoi.value = null
isPoiCardCollapsed.value = false
is3DMode.value = true
indoorView.value = 'floor'
activeGuideFloor.value = pendingFocusRequest.floorId
searchTargetFocusRequest.value = pendingFocusRequest
}
return return
} }
@@ -1455,16 +1469,8 @@ const createSearchDetailUrl = (poi: MuseumPoi, context: PoiSearchContext) => {
} }
const handleHomeSearchResultTap = async (poi: MuseumPoi, context: PoiSearchContext) => { const handleHomeSearchResultTap = async (poi: MuseumPoi, context: PoiSearchContext) => {
closeArrivalPanel()
showRoutePlanner.value = false
isSimulatingRoute.value = false
selectedGuidePoi.value = null
isPoiCardCollapsed.value = false
is3DMode.value = true
indoorView.value = 'floor'
activeGuideFloor.value = poi.floorId
searchTargetFocusRequestId += 1 searchTargetFocusRequestId += 1
searchTargetFocusRequest.value = toTargetFocusRequestViewModel({ pendingSearchDetailFocusRequest = toTargetFocusRequestViewModel({
poiId: poi.id, poiId: poi.id,
name: poi.name, name: poi.name,
floorId: poi.floorId, floorId: poi.floorId,
@@ -1478,13 +1484,14 @@ const handleHomeSearchResultTap = async (poi: MuseumPoi, context: PoiSearchConte
entrances: poi.entrances entrances: poi.entrances
}, searchTargetFocusRequestId) }, searchTargetFocusRequestId)
// 先把楼层与目标请求交给地图,再进入详情页;返回时仅收起搜索 UI不清焦点 // 详情页会一次性挂载目标楼层;首页不再先启动并销毁 ThreeMap
await nextTick() searchTargetFocusRequest.value = null
pendingCollapseSearchAfterDetail = true pendingCollapseSearchAfterDetail = true
uni.navigateTo({ uni.navigateTo({
url: createSearchDetailUrl(poi, context), url: createSearchDetailUrl(poi, context),
fail: () => { fail: () => {
pendingCollapseSearchAfterDetail = false pendingCollapseSearchAfterDetail = false
pendingSearchDetailFocusRequest = null
uni.showToast({ uni.showToast({
title: '点位详情打开失败,请重试', title: '点位详情打开失败,请重试',
icon: 'none' icon: 'none'

View File

@@ -143,6 +143,26 @@ afterEach(() => {
}) })
describe('点位详情实际渲染与返回闭环', () => { describe('点位详情实际渲染与返回闭环', () => {
it('解析目标楼层和点位后才挂载地图,避免默认楼层中间帧', async () => {
const wrapper = mountDetail()
expect(wrapper.find('[data-testid="guide-map-shell"]').exists()).toBe(false)
expect(wrapper.get('[data-testid="facility-map-loading"]').text()).toContain('正在准备位置预览')
await loadDetail({
id: encodeURIComponent(facilityPoi.id),
target: encodeURIComponent(facilityPoi.name),
floorId: facilityPoi.floorId,
floorLabel: facilityPoi.floorLabel
})
expect(wrapper.getComponent(GuideMapShellStub).props('activeFloor')).toBe('floor-l2')
expect(wrapper.getComponent(GuideMapShellStub).props('targetFocusRequest')).toMatchObject({
poiId: facilityPoi.id,
floorId: facilityPoi.floorId
})
})
it('只展示真实详情字段,并向地图发送正确楼层与点位定位请求', async () => { it('只展示真实详情字段,并向地图发送正确楼层与点位定位请求', async () => {
const wrapper = mountDetail() const wrapper = mountDetail()

View File

@@ -202,7 +202,7 @@ describe('首页搜索与地图闭环', () => {
expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds) expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
}) })
it('分类结果进入详情并返回后收起搜索、清空 marker但保留目标焦点与楼层', async () => { it('分类结果进入详情时不触发首页地图,返回后才恢复目标焦点与楼层', async () => {
const wrapper = await mountIndex() const wrapper = await mountIndex()
const search = wrapper.getComponent(PoiSearchPanelStub) const search = wrapper.getComponent(PoiSearchPanelStub)
const map = wrapper.getComponent(GuideMapShellStub) const map = wrapper.getComponent(GuideMapShellStub)
@@ -215,13 +215,7 @@ describe('首页搜索与地图闭环', () => {
search.vm.$emit('result-tap', poi, categoryContext) search.vm.$emit('result-tap', poi, categoryContext)
await flushPromises() await flushPromises()
expect(map.props('activeFloor')).toBe('L2') expect(map.props('targetFocusRequest')).toBeNull()
expect(map.props('targetFocusRequest')).toMatchObject({
poiId: poi.id,
floorId: 'L2',
floorLabel: '2F',
positionGltf: poi.positionGltf
})
const navigateTo = vi.mocked(uni.navigateTo) const navigateTo = vi.mocked(uni.navigateTo)
const detailUrl = navigateTo.mock.calls[0]?.[0]?.url as string const detailUrl = navigateTo.mock.calls[0]?.[0]?.url as string
@@ -298,7 +292,7 @@ describe('首页搜索与地图闭环', () => {
expect(testState.searchCollapseAfterDetailCount).toBe(0) expect(testState.searchCollapseAfterDetailCount).toBe(0)
expect(testState.searchRestoreCount).toBe(1) expect(testState.searchRestoreCount).toBe(1)
expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds) expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
expect(map.props('targetFocusRequest')).toMatchObject({ poiId: poi.id }) expect(map.props('targetFocusRequest')).toBeNull()
}) })
it('地图点位卡隐藏首页 dock 时仍保留搜索组件,关闭后可继续使用原列表', async () => { it('地图点位卡隐藏首页 dock 时仍保留搜索组件,关闭后可继续使用原列表', async () => {
@@ -342,7 +336,7 @@ describe('首页搜索与地图闭环', () => {
const detailUrl = vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url as string const detailUrl = vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url as string
expect(new URLSearchParams(detailUrl.split('?')[1]).get('resultCount')).toBe('1') expect(new URLSearchParams(detailUrl.split('?')[1]).get('resultCount')).toBe('1')
expect(map.props('targetFocusRequest')).not.toBeNull() expect(map.props('targetFocusRequest')).toBeNull()
search.vm.$emit('cancel') search.vm.$emit('cancel')
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()