@@ -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) {
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -412,7 +419,11 @@ const requestJson = <T>(
|
||||
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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -17,11 +17,26 @@ type VisitorPoiSource = Pick<MuseumPoi, 'id' | 'name' | 'floorId' | 'floorLabel'
|
||||
primaryCategory?: { label?: string }
|
||||
}
|
||||
|
||||
const locationHintFor = (poi: VisitorPoiSource) => [
|
||||
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<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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 · 服务设施')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user