@@ -627,6 +627,12 @@ const resetSearchState = async () => {
|
|||||||
emitResultsState()
|
emitResultsState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从详情页返回时只清理搜索 UI 与分类 marker,不参与地图目标焦点的复位。
|
||||||
|
const returnToCollapsedAfterDetail = async () => {
|
||||||
|
await resetSearchState()
|
||||||
|
uni.hideKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
const handleCancel = async () => {
|
const handleCancel = async () => {
|
||||||
await resetSearchState()
|
await resetSearchState()
|
||||||
uni.hideKeyboard()
|
uni.hideKeyboard()
|
||||||
@@ -744,14 +750,27 @@ const handleSearchClear = async () => {
|
|||||||
await focusSearchInput()
|
await focusSearchInput()
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchShortcut = async (categoryInput: PoiCategoryDefinition | PoiCategoryId) => {
|
type SearchShortcutOptions = {
|
||||||
|
homeCategoryResults?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchShortcut = async (
|
||||||
|
categoryInput: PoiCategoryDefinition | PoiCategoryId,
|
||||||
|
options: SearchShortcutOptions = {}
|
||||||
|
) => {
|
||||||
const category = typeof categoryInput === 'string'
|
const category = typeof categoryInput === 'string'
|
||||||
? getPoiCategoryById(categoryInput)
|
? getPoiCategoryById(categoryInput)
|
||||||
: categoryInput
|
: categoryInput
|
||||||
if (!category) return
|
if (!category) return
|
||||||
|
|
||||||
const requestSeq = ++searchRequestSeq
|
const requestSeq = ++searchRequestSeq
|
||||||
const useHomeResultList = props.variant === 'home' && !homeExpanded.value
|
const useHomeResultList = props.variant === 'home'
|
||||||
|
&& (options.homeCategoryResults ?? !homeExpanded.value)
|
||||||
|
if (useHomeResultList) {
|
||||||
|
homeExpanded.value = false
|
||||||
|
searchInputFocused.value = false
|
||||||
|
uni.hideKeyboard()
|
||||||
|
}
|
||||||
searchKeyword.value = category.label
|
searchKeyword.value = category.label
|
||||||
searchDraftKeyword.value = category.label
|
searchDraftKeyword.value = category.label
|
||||||
activeCategoryId.value = category.id
|
activeCategoryId.value = category.id
|
||||||
@@ -778,7 +797,10 @@ const searchShortcut = async (categoryInput: PoiCategoryDefinition | PoiCategory
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleFacilityShortcut = (category: PoiCategoryDefinition) => {
|
const handleFacilityShortcut = (category: PoiCategoryDefinition) => {
|
||||||
void searchShortcut(category)
|
void searchShortcut(category, {
|
||||||
|
// 快捷入口始终进入首页底部分类结果;关键词确认仍保留全屏搜索结果。
|
||||||
|
homeCategoryResults: props.variant === 'home'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSearchConfirm = async (event?: any) => {
|
const handleSearchConfirm = async (event?: any) => {
|
||||||
@@ -959,6 +981,7 @@ defineExpose({
|
|||||||
expandHomePanel,
|
expandHomePanel,
|
||||||
collapseHomePanel,
|
collapseHomePanel,
|
||||||
resetSearchState,
|
resetSearchState,
|
||||||
|
returnToCollapsedAfterDetail,
|
||||||
focusSearchInput,
|
focusSearchInput,
|
||||||
searchShortcut,
|
searchShortcut,
|
||||||
retryCurrentSearch,
|
retryCurrentSearch,
|
||||||
|
|||||||
@@ -416,6 +416,7 @@ const guideMapShellRef = ref<{
|
|||||||
const homeSearchPanelRef = ref<{
|
const homeSearchPanelRef = ref<{
|
||||||
collapseHomePanel?: () => void
|
collapseHomePanel?: () => void
|
||||||
resetSearchState?: () => void
|
resetSearchState?: () => void
|
||||||
|
returnToCollapsedAfterDetail?: () => Promise<void>
|
||||||
restoreResultListScroll?: () => Promise<void>
|
restoreResultListScroll?: () => Promise<void>
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const homeSearchExpanded = ref(false)
|
const homeSearchExpanded = ref(false)
|
||||||
@@ -423,6 +424,7 @@ const homeCategoryModeActive = ref(false)
|
|||||||
const searchVisiblePoiIds = ref<string[] | null>(null)
|
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 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
|
||||||
@@ -1350,6 +1352,15 @@ const closeHomeSearchDock = () => {
|
|||||||
|
|
||||||
onShow(async () => {
|
onShow(async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
if (pendingCollapseSearchAfterDetail) {
|
||||||
|
pendingCollapseSearchAfterDetail = false
|
||||||
|
await homeSearchPanelRef.value?.returnToCollapsedAfterDetail?.()
|
||||||
|
homeSearchExpanded.value = false
|
||||||
|
homeCategoryModeActive.value = false
|
||||||
|
searchVisiblePoiIds.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await homeSearchPanelRef.value?.restoreResultListScroll?.()
|
await homeSearchPanelRef.value?.restoreResultListScroll?.()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1462,11 +1473,13 @@ const handleHomeSearchResultTap = async (poi: MuseumPoi, context: PoiSearchConte
|
|||||||
entrances: poi.entrances
|
entrances: poi.entrances
|
||||||
}, searchTargetFocusRequestId)
|
}, searchTargetFocusRequestId)
|
||||||
|
|
||||||
// 先把楼层与目标请求交给地图,再进入详情页;返回时列表组件仍保持原状态。
|
// 先把楼层与目标请求交给地图,再进入详情页;返回时仅收起搜索 UI,不清焦点。
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
pendingCollapseSearchAfterDetail = true
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: createSearchDetailUrl(poi, context),
|
url: createSearchDetailUrl(poi, context),
|
||||||
fail: () => {
|
fail: () => {
|
||||||
|
pendingCollapseSearchAfterDetail = false
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '点位详情打开失败,请重试',
|
title: '点位详情打开失败,请重试',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const testState = vi.hoisted(() => ({
|
|||||||
searchMountCount: 0,
|
searchMountCount: 0,
|
||||||
searchUnmountCount: 0,
|
searchUnmountCount: 0,
|
||||||
searchRestoreCount: 0,
|
searchRestoreCount: 0,
|
||||||
|
searchCollapseAfterDetailCount: 0,
|
||||||
onShowHandler: null as null | (() => Promise<void> | void)
|
onShowHandler: null as null | (() => Promise<void> | void)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -100,6 +101,9 @@ const PoiSearchPanelStub = defineComponent({
|
|||||||
methods: {
|
methods: {
|
||||||
collapseHomePanel() {},
|
collapseHomePanel() {},
|
||||||
resetSearchState() {},
|
resetSearchState() {},
|
||||||
|
returnToCollapsedAfterDetail() {
|
||||||
|
testState.searchCollapseAfterDetailCount += 1
|
||||||
|
},
|
||||||
restoreResultListScroll() {
|
restoreResultListScroll() {
|
||||||
testState.searchRestoreCount += 1
|
testState.searchRestoreCount += 1
|
||||||
}
|
}
|
||||||
@@ -151,6 +155,7 @@ beforeEach(() => {
|
|||||||
testState.searchMountCount = 0
|
testState.searchMountCount = 0
|
||||||
testState.searchUnmountCount = 0
|
testState.searchUnmountCount = 0
|
||||||
testState.searchRestoreCount = 0
|
testState.searchRestoreCount = 0
|
||||||
|
testState.searchCollapseAfterDetailCount = 0
|
||||||
testState.onShowHandler = null
|
testState.onShowHandler = null
|
||||||
window.history.replaceState({}, '', '/#/pages/index/index?tab=guide')
|
window.history.replaceState({}, '', '/#/pages/index/index?tab=guide')
|
||||||
vi.stubGlobal('uni', {
|
vi.stubGlobal('uni', {
|
||||||
@@ -187,7 +192,7 @@ describe('首页搜索与地图闭环', () => {
|
|||||||
expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
|
expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('跨楼层结果使用既有 target-focus,并携带返回上下文打开详情', async () => {
|
it('分类结果进入详情并返回后收起搜索、清空 marker,但保留目标焦点与楼层', 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)
|
||||||
@@ -222,6 +227,39 @@ describe('首页搜索与地图闭环', () => {
|
|||||||
expect(query.get('listScrollTop')).toBe('126')
|
expect(query.get('listScrollTop')).toBe('126')
|
||||||
expect(testState.searchMountCount).toBe(1)
|
expect(testState.searchMountCount).toBe(1)
|
||||||
expect(testState.searchUnmountCount).toBe(0)
|
expect(testState.searchUnmountCount).toBe(0)
|
||||||
|
|
||||||
|
await testState.onShowHandler?.()
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(testState.searchCollapseAfterDetailCount).toBe(1)
|
||||||
|
expect(testState.searchRestoreCount).toBe(0)
|
||||||
|
expect(map.props('activeFloor')).toBe('L2')
|
||||||
|
expect(map.props('visiblePoiIds')).toBeNull()
|
||||||
|
expect(map.props('targetFocusRequest')).toMatchObject({
|
||||||
|
poiId: poi.id,
|
||||||
|
floorId: 'L2'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('全屏搜索结果进入详情并返回后同样恢复为收缩态', async () => {
|
||||||
|
const wrapper = await mountIndex()
|
||||||
|
const search = wrapper.getComponent(PoiSearchPanelStub)
|
||||||
|
const map = wrapper.getComponent(GuideMapShellStub)
|
||||||
|
const fullSearchContext = {
|
||||||
|
...categoryContext,
|
||||||
|
categoryId: '' as const,
|
||||||
|
keyword: '二层卫生间'
|
||||||
|
}
|
||||||
|
|
||||||
|
search.vm.$emit('result-tap', poi, fullSearchContext)
|
||||||
|
await flushPromises()
|
||||||
|
await testState.onShowHandler?.()
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(testState.searchCollapseAfterDetailCount).toBe(1)
|
||||||
|
expect(testState.searchRestoreCount).toBe(0)
|
||||||
|
expect(map.props('visiblePoiIds')).toBeNull()
|
||||||
|
expect(map.props('targetFocusRequest')).toMatchObject({ poiId: poi.id })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('页面重新显示时要求搜索面板恢复原列表位置', async () => {
|
it('页面重新显示时要求搜索面板恢复原列表位置', async () => {
|
||||||
@@ -232,6 +270,27 @@ describe('首页搜索与地图闭环', () => {
|
|||||||
expect(testState.searchRestoreCount).toBe(1)
|
expect(testState.searchRestoreCount).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('详情打开失败会撤销收缩标记并保持当前搜索状态', async () => {
|
||||||
|
const wrapper = await mountIndex()
|
||||||
|
const search = wrapper.getComponent(PoiSearchPanelStub)
|
||||||
|
const map = wrapper.getComponent(GuideMapShellStub)
|
||||||
|
|
||||||
|
search.vm.$emit('category-mode-change', true)
|
||||||
|
search.vm.$emit('results-change', { ...categoryContext, active: true })
|
||||||
|
search.vm.$emit('result-tap', poi, categoryContext)
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const navigateToCall = vi.mocked(uni.navigateTo).mock.calls[0]?.[0]
|
||||||
|
navigateToCall?.fail?.({ errMsg: 'navigateTo:fail simulated' })
|
||||||
|
await testState.onShowHandler?.()
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(testState.searchCollapseAfterDetailCount).toBe(0)
|
||||||
|
expect(testState.searchRestoreCount).toBe(1)
|
||||||
|
expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
|
||||||
|
expect(map.props('targetFocusRequest')).toMatchObject({ poiId: poi.id })
|
||||||
|
})
|
||||||
|
|
||||||
it('地图点位卡隐藏首页 dock 时仍保留搜索组件,关闭后可继续使用原列表', async () => {
|
it('地图点位卡隐藏首页 dock 时仍保留搜索组件,关闭后可继续使用原列表', async () => {
|
||||||
const wrapper = await mountIndex()
|
const wrapper = await mountIndex()
|
||||||
const map = wrapper.getComponent(GuideMapShellStub)
|
const map = wrapper.getComponent(GuideMapShellStub)
|
||||||
|
|||||||
@@ -130,6 +130,91 @@ describe('POI 搜索面板实际渲染与状态闭环', () => {
|
|||||||
pageWrapper.unmount()
|
pageWrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('展开态快捷入口退出全屏并进入当前楼层的分类结果', async () => {
|
||||||
|
const wrapper = mount(PoiSearchPanel, {
|
||||||
|
props: {
|
||||||
|
variant: 'home',
|
||||||
|
currentFloorId: 'floor-1',
|
||||||
|
currentFloorLabel: '1F'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await flushMountedSearch()
|
||||||
|
|
||||||
|
await wrapper.get('.search-box').trigger('tap')
|
||||||
|
await flushMountedSearch()
|
||||||
|
expect(wrapper.find('.poi-search-content').exists()).toBe(true)
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="poi-category-restroom"]').trigger('tap')
|
||||||
|
await flushMountedSearch()
|
||||||
|
|
||||||
|
expect(wrapper.find('.poi-search-content').exists()).toBe(false)
|
||||||
|
expect(wrapper.find('[data-testid="poi-home-category-results"]').exists()).toBe(true)
|
||||||
|
expect(wrapper.get('[data-testid="poi-home-category-results"]').text())
|
||||||
|
.toContain('当前楼层 1F · 2 个点位')
|
||||||
|
expect(uni.hideKeyboard).toHaveBeenCalled()
|
||||||
|
expect((wrapper.emitted('results-change') || []).at(-1)?.[0]).toMatchObject({
|
||||||
|
active: true,
|
||||||
|
categoryId: 'restroom',
|
||||||
|
floorId: 'floor-1',
|
||||||
|
visiblePoiIds: ['restroom-1']
|
||||||
|
})
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('关键词确认命中分类时仍保留全屏搜索结果', async () => {
|
||||||
|
const wrapper = mount(PoiSearchPanel, {
|
||||||
|
props: {
|
||||||
|
variant: 'home',
|
||||||
|
currentFloorId: 'floor-1',
|
||||||
|
currentFloorLabel: '1F'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await flushMountedSearch()
|
||||||
|
|
||||||
|
await wrapper.get('.search-box').trigger('tap')
|
||||||
|
await wrapper.get('.search-input').trigger('confirm', {
|
||||||
|
detail: { value: '洗手间' }
|
||||||
|
})
|
||||||
|
await flushMountedSearch()
|
||||||
|
|
||||||
|
expect(wrapper.find('.poi-search-content').exists()).toBe(true)
|
||||||
|
expect(wrapper.find('[data-testid="poi-home-category-results"]').exists()).toBe(false)
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('详情返回专用复位恢复默认快捷入口并清空搜索状态', async () => {
|
||||||
|
const wrapper = mount(PoiSearchPanel, {
|
||||||
|
props: {
|
||||||
|
variant: 'home',
|
||||||
|
currentFloorId: 'floor-1',
|
||||||
|
currentFloorLabel: '1F'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await flushMountedSearch()
|
||||||
|
await wrapper.get('[data-testid="poi-category-restroom"]').trigger('tap')
|
||||||
|
await flushMountedSearch()
|
||||||
|
|
||||||
|
wrapper.get('[data-testid="poi-result-list"]').element.scrollTop = 126
|
||||||
|
await (wrapper.vm as unknown as {
|
||||||
|
returnToCollapsedAfterDetail: () => Promise<void>
|
||||||
|
}).returnToCollapsedAfterDetail()
|
||||||
|
await flushMountedSearch()
|
||||||
|
|
||||||
|
expect(wrapper.find('[data-testid="poi-home-category-results"]').exists()).toBe(false)
|
||||||
|
expect(wrapper.find('.poi-search-content').exists()).toBe(false)
|
||||||
|
expect(wrapper.findAll('.home-category-chip')).toHaveLength(5)
|
||||||
|
expect(wrapper.findAll('.home-category-chip.active')).toHaveLength(0)
|
||||||
|
expect((wrapper.get('.search-input').element as HTMLInputElement).value).toBe('')
|
||||||
|
expect((wrapper.emitted('results-change') || []).at(-1)?.[0]).toMatchObject({
|
||||||
|
active: false,
|
||||||
|
keyword: '',
|
||||||
|
categoryId: '',
|
||||||
|
visiblePoiIds: [],
|
||||||
|
listScrollTop: 0
|
||||||
|
})
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
it('首页分类只展示当前楼层结果,并上报可定位点位的精确 marker ID', async () => {
|
it('首页分类只展示当前楼层结果,并上报可定位点位的精确 marker ID', async () => {
|
||||||
const wrapper = mount(PoiSearchPanel, {
|
const wrapper = mount(PoiSearchPanel, {
|
||||||
props: {
|
props: {
|
||||||
@@ -210,7 +295,7 @@ describe('POI 搜索面板实际渲染与状态闭环', () => {
|
|||||||
await flushMountedSearch()
|
await flushMountedSearch()
|
||||||
expect(wrapper.get('[data-testid="poi-empty"]').text()).toContain('当前楼层暂无匹配点位')
|
expect(wrapper.get('[data-testid="poi-empty"]').text()).toContain('当前楼层暂无匹配点位')
|
||||||
expect((wrapper.emitted('results-change') || []).at(-1)?.[0]).toMatchObject({
|
expect((wrapper.emitted('results-change') || []).at(-1)?.[0]).toMatchObject({
|
||||||
active: false,
|
active: true,
|
||||||
visiblePoiIds: []
|
visiblePoiIds: []
|
||||||
})
|
})
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
|
|||||||
Reference in New Issue
Block a user