修复导览位置预览与楼层状态
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-19 21:04:57 +08:00
parent 3cba97b786
commit 47dd4c7c87
9 changed files with 128 additions and 28 deletions

View File

@@ -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) {

View File

@@ -371,10 +371,17 @@ const parseJsonPayload = <T>(payload: unknown, requestUrl: string, contentType:
return payload as T
}
const inFlightRequests = new Map<string, Promise<unknown>>()
const requestJson = <T>(
path: string,
options: { method?: 'GET' | 'POST'; data?: unknown } = {}
): Promise<T> => new Promise((resolve, reject) => {
): Promise<T> => {
const requestKey = `${options.method || 'GET'}:${path}:${JSON.stringify(options.data || {})}`
const existing = inFlightRequests.get(requestKey)
if (existing) return existing as Promise<T>
const request = new Promise<T>((resolve, reject) => {
const baseUrl = resolveAppApiBaseUrl()
const requestUrl = `${baseUrl}${path}`
@@ -413,6 +420,10 @@ const requestJson = <T>(
}
})
})
inFlightRequests.set(requestKey, request)
void request.finally(() => inFlightRequests.delete(requestKey)).catch(() => undefined)
return request
}
const buildQueryString = (params: object) => {
const query = new URLSearchParams()

View File

@@ -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,

View File

@@ -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
}

View File

@@ -89,7 +89,18 @@
</view>
</template>
<view v-if="!hasRouteTarget" class="target-required-card">
<view v-if="routeResolveState === 'loading'" class="target-required-card" role="status" aria-live="polite">
<text class="target-required-title">正在解析目标位置</text>
<text class="target-required-desc">正在加载所需的三维位置数据</text>
</view>
<view v-else-if="routeResolveState === 'error'" class="target-required-card" role="alert">
<text class="target-required-title">目标位置加载失败</text>
<text class="target-required-desc">{{ routeResolveError || '请检查网络后重试。' }}</text>
<button class="target-required-btn" @tap="retryRouteOptions">重新加载</button>
</view>
<view v-else-if="!hasRouteTarget" class="target-required-card">
<text class="target-required-title">请选择目标位置</text>
<text class="target-required-desc">需要先选择展厅或设施才能查看馆内三维位置</text>
<view class="target-required-btn" @tap="handleSelectTarget">
@@ -132,7 +143,7 @@
<view v-if="routePlanError" class="indoor-guide-error">
<text class="indoor-guide-error-text">{{ routePlanError }}</text>
</view>
<view class="start-point-select" @tap="openStartPointPicker">
<view v-if="routeReady" class="start-point-select" role="button" tabindex="0" aria-label="选择位置关系起点" @tap="openStartPointPicker">
<text class="start-point-select-label">起点</text>
<text class="start-point-select-value" :class="{ placeholder: !selectedStartPoint }">
{{ selectedStartPoint ? `${selectedStartPoint.name} · ${selectedStartPoint.floorLabel}` : '请选择起点' }}
@@ -140,6 +151,7 @@
</view>
<view class="indoor-guide-actions">
<view
v-if="routeReady"
class="indoor-guide-btn secondary"
@tap="handleReturnToPreview"
>
@@ -241,6 +253,9 @@ const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloor
const route = ref<RoutePlan>(initialLocationPreviewPlan())
const hasRouteTarget = ref(false)
const routeResolveState = ref<'idle' | 'loading' | 'success' | 'error'>('idle')
const routeResolveError = ref('')
const lastRouteOptions = ref<Record<string, unknown>>({})
const routeViewState = ref<RouteViewState>('preview')
const activeFloor = ref('1F')
const targetFocusRequest = ref<TargetPoiFocusRequest | null>(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<RouteTargetLocation> | null, fallbac
const applyRouteOptions = async (options: Record<string, unknown> = {}) => {
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<string, unknown> = {}) => {
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<string, unknown> = {}) => {
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<ReturnType<typeof guideUseCase.getPoiById>> = 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<string, unknown> = {}) => {
activeRoutePreview.value = null
isManualLocationPanelOpen.value = false
isIndoorGuidePanelHidden.value = false
routeResolveState.value = 'success'
return false
}
@@ -564,11 +591,15 @@ const applyRouteOptions = async (options: Record<string, unknown> = {}) => {
setLocationPreviewTitle()
saveCurrentLocationPreviewUrl()
routeResolveState.value = 'success'
return true
}
let routeOptionsSequence = 0
const retryRouteOptions = () => {
void applyRouteOptions(lastRouteOptions.value)
}
onLoad((options: any) => {
setLocationPreviewTitle()

View File

@@ -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

View File

@@ -17,11 +17,26 @@ type VisitorPoiSource = Pick<MuseumPoi, 'id' | 'name' | 'floorId' | 'floorLabel'
primaryCategory?: { label?: string }
}
const locationHintFor = (poi: VisitorPoiSource) => [
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, index, values) => Boolean(value) && values.indexOf(value) === index).join(' · ')
].filter((value): value is string => Boolean(value))
const seen = new Set<string>([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 = <TPoi extends VisitorPoiSource>(pois: TPoi[]) => {
const baseNames = new Map<string, number>()
@@ -31,13 +46,20 @@ export const createVisitorPoiPresentations = <TPoi extends VisitorPoiSource>(poi
})
const duplicateIndexes = new Map<string, number>()
const duplicateLocationCounts = new Map<string, number>()
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,

View File

@@ -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')

View File

@@ -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 · 服务设施')
})
})