diff --git a/src/components/map/ThreeMap.vue b/src/components/map/ThreeMap.vue index 2e5b539..d95caaf 100644 --- a/src/components/map/ThreeMap.vue +++ b/src/components/map/ThreeMap.vue @@ -5527,7 +5527,8 @@ const handleFloorChange = async (floorId: string) => { const didCommit = await loadFloor(requestedFloorId, { preserveCurrentSceneUntilReady: true, suppressProgress: false, - detachPoiBeforeLoad: true + detachPoiBeforeLoad: true, + applyFloorBaseline: true }) updatePoiVisibilityByDistance() if (didCommit) { diff --git a/src/data/providers/sgsSdkApiProvider.ts b/src/data/providers/sgsSdkApiProvider.ts index 6f864c1..58fa2ca 100644 --- a/src/data/providers/sgsSdkApiProvider.ts +++ b/src/data/providers/sgsSdkApiProvider.ts @@ -371,10 +371,17 @@ const parseJsonPayload = (payload: unknown, requestUrl: string, contentType: return payload as T } +const inFlightRequests = new Map>() + const requestJson = ( path: string, options: { method?: 'GET' | 'POST'; data?: unknown } = {} -): Promise => new Promise((resolve, reject) => { +): Promise => { + const requestKey = `${options.method || 'GET'}:${path}:${JSON.stringify(options.data || {})}` + const existing = inFlightRequests.get(requestKey) + if (existing) return existing as Promise + + const request = new Promise((resolve, reject) => { const baseUrl = resolveAppApiBaseUrl() const requestUrl = `${baseUrl}${path}` @@ -412,7 +419,11 @@ const requestJson = ( reject(new Error(`SGS 数据接口网络失败: ${path} ${JSON.stringify(error)}`)) } }) -}) + }) + inFlightRequests.set(requestKey, request) + void request.finally(() => inFlightRequests.delete(requestKey)).catch(() => undefined) + return request +} const buildQueryString = (params: object) => { const query = new URLSearchParams() diff --git a/src/domain/guideReadiness.ts b/src/domain/guideReadiness.ts index 951d8b4..7950fd6 100644 --- a/src/domain/guideReadiness.ts +++ b/src/domain/guideReadiness.ts @@ -4,7 +4,7 @@ import type { export const NAV_ROUTE_GRAPH_READY = false -export const NAV_ROUTE_UNAVAILABLE_MESSAGE = '当前可查看位置预览、位置关系和馆外参考,暂不提供正式室内导航' +export const NAV_ROUTE_UNAVAILABLE_MESSAGE = '当前可查看馆内三维位置预览和馆外入口参考' export const NAV_ROUTE_READINESS: GuideRouteReadiness = { ready: NAV_ROUTE_GRAPH_READY, diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue index 2e459c3..aeaa997 100644 --- a/src/pages/index/index.vue +++ b/src/pages/index/index.vue @@ -1573,9 +1573,7 @@ const resetGuideModelToFloorBaseline = async ({ indoorView.value = 'floor' activeGuideFloor.value = floorId || initial.defaultActiveFloorId || activeGuideFloor.value closeArrivalPanel() - await homeSearchPanelRef.value?.resetSearchState?.() if (!guideModelState.isCurrentRequest(resetRequest.requestId)) return - closeHomeSearchDock() const resetResult = await guideMapShellRef.value?.resetToViewBaseline?.({ view: 'floor', @@ -1600,6 +1598,7 @@ onShow(async () => { rendererReason: 'poi-detail-close', floorId: detailReturnContext.floorId }) + await homeSearchPanelRef.value?.restoreResultListScroll?.() return } diff --git a/src/pages/route/detail.vue b/src/pages/route/detail.vue index 9d33823..2aaf450 100644 --- a/src/pages/route/detail.vue +++ b/src/pages/route/detail.vue @@ -89,7 +89,18 @@ - + + 正在解析目标位置 + 正在加载所需的三维位置数据。 + + + + 目标位置加载失败 + {{ routeResolveError || '请检查网络后重试。' }} + + + + 请选择目标位置 需要先选择展厅或设施,才能查看馆内三维位置。 @@ -132,7 +143,7 @@ {{ routePlanError }} - + 起点 {{ selectedStartPoint ? `${selectedStartPoint.name} · ${selectedStartPoint.floorLabel}` : '请选择起点' }} @@ -140,6 +151,7 @@ @@ -241,6 +253,9 @@ const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloor const route = ref(initialLocationPreviewPlan()) const hasRouteTarget = ref(false) +const routeResolveState = ref<'idle' | 'loading' | 'success' | 'error'>('idle') +const routeResolveError = ref('') +const lastRouteOptions = ref>({}) const routeViewState = ref('preview') const activeFloor = ref('1F') const targetFocusRequest = ref(null) @@ -388,8 +403,8 @@ const indoorGuideDesc = computed(() => { } return routeReady.value - ? '可选择起点查看馆内位置关系,当前不作为正式室内导航。' - : routeReadinessMessage.value || '当前可查看位置预览,暂不提供正式室内导航。' + ? '可选择明确起点查看位置关系。' + : routeReadinessMessage.value || '当前可查看馆内三维位置预览和馆外入口参考。' }) const indoorGuideDragOffset = computed(() => Math.max( @@ -508,9 +523,7 @@ const requestTargetFocus = (target: Partial | null, fallbac const applyRouteOptions = async (options: Record = {}) => { const requestSequence = ++routeOptionsSequence - await loadGuideFloors() - await refreshRouteReadinessState() - if (requestSequence !== routeOptionsSequence) return false + lastRouteOptions.value = options const facilityId = normalizeStringOption(options.facilityId) const target = normalizeStringOption(options.target) @@ -518,6 +531,7 @@ const applyRouteOptions = async (options: Record = {}) => { const startLocation = createStartLocationFromOptions(options) if (!facilityId && !target) { + await Promise.all([loadGuideFloors(), refreshRouteReadinessState()]) clearLastGuideLocationPreviewUrl() hasRouteTarget.value = false route.value = initialLocationPreviewPlan() @@ -527,10 +541,22 @@ const applyRouteOptions = async (options: Record = {}) => { activeRoutePreview.value = null isManualLocationPanelOpen.value = false isIndoorGuidePanelHidden.value = false + routeResolveState.value = 'idle' return false } - const matchedPoi = facilityId ? await guideUseCase.getPoiById(facilityId) : null + routeResolveState.value = 'loading' + routeResolveError.value = '' + let matchedPoi: Awaited> = null + try { + matchedPoi = facilityId ? await guideUseCase.getPoiById(facilityId) : null + await Promise.all([loadGuideFloors(), refreshRouteReadinessState()]) + } catch (error) { + if (requestSequence !== routeOptionsSequence) return false + routeResolveState.value = 'error' + routeResolveError.value = error instanceof Error ? error.message : '目标位置加载失败' + return false + } if (requestSequence !== routeOptionsSequence) return false if (!matchedPoi) { @@ -542,6 +568,7 @@ const applyRouteOptions = async (options: Record = {}) => { activeRoutePreview.value = null isManualLocationPanelOpen.value = false isIndoorGuidePanelHidden.value = false + routeResolveState.value = 'success' return false } @@ -564,11 +591,15 @@ const applyRouteOptions = async (options: Record = {}) => { setLocationPreviewTitle() saveCurrentLocationPreviewUrl() + routeResolveState.value = 'success' return true } let routeOptionsSequence = 0 +const retryRouteOptions = () => { + void applyRouteOptions(lastRouteOptions.value) +} onLoad((options: any) => { setLocationPreviewTitle() diff --git a/src/repositories/GuideRepository.ts b/src/repositories/GuideRepository.ts index 03ee407..f154446 100644 --- a/src/repositories/GuideRepository.ts +++ b/src/repositories/GuideRepository.ts @@ -548,8 +548,25 @@ export class SgsSdkGuideRepository implements GuideRepository { } async getPoiById(id: string) { - const pois = await this.searchPois() const normalizedId = id.trim() + if (!normalizedId) return null + + // The SGS query endpoint resolves stable IDs without forcing every floor's + // POI, business-POI, space, and navigable-place collection to load. + try { + const [manifest, directPois] = await Promise.all([ + this.provider.getManifest(), + this.provider.queryPois({ keyword: normalizedId, limit: 8 }) + ]) + const direct = directPois + .map((poi) => toMuseumPoiFromSgs(poi, manifest.floors)) + .find((poi) => poi.id === normalizedId || getPoiSourceLookupIdentity(normalizedId) === poi.spaceId || getPoiSourceLookupIdentity(normalizedId) === poi.sourceSpaceId) + if (direct) return direct + } catch (error) { + if (import.meta.env.DEV) console.warn('[SGS 点位数据] 单点解析失败,回退全量搜索', error) + } + + const pois = await this.searchPois() const exactPoi = pois.find((poi) => poi.id === normalizedId) if (exactPoi) return exactPoi diff --git a/src/view-models/visitorPoiPresentation.ts b/src/view-models/visitorPoiPresentation.ts index b1b5d50..6ef200e 100644 --- a/src/view-models/visitorPoiPresentation.ts +++ b/src/view-models/visitorPoiPresentation.ts @@ -17,11 +17,26 @@ type VisitorPoiSource = Pick [ - poi.floorLabel?.trim(), - poi.hallName?.trim(), - poi.primaryCategory?.label?.trim() -].filter((value, index, values) => Boolean(value) && values.indexOf(value) === index).join(' · ') +const normalizedComparisonText = (value?: string) => normalizeDisplayName(value || '') + .replace(/[\s·、,,。.!!??]/g, '') + .toLowerCase() + +const locationHintFor = (poi: VisitorPoiSource, baseName: string) => { + const baseKey = normalizedComparisonText(baseName) + const values = [ + poi.floorLabel?.trim(), + poi.hallName?.trim(), + poi.primaryCategory?.label?.trim() + ].filter((value): value is string => Boolean(value)) + + const seen = new Set([baseKey]) + return values.filter((value) => { + const key = normalizedComparisonText(value) + if (!key || seen.has(key)) return false + seen.add(key) + return true + }).join(' · ') +} export const createVisitorPoiPresentations = (pois: TPoi[]) => { const baseNames = new Map() @@ -31,13 +46,20 @@ export const createVisitorPoiPresentations = (poi }) const duplicateIndexes = new Map() + const duplicateLocationCounts = new Map() return new Map(pois.map((poi) => { const baseName = normalizeDisplayName(poi.name || '') || '未命名点位' const duplicateCount = baseNames.get(baseName) || 0 const nextIndex = (duplicateIndexes.get(baseName) || 0) + 1 duplicateIndexes.set(baseName, nextIndex) - const locationHint = locationHintFor(poi) - const disambiguation = duplicateCount > 1 ? (locationHint || `设施 ${nextIndex}`) : locationHint + const locationHint = locationHintFor(poi, baseName) + const duplicateLocationKey = `${baseName}|${locationHint}` + const locationIndex = (duplicateLocationCounts.get(duplicateLocationKey) || 0) + 1 + duplicateLocationCounts.set(duplicateLocationKey, locationIndex) + const sameLocation = duplicateCount > 1 && locationIndex > 1 + const disambiguation = duplicateCount > 1 + ? `${locationHint || '设施'}${sameLocation ? ` ${locationIndex}` : ''}` + : locationHint return [poi.id, { displayName: duplicateCount > 1 ? `${baseName} · ${disambiguation}` : baseName, diff --git a/tests/unit/IndexPoiSearchFlow.spec.ts b/tests/unit/IndexPoiSearchFlow.spec.ts index 483f985..52f5518 100644 --- a/tests/unit/IndexPoiSearchFlow.spec.ts +++ b/tests/unit/IndexPoiSearchFlow.spec.ts @@ -385,7 +385,7 @@ describe('首页搜索与地图闭环', () => { } }) - it('分类结果进入详情后恢复目标楼层标准视图', async () => { + it('分类结果进入详情后恢复目标楼层与搜索列表位置', async () => { const wrapper = await mountIndex() const search = wrapper.getComponent(PoiSearchPanelStub) const map = wrapper.getComponent(GuideMapShellStub) @@ -419,8 +419,8 @@ describe('首页搜索与地图闭环', () => { await testState.onShowHandler?.() await flushPromises() - expect(testState.searchResetCount).toBeGreaterThan(0) - expect(testState.searchRestoreCount).toBe(0) + expect(testState.searchResetCount).toBe(0) + expect(testState.searchRestoreCount).toBeGreaterThan(0) expect(map.props('activeFloor')).toBe('L2') expect(map.props('visiblePoiIds')).toBeNull() expect(map.props('targetFocusRequest')).toBeNull() @@ -431,7 +431,7 @@ describe('首页搜索与地图闭环', () => { expect(testState.searchResetCount).toBe(resetCount) }) - it('全屏搜索结果进入详情并返回后同样恢复为默认收缩态', async () => { + it('全屏搜索结果进入详情并返回后保留搜索上下文', async () => { const wrapper = await mountIndex() const search = wrapper.getComponent(PoiSearchPanelStub) const map = wrapper.getComponent(GuideMapShellStub) @@ -447,8 +447,8 @@ describe('首页搜索与地图闭环', () => { await testState.onShowHandler?.() await flushPromises() - expect(testState.searchResetCount).toBeGreaterThan(0) - expect(testState.searchRestoreCount).toBe(0) + expect(testState.searchResetCount).toBe(0) + expect(testState.searchRestoreCount).toBeGreaterThan(0) expect(map.props('visiblePoiIds')).toBeNull() expect(map.props('targetFocusRequest')).toBeNull() expect(map.props('activeFloor')).toBe('L2') diff --git a/tests/unit/visitorPoiPresentation.spec.ts b/tests/unit/visitorPoiPresentation.spec.ts index 7817085..4a5aec5 100644 --- a/tests/unit/visitorPoiPresentation.spec.ts +++ b/tests/unit/visitorPoiPresentation.spec.ts @@ -32,4 +32,23 @@ describe('VisitorPoiPresentation', () => { expect(result.get('p1')?.displayName).toBe('无障碍卫生间 · 1F · 恐龙厅 · 服务设施') expect(result.get('p2')?.displayName).toBe('无障碍卫生间 · 2F · 生态厅 · 服务设施') }) + + it('用稳定序号区分同楼层同区域的重名设施', () => { + const result = createVisitorPoiPresentations([ + poi('p1', '无障碍卫生间', '1F', '恐龙厅'), + poi('p2', '无障碍卫生间', '1F', '恐龙厅') + ]) + + expect(result.get('p1')?.displayName).toBe('无障碍卫生间 · 1F · 恐龙厅 · 服务设施') + expect(result.get('p2')?.displayName).toBe('无障碍卫生间 · 1F · 恐龙厅 · 服务设施 2') + }) + + it('省略与名称重复的展厅提示,保留最小位置上下文', () => { + const result = createVisitorPoiPresentations([ + poi('p1', '球幕影院', '1F', '球幕影院') + ]) + + expect(result.get('p1')?.displayName).toBe('球幕影院') + expect(result.get('p1')?.locationHint).toBe('1F · 服务设施') + }) })