统一首页点位详情目标解析
This commit is contained in:
@@ -248,7 +248,7 @@ const resolveFacilityPoi = async () => {
|
||||
if (poiById) return poiById
|
||||
|
||||
// 搜索结果必须按稳定 ID 解析,避免同名点位被定位到错误楼层。
|
||||
if (searchContext.value.source === 'search') return null
|
||||
if (searchContext.value.source === 'search' || searchContext.value.source === 'guide-poi') return null
|
||||
|
||||
const poiByHall = await resolvePoiFromHall()
|
||||
if (poiByHall) return poiByHall
|
||||
@@ -273,7 +273,10 @@ const resolveFacilityPoi = async () => {
|
||||
}
|
||||
|
||||
const resolveRenderPoi = async () => {
|
||||
const resolveByExactId = searchContext.value.source === 'search' && Boolean(facility.value.id)
|
||||
const resolveByExactId = (
|
||||
searchContext.value.source === 'search'
|
||||
|| searchContext.value.source === 'guide-poi'
|
||||
) && Boolean(facility.value.id)
|
||||
const normalizedTarget = normalizePoiName(facility.value.name)
|
||||
if (!resolveByExactId && !normalizedTarget) return null
|
||||
|
||||
|
||||
@@ -99,11 +99,11 @@
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="selectedGuidePoiExplainHall"
|
||||
v-if="selectedGuidePoiDetailTarget"
|
||||
class="poi-card-actions"
|
||||
>
|
||||
<view class="poi-action primary" @tap="handleViewSelectedHall">
|
||||
<text class="poi-action-text">查看展厅</text>
|
||||
<view class="poi-action primary" @tap="handleViewSelectedPoiDetail">
|
||||
<text class="poi-action-text">{{ selectedGuidePoiDetailActionText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -316,6 +316,11 @@ import {
|
||||
import {
|
||||
explainUseCase
|
||||
} from '@/usecases/explainUseCase'
|
||||
import {
|
||||
poiDetailUseCase,
|
||||
type PoiDetailSource,
|
||||
type PoiDetailTarget
|
||||
} from '@/usecases/poiDetailUseCase'
|
||||
import type {
|
||||
GuideRenderPoi
|
||||
} from '@/domain/guideModel'
|
||||
@@ -341,7 +346,6 @@ import type {
|
||||
GuideRouteResult,
|
||||
GuideRouteTarget,
|
||||
MuseumFloor,
|
||||
MuseumHall,
|
||||
MuseumPoi
|
||||
} from '@/domain/museum'
|
||||
import { getFloorSortLevel } from '@/domain/guideFloor'
|
||||
@@ -397,8 +401,19 @@ const loadingFloorId = ref('')
|
||||
const renderedFloorId = ref('')
|
||||
const failedFloorId = ref('')
|
||||
const selectedGuidePoi = ref<GuideRenderPoi | null>(null)
|
||||
const selectedGuidePoiExplainHall = ref<MuseumHall | null>(null)
|
||||
let selectedGuidePoiExplainHallRequestId = 0
|
||||
const selectedGuidePoiDetailTarget = ref<PoiDetailTarget | null>(null)
|
||||
let poiDetailResolutionRequestId = 0
|
||||
|
||||
type PoiDetailUiContext = Readonly<{
|
||||
source: PoiDetailSource
|
||||
categoryModeActive: boolean
|
||||
visiblePoiIds: string[] | null
|
||||
}>
|
||||
|
||||
let pendingPoiDetailUiContext: Readonly<{
|
||||
requestId: number
|
||||
value: PoiDetailUiContext
|
||||
}> | null = null
|
||||
|
||||
// 状态
|
||||
const currentTab = ref<GuideTopTab>(initialTopTab())
|
||||
@@ -726,9 +741,18 @@ const isGuidePoiHallLike = (poi: GuideRenderPoi | null) => Boolean(
|
||||
const selectedGuidePoiIsHall = computed(() => isGuidePoiHallLike(selectedGuidePoi.value))
|
||||
|
||||
const selectedGuidePoiCollapsedSubtitle = computed(() => (
|
||||
selectedGuidePoiExplainHall.value ? '点按展开查看展厅' : selectedGuidePoiSubtitle.value
|
||||
selectedGuidePoiDetailTarget.value?.detailType === 'hall'
|
||||
? '点按展开查看展厅'
|
||||
: selectedGuidePoiSubtitle.value
|
||||
))
|
||||
|
||||
const selectedGuidePoiDetailActionText = computed(() => {
|
||||
const detailType = selectedGuidePoiDetailTarget.value?.detailType
|
||||
if (detailType === 'hall') return '查看展厅'
|
||||
if (detailType === 'exhibit') return '查看展品'
|
||||
return '查看详情'
|
||||
})
|
||||
|
||||
const routeSimulationStepText = computed(() => {
|
||||
if (!activeRoutePreview.value) return '位置预览 · 查看位置关系'
|
||||
const hasConnector = activeRoutePreview.value.connectorPoints.length > 0
|
||||
@@ -1127,110 +1151,63 @@ const togglePoiCardCollapsed = () => {
|
||||
isPoiCardCollapsed.value = !isPoiCardCollapsed.value
|
||||
}
|
||||
|
||||
const normalizeHallMatchText = (value?: string) => (value || '')
|
||||
.trim()
|
||||
.replace(/[\s()()【】[\]_-]/g, '')
|
||||
.replace(/^展厅\d+/, '')
|
||||
.toLowerCase()
|
||||
|
||||
const resolvePoiExplainHall = async (poi: GuideRenderPoi): Promise<MuseumHall | null> => {
|
||||
const halls = await explainUseCase.listHalls()
|
||||
const matchedByPoiId = halls.find((hall) => (
|
||||
hall.poiId === poi.id
|
||||
|| hall.location?.poiId === poi.id
|
||||
))
|
||||
if (matchedByPoiId) return matchedByPoiId
|
||||
|
||||
const hallId = poi.hallId
|
||||
if (hallId) {
|
||||
const matchedById = halls.find((hall) => hall.id === hallId)
|
||||
if (matchedById) return matchedById
|
||||
}
|
||||
|
||||
const matchedByEntrance = halls.find((hall) => (
|
||||
(poi.entrances || []).some((entrance) => (
|
||||
hall.poiId === entrance.sourcePlaceId
|
||||
|| hall.poiId === entrance.id
|
||||
))
|
||||
))
|
||||
if (matchedByEntrance) return matchedByEntrance
|
||||
|
||||
const poiHallName = normalizeHallMatchText(poi.hallName || poi.name)
|
||||
return halls.find((hall) => {
|
||||
const hallName = normalizeHallMatchText(hall.name)
|
||||
return hallName && poiHallName && hallName === poiHallName
|
||||
}) || null
|
||||
}
|
||||
|
||||
watch(selectedGuidePoi, (poi) => {
|
||||
const requestId = ++selectedGuidePoiExplainHallRequestId
|
||||
selectedGuidePoiExplainHall.value = null
|
||||
if (!poi || !isGuidePoiHallLike(poi)) return
|
||||
const requestId = ++poiDetailResolutionRequestId
|
||||
selectedGuidePoiDetailTarget.value = null
|
||||
if (!poi) return
|
||||
|
||||
void resolvePoiExplainHall(poi)
|
||||
.then((hall) => {
|
||||
void poiDetailUseCase.resolve(poi)
|
||||
.then((target) => {
|
||||
if (
|
||||
requestId !== selectedGuidePoiExplainHallRequestId
|
||||
requestId !== poiDetailResolutionRequestId
|
||||
|| selectedGuidePoi.value?.id !== poi.id
|
||||
) return
|
||||
|
||||
selectedGuidePoiExplainHall.value = hall
|
||||
selectedGuidePoiDetailTarget.value = target
|
||||
})
|
||||
.catch((error) => {
|
||||
if (requestId !== selectedGuidePoiExplainHallRequestId) return
|
||||
console.warn('点位讲解展厅关联解析失败:', error)
|
||||
if (requestId !== poiDetailResolutionRequestId) return
|
||||
console.warn('点位详情目标解析失败:', error)
|
||||
})
|
||||
}, { flush: 'sync' })
|
||||
|
||||
const navigateToPoiDetail = ({
|
||||
url,
|
||||
floorId,
|
||||
failureMessage
|
||||
}: {
|
||||
url: string
|
||||
floorId: string
|
||||
failureMessage: string
|
||||
}) => {
|
||||
const returnContext = guideModelState.beginPoiDetailReturn(floorId)
|
||||
const navigateToPoiDetailTarget = (
|
||||
target: PoiDetailTarget,
|
||||
uiContext: PoiDetailUiContext
|
||||
) => {
|
||||
const returnContext = guideModelState.beginPoiDetailReturn(target.floorId)
|
||||
pendingPoiDetailUiContext = {
|
||||
requestId: returnContext.requestId,
|
||||
value: uiContext
|
||||
}
|
||||
uni.navigateTo({
|
||||
url,
|
||||
url: target.url,
|
||||
success: () => {
|
||||
guideModelState.confirmPoiDetailReturn(returnContext.requestId)
|
||||
},
|
||||
fail: () => {
|
||||
guideModelState.cancelPoiDetailReturn(returnContext.requestId)
|
||||
if (pendingPoiDetailUiContext?.requestId === returnContext.requestId) {
|
||||
pendingPoiDetailUiContext = null
|
||||
}
|
||||
uni.showToast({
|
||||
title: failureMessage,
|
||||
title: target.failureMessage,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const navigateFromSelectedPoiToDetail = (url: string) => {
|
||||
const poi = selectedGuidePoi.value
|
||||
if (!poi) return
|
||||
|
||||
// Detail pages do not own the mounted renderer. The homepage consumes this once on return.
|
||||
navigateToPoiDetail({
|
||||
url,
|
||||
floorId: poi.floorId,
|
||||
failureMessage: '讲解详情打开失败,请重试'
|
||||
const handleViewSelectedPoiDetail = () => {
|
||||
const target = selectedGuidePoiDetailTarget.value
|
||||
if (!target) return
|
||||
navigateToPoiDetailTarget(target, {
|
||||
source: 'map',
|
||||
categoryModeActive: false,
|
||||
visiblePoiIds: null
|
||||
})
|
||||
}
|
||||
|
||||
const navigateToHallExplainObjects = (hallId: string, hallName = '讲解') => {
|
||||
navigateFromSelectedPoiToDetail(
|
||||
explainGuideStopListUrl(hallId, hallName)
|
||||
)
|
||||
}
|
||||
|
||||
const handleViewSelectedHall = () => {
|
||||
const hall = selectedGuidePoiExplainHall.value
|
||||
if (!hall) return
|
||||
navigateToHallExplainObjects(hall.id, hall.name)
|
||||
}
|
||||
|
||||
const loadRouteTargets = async (keyword = '') => {
|
||||
routeTargetsLoading.value = true
|
||||
routeTargetsError.value = ''
|
||||
@@ -1579,10 +1556,20 @@ onShow(async () => {
|
||||
await nextTick()
|
||||
const detailReturnContext = guideModelState.consumePoiDetailReturn()
|
||||
if (detailReturnContext) {
|
||||
const uiContext = pendingPoiDetailUiContext?.requestId === detailReturnContext.requestId
|
||||
? pendingPoiDetailUiContext.value
|
||||
: null
|
||||
pendingPoiDetailUiContext = null
|
||||
await resetGuideModelToFloorBaseline({
|
||||
rendererReason: 'poi-detail-close',
|
||||
floorId: detailReturnContext.floorId
|
||||
})
|
||||
if (uiContext?.source === 'search') {
|
||||
homeCategoryModeActive.value = uiContext.categoryModeActive
|
||||
searchVisiblePoiIds.value = uiContext.visiblePoiIds
|
||||
? [...uiContext.visiblePoiIds]
|
||||
: null
|
||||
}
|
||||
await homeSearchPanelRef.value?.restoreResultListScroll?.()
|
||||
return
|
||||
}
|
||||
@@ -1658,38 +1645,28 @@ const handleHomeSearchResultsChange = (state: PoiCategoryResultState) => {
|
||||
}
|
||||
}
|
||||
|
||||
const createSearchDetailUrl = (poi: MuseumPoi, context: PoiSearchContext) => {
|
||||
const resultCount = homeCategoryModeActive.value
|
||||
? context.floorResultCount
|
||||
: context.resultCount
|
||||
const params = new URLSearchParams({
|
||||
source: 'search',
|
||||
searchOrigin: context.origin,
|
||||
searchKeyword: context.keyword,
|
||||
searchCategoryId: context.categoryId,
|
||||
searchFloorId: context.floorId,
|
||||
searchFloorLabel: context.floorLabel,
|
||||
resultCount: String(Math.max(1, resultCount)),
|
||||
floorResultCount: String(context.floorResultCount),
|
||||
visiblePoiIds: context.visiblePoiIds.join(','),
|
||||
listScrollTop: String(context.listScrollTop),
|
||||
id: poi.id,
|
||||
target: poi.name,
|
||||
floorId: poi.floorId,
|
||||
floorLabel: poi.floorLabel
|
||||
})
|
||||
|
||||
return `/pages/facility/detail?${params.toString()}`
|
||||
}
|
||||
|
||||
const handleHomeSearchResultTap = async (poi: MuseumPoi, context: PoiSearchContext) => {
|
||||
// The homepage keeps its renderer mounted; detail close is consumed by onShow once.
|
||||
const requestId = ++poiDetailResolutionRequestId
|
||||
const categoryModeActive = homeCategoryModeActive.value
|
||||
const visiblePoiIds = categoryModeActive
|
||||
? [...context.visiblePoiIds]
|
||||
: searchVisiblePoiIds.value
|
||||
? [...searchVisiblePoiIds.value]
|
||||
: null
|
||||
searchTargetFocusRequest.value = null
|
||||
navigateToPoiDetail({
|
||||
url: createSearchDetailUrl(poi, context),
|
||||
floorId: poi.floorId,
|
||||
failureMessage: '点位详情打开失败,请重试'
|
||||
})
|
||||
try {
|
||||
const target = await poiDetailUseCase.resolve(poi)
|
||||
if (requestId !== poiDetailResolutionRequestId) return
|
||||
navigateToPoiDetailTarget(target, {
|
||||
source: 'search',
|
||||
categoryModeActive,
|
||||
visiblePoiIds
|
||||
})
|
||||
} catch (error) {
|
||||
if (requestId !== poiDetailResolutionRequestId) return
|
||||
console.warn('搜索点位详情目标解析失败:', error)
|
||||
uni.showToast({ title: '点位详情解析失败,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const handleHomeSearchCancel = () => {
|
||||
|
||||
159
src/usecases/poiDetailUseCase.ts
Normal file
159
src/usecases/poiDetailUseCase.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import type {
|
||||
MuseumCategory,
|
||||
MuseumHall,
|
||||
MuseumPoiEntrance,
|
||||
MuseumPoiKind
|
||||
} from '@/domain/museum'
|
||||
import { explainUseCase } from '@/usecases/explainUseCase'
|
||||
import { explainGuideStopListUrl } from '@/utils/explainNavigation'
|
||||
|
||||
export type PoiDetailType = 'hall' | 'exhibit' | 'facility'
|
||||
|
||||
export type PoiDetailSource = 'map' | 'search'
|
||||
|
||||
export interface PoiDetailSourcePoi {
|
||||
id: string
|
||||
name: string
|
||||
floorId: string
|
||||
floorLabel?: string
|
||||
primaryCategory?: MuseumCategory | string
|
||||
kind?: MuseumPoiKind
|
||||
hallId?: string
|
||||
hallName?: string
|
||||
exhibitId?: string
|
||||
sourcePlaceId?: string
|
||||
entrances?: MuseumPoiEntrance[]
|
||||
}
|
||||
|
||||
export interface PoiDetailTarget {
|
||||
poiId: string
|
||||
floorId: string
|
||||
detailType: PoiDetailType
|
||||
detailId: string
|
||||
hallId?: string
|
||||
exhibitId?: string
|
||||
url: string
|
||||
failureMessage: string
|
||||
}
|
||||
|
||||
const normalizeMatchText = (value?: string) => (value || '')
|
||||
.trim()
|
||||
.replace(/[\s()()【】[\]_-]/g, '')
|
||||
.replace(/^展厅\d*/, '')
|
||||
.toLowerCase()
|
||||
|
||||
const categoryIdOf = (poi: PoiDetailSourcePoi) => (
|
||||
typeof poi.primaryCategory === 'string'
|
||||
? poi.primaryCategory
|
||||
: poi.primaryCategory?.id || ''
|
||||
)
|
||||
|
||||
const isHallNameFallbackAllowed = (poi: PoiDetailSourcePoi) => {
|
||||
const categoryId = categoryIdOf(poi)
|
||||
return (
|
||||
poi.kind === 'hall'
|
||||
|| poi.kind === 'hall_entrance'
|
||||
|| categoryId === 'touring_poi'
|
||||
|| categoryId === 'exhibition_hall'
|
||||
|| categoryId === 'exhibition_hall_entrance'
|
||||
)
|
||||
}
|
||||
|
||||
const appendQuery = (url: string, values: Record<string, string | number>) => {
|
||||
const [path, query = ''] = url.split('?')
|
||||
const params = new URLSearchParams(query)
|
||||
Object.entries(values).forEach(([key, value]) => params.set(key, String(value)))
|
||||
return `${path}?${params.toString()}`
|
||||
}
|
||||
|
||||
const createCanonicalDetailUrl = (url: string, poi: PoiDetailSourcePoi) => (
|
||||
// 详情 URL 只承载稳定目标,搜索筛选等首页状态由调用方单独保存。
|
||||
appendQuery(url, {
|
||||
source: 'guide-poi',
|
||||
poiId: poi.id,
|
||||
floorId: poi.floorId
|
||||
})
|
||||
)
|
||||
|
||||
const resolveHallByStableRelationship = (
|
||||
poi: PoiDetailSourcePoi,
|
||||
halls: MuseumHall[]
|
||||
) => {
|
||||
if (poi.hallId) {
|
||||
const hall = halls.find((item) => item.id === poi.hallId)
|
||||
if (hall) return hall
|
||||
}
|
||||
|
||||
const stablePoiIds = new Set([
|
||||
poi.id,
|
||||
poi.sourcePlaceId,
|
||||
...(poi.entrances || []).flatMap((entrance) => [
|
||||
entrance.id,
|
||||
entrance.sourcePlaceId
|
||||
])
|
||||
].filter((id): id is string => Boolean(id)))
|
||||
|
||||
return halls.find((hall) => (
|
||||
(hall.poiId ? stablePoiIds.has(hall.poiId) : false)
|
||||
|| (hall.location?.poiId ? stablePoiIds.has(hall.location.poiId) : false)
|
||||
)) || null
|
||||
}
|
||||
|
||||
const resolveHallByControlledNameFallback = (
|
||||
poi: PoiDetailSourcePoi,
|
||||
halls: MuseumHall[]
|
||||
) => {
|
||||
// 名称只对明确的展厅语义兜底,避免普通空间误入讲解列表。
|
||||
if (!isHallNameFallbackAllowed(poi)) return null
|
||||
const poiHallName = normalizeMatchText(poi.hallName || poi.name)
|
||||
if (!poiHallName) return null
|
||||
return halls.find((hall) => normalizeMatchText(hall.name) === poiHallName) || null
|
||||
}
|
||||
|
||||
export class PoiDetailUseCase {
|
||||
async resolve(poi: PoiDetailSourcePoi): Promise<PoiDetailTarget> {
|
||||
if (poi.exhibitId) {
|
||||
return {
|
||||
poiId: poi.id,
|
||||
floorId: poi.floorId,
|
||||
detailType: 'exhibit',
|
||||
detailId: poi.exhibitId,
|
||||
exhibitId: poi.exhibitId,
|
||||
url: createCanonicalDetailUrl(`/pages/exhibit/detail?id=${encodeURIComponent(poi.exhibitId)}`, poi),
|
||||
failureMessage: '展品详情打开失败,请重试'
|
||||
}
|
||||
}
|
||||
|
||||
const halls = await explainUseCase.listHalls().catch((error) => {
|
||||
console.warn('展厅关联数据加载失败,点位详情降级为普通设施:', error)
|
||||
return []
|
||||
})
|
||||
const hall = resolveHallByStableRelationship(poi, halls)
|
||||
|| resolveHallByControlledNameFallback(poi, halls)
|
||||
if (hall) {
|
||||
return {
|
||||
poiId: poi.id,
|
||||
floorId: poi.floorId,
|
||||
detailType: 'hall',
|
||||
detailId: hall.id,
|
||||
hallId: hall.id,
|
||||
url: createCanonicalDetailUrl(explainGuideStopListUrl(hall.id, hall.name), poi),
|
||||
failureMessage: '展厅讲解列表打开失败,请重试'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
poiId: poi.id,
|
||||
floorId: poi.floorId,
|
||||
detailType: 'facility',
|
||||
detailId: poi.id,
|
||||
url: createCanonicalDetailUrl(
|
||||
`/pages/facility/detail?id=${encodeURIComponent(poi.id)}&target=${encodeURIComponent(poi.name)}`,
|
||||
poi
|
||||
),
|
||||
failureMessage: '点位详情打开失败,请重试'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const poiDetailUseCase = new PoiDetailUseCase()
|
||||
@@ -25,7 +25,7 @@ export interface FacilityDetailViewModel {
|
||||
}
|
||||
|
||||
export interface FacilityDetailSearchContext {
|
||||
source: 'search' | 'direct'
|
||||
source: 'search' | 'guide-poi' | 'direct'
|
||||
searchOrigin: 'home' | 'page' | ''
|
||||
searchKeyword: string
|
||||
searchCategoryId: string
|
||||
@@ -84,6 +84,7 @@ const parseNonNegativeInteger = (value: unknown) => {
|
||||
export const parseFacilityDetailSearchContext = (
|
||||
options: Record<string, unknown> = {}
|
||||
): FacilityDetailSearchContext => {
|
||||
const source = decodeQueryText(options.source)
|
||||
const searchOrigin = decodeQueryText(options.searchOrigin)
|
||||
const visiblePoiIds = decodeQueryText(options.visiblePoiIds)
|
||||
.split(',')
|
||||
@@ -91,7 +92,7 @@ export const parseFacilityDetailSearchContext = (
|
||||
.filter(Boolean)
|
||||
|
||||
return {
|
||||
source: decodeQueryText(options.source) === 'search' ? 'search' : 'direct',
|
||||
source: source === 'search' || source === 'guide-poi' ? source : 'direct',
|
||||
searchOrigin: searchOrigin === 'home' || searchOrigin === 'page' ? searchOrigin : '',
|
||||
searchKeyword: decodeQueryText(options.searchKeyword),
|
||||
searchCategoryId: decodeQueryText(options.searchCategoryId),
|
||||
|
||||
@@ -20,6 +20,12 @@ const testState = vi.hoisted(() => ({
|
||||
searchResetCount: 0,
|
||||
onShowHandler: null as null | (() => Promise<void> | void),
|
||||
halls: [] as Array<{ id: string; poiId?: string; location?: { poiId?: string }; name: string }>,
|
||||
hallResolutionPromises: [] as Array<Promise<Array<{
|
||||
id: string
|
||||
poiId?: string
|
||||
location?: { poiId?: string }
|
||||
name: string
|
||||
}>>>,
|
||||
explainResults: [] as Array<{
|
||||
type: 'hall' | 'exhibit'
|
||||
hallId?: string
|
||||
@@ -76,7 +82,9 @@ vi.mock('@/usecases/explainUseCase', () => ({
|
||||
explainUseCase: {
|
||||
loadExplainHalls: async () => [],
|
||||
loadExplainHallSummaries: async () => ({}),
|
||||
listHalls: async () => testState.halls,
|
||||
listHalls: async () => (
|
||||
testState.hallResolutionPromises.shift() || Promise.resolve(testState.halls)
|
||||
),
|
||||
searchExplain: async () => testState.explainResults
|
||||
}
|
||||
}))
|
||||
@@ -211,6 +219,7 @@ beforeEach(() => {
|
||||
testState.searchResetCount = 0
|
||||
testState.onShowHandler = null
|
||||
testState.halls = []
|
||||
testState.hallResolutionPromises = []
|
||||
testState.explainResults = []
|
||||
window.history.replaceState({}, '', '/#/pages/index/index?tab=guide')
|
||||
vi.stubGlobal('uni', {
|
||||
@@ -403,15 +412,10 @@ describe('首页搜索与地图闭环', () => {
|
||||
const navigateTo = vi.mocked(uni.navigateTo)
|
||||
const detailUrl = navigateTo.mock.calls[0]?.[0]?.url as string
|
||||
const query = new URLSearchParams(detailUrl.split('?')[1])
|
||||
expect(query.get('source')).toBe('search')
|
||||
expect(query.get('searchOrigin')).toBe('home')
|
||||
expect(query.get('searchKeyword')).toBe('卫生间')
|
||||
expect(query.get('searchCategoryId')).toBe('restroom')
|
||||
expect(query.get('searchFloorId')).toBe('L2')
|
||||
expect(query.get('resultCount')).toBe('2')
|
||||
expect(query.get('floorResultCount')).toBe('2')
|
||||
expect(query.get('visiblePoiIds')).toBe(categoryContext.visiblePoiIds.join(','))
|
||||
expect(query.get('listScrollTop')).toBe('126')
|
||||
expect(query.get('source')).toBe('guide-poi')
|
||||
expect(query.get('id')).toBe(poi.id)
|
||||
expect(query.get('floorId')).toBe('L2')
|
||||
expect(query.has('searchKeyword')).toBe(false)
|
||||
expect(testState.searchMountCount).toBe(1)
|
||||
expect(testState.searchUnmountCount).toBe(0)
|
||||
|
||||
@@ -422,7 +426,7 @@ describe('首页搜索与地图闭环', () => {
|
||||
expect(testState.searchResetCount).toBe(0)
|
||||
expect(testState.searchRestoreCount).toBeGreaterThan(0)
|
||||
expect(map.props('activeFloor')).toBe('L2')
|
||||
expect(map.props('visiblePoiIds')).toBeNull()
|
||||
expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
|
||||
expect(map.props('targetFocusRequest')).toBeNull()
|
||||
|
||||
const resetCount = testState.searchResetCount
|
||||
@@ -560,7 +564,74 @@ describe('首页搜索与地图闭环', () => {
|
||||
expect(wrapper.find('.guide-poi-card').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('展厅类型空间没有讲解展厅关联时不显示展厅与讲解按钮', async () => {
|
||||
it('同一个展厅从地图和搜索进入时使用相同详情路由和 hallId', async () => {
|
||||
const hallPoi = {
|
||||
...poi,
|
||||
kind: 'hall' as const,
|
||||
hallId: 'hall-l2',
|
||||
primaryCategory: { id: 'exhibition_hall', label: '展厅' }
|
||||
}
|
||||
testState.halls = [{ id: 'hall-l2', poiId: poi.id, name: '二层展厅' }]
|
||||
const wrapper = await mountIndex()
|
||||
const map = wrapper.getComponent(GuideMapShellStub)
|
||||
const search = wrapper.getComponent(PoiSearchPanelStub)
|
||||
|
||||
map.vm.$emit('poi-click', {
|
||||
...hallPoi,
|
||||
primaryCategory: 'exhibition_hall',
|
||||
primaryCategoryZh: '展厅'
|
||||
})
|
||||
await flushPromises()
|
||||
await wrapper.find('.poi-action.primary').trigger('tap')
|
||||
await flushPromises()
|
||||
const mapUrl = vi.mocked(uni.navigateTo).mock.calls.at(-1)?.[0]?.url as string
|
||||
vi.mocked(uni.navigateTo).mock.calls.at(-1)?.[0]?.fail?.({ errMsg: 'navigateTo:fail test cleanup' })
|
||||
|
||||
search.vm.$emit('result-tap', hallPoi, categoryContext)
|
||||
await flushPromises()
|
||||
const searchUrl = vi.mocked(uni.navigateTo).mock.calls.at(-1)?.[0]?.url as string
|
||||
const mapRoute = mapUrl.split('?')[0]
|
||||
const searchRoute = searchUrl.split('?')[0]
|
||||
const mapQuery = new URLSearchParams(mapUrl.split('?')[1])
|
||||
const searchQuery = new URLSearchParams(searchUrl.split('?')[1])
|
||||
|
||||
expect(mapRoute).toBe('/pages/explain/guide-stop-list')
|
||||
expect(searchRoute).toBe(mapRoute)
|
||||
expect(mapQuery.get('hallId')).toBe('hall-l2')
|
||||
expect(searchQuery.get('hallId')).toBe(mapQuery.get('hallId'))
|
||||
expect(searchUrl).toBe(mapUrl)
|
||||
})
|
||||
|
||||
it('同一个普通设施从地图和搜索进入时都解析为设施详情', async () => {
|
||||
const wrapper = await mountIndex()
|
||||
const map = wrapper.getComponent(GuideMapShellStub)
|
||||
const search = wrapper.getComponent(PoiSearchPanelStub)
|
||||
|
||||
map.vm.$emit('poi-click', {
|
||||
...poi,
|
||||
primaryCategory: 'restroom',
|
||||
primaryCategoryZh: '卫生间'
|
||||
})
|
||||
await flushPromises()
|
||||
await wrapper.find('.poi-card-collapsed').trigger('tap')
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.find('.poi-action.primary').trigger('tap')
|
||||
await flushPromises()
|
||||
const mapUrl = vi.mocked(uni.navigateTo).mock.calls.at(-1)?.[0]?.url as string
|
||||
vi.mocked(uni.navigateTo).mock.calls.at(-1)?.[0]?.fail?.({ errMsg: 'navigateTo:fail test cleanup' })
|
||||
|
||||
search.vm.$emit('result-tap', poi, categoryContext)
|
||||
await flushPromises()
|
||||
const searchUrl = vi.mocked(uni.navigateTo).mock.calls.at(-1)?.[0]?.url as string
|
||||
|
||||
expect(mapUrl.split('?')[0]).toBe('/pages/facility/detail')
|
||||
expect(searchUrl.split('?')[0]).toBe('/pages/facility/detail')
|
||||
expect(new URLSearchParams(mapUrl.split('?')[1]).get('id')).toBe(poi.id)
|
||||
expect(new URLSearchParams(searchUrl.split('?')[1]).get('id')).toBe(poi.id)
|
||||
expect(searchUrl).toBe(mapUrl)
|
||||
})
|
||||
|
||||
it('展厅类型空间没有讲解展厅关联时只进入普通点位详情,不显示虚假讲解入口', async () => {
|
||||
testState.halls = [{ id: 'hall-biology', poiId: 'another-poi', name: '生物厅' }]
|
||||
const wrapper = await mountIndex()
|
||||
const map = wrapper.getComponent(GuideMapShellStub)
|
||||
@@ -577,9 +648,53 @@ describe('首页搜索与地图闭环', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('.guide-poi-card').exists()).toBe(true)
|
||||
expect(wrapper.find('.poi-card-actions').exists()).toBe(false)
|
||||
expect(wrapper.find('.poi-card-actions').exists()).toBe(true)
|
||||
expect(wrapper.text()).not.toContain('查看展厅')
|
||||
expect(wrapper.text()).not.toContain('相关讲解')
|
||||
expect(wrapper.text()).toContain('查看详情')
|
||||
await wrapper.find('.poi-action.primary').trigger('tap')
|
||||
await flushPromises()
|
||||
expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain('/pages/facility/detail?')
|
||||
})
|
||||
|
||||
it('快速切换点位时忽略上一个点位较晚返回的展厅关联结果', async () => {
|
||||
let resolveFirst!: (halls: typeof testState.halls) => void
|
||||
let resolveSecond!: (halls: typeof testState.halls) => void
|
||||
testState.hallResolutionPromises = [
|
||||
new Promise((resolve) => { resolveFirst = resolve }),
|
||||
new Promise((resolve) => { resolveSecond = resolve })
|
||||
]
|
||||
const wrapper = await mountIndex()
|
||||
const map = wrapper.getComponent(GuideMapShellStub)
|
||||
const firstPoi = {
|
||||
...poi,
|
||||
id: 'poi-hall-first',
|
||||
kind: 'hall' as const,
|
||||
hallId: 'hall-first',
|
||||
primaryCategory: 'exhibition_hall',
|
||||
primaryCategoryZh: '展厅'
|
||||
}
|
||||
const secondPoi = {
|
||||
...poi,
|
||||
id: 'poi-hall-second',
|
||||
kind: 'hall' as const,
|
||||
hallId: 'hall-second',
|
||||
primaryCategory: 'exhibition_hall',
|
||||
primaryCategoryZh: '展厅'
|
||||
}
|
||||
|
||||
map.vm.$emit('poi-click', firstPoi)
|
||||
map.vm.$emit('poi-click', secondPoi)
|
||||
resolveSecond([{ id: 'hall-second', poiId: secondPoi.id, name: '第二展厅' }])
|
||||
await flushPromises()
|
||||
resolveFirst([{ id: 'hall-first', poiId: firstPoi.id, name: '第一展厅' }])
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain(secondPoi.name)
|
||||
await wrapper.find('.poi-action.primary').trigger('tap')
|
||||
await flushPromises()
|
||||
const url = vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url as string
|
||||
expect(new URLSearchParams(url.split('?')[1]).get('hallId')).toBe('hall-second')
|
||||
})
|
||||
|
||||
it('展厅编号名称规范化后仍能关联真实讲解展厅', async () => {
|
||||
@@ -746,7 +861,7 @@ describe('首页搜索与地图闭环', () => {
|
||||
expect(testState.resetToViewBaselineCalls).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('单结果分类写入 resultCount=1,主动取消恢复默认标记并清除目标', async () => {
|
||||
it('单结果分类使用统一详情目标,主动取消恢复默认标记并清除目标', async () => {
|
||||
const wrapper = await mountIndex()
|
||||
const search = wrapper.getComponent(PoiSearchPanelStub)
|
||||
const map = wrapper.getComponent(GuideMapShellStub)
|
||||
@@ -762,7 +877,7 @@ describe('首页搜索与地图闭环', () => {
|
||||
await flushPromises()
|
||||
|
||||
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('source')).toBe('guide-poi')
|
||||
expect(map.props('targetFocusRequest')).toBeNull()
|
||||
|
||||
search.vm.$emit('cancel')
|
||||
|
||||
Reference in New Issue
Block a user