This commit is contained in:
@@ -3430,6 +3430,8 @@ const restoreCameraSnapshot = (snapshot: CameraSnapshot) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ResetViewBaselineResult = 'applied' | 'not-ready' | 'invalid-target' | 'failed' | 'stale'
|
||||||
|
|
||||||
const applyReferenceOverviewCameraState = () => {
|
const applyReferenceOverviewCameraState = () => {
|
||||||
restoreCameraSnapshot(referenceOverviewCameraState)
|
restoreCameraSnapshot(referenceOverviewCameraState)
|
||||||
}
|
}
|
||||||
@@ -3652,6 +3654,49 @@ const getVisualFocusReport = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getVisiblePoiScreenPositions = () => {
|
||||||
|
if (!renderer || !camera) return []
|
||||||
|
|
||||||
|
const rect = renderer.domElement.getBoundingClientRect()
|
||||||
|
return getPoiSprites().flatMap((sprite) => {
|
||||||
|
const poi = sprite.userData.poi as RenderPoi | undefined
|
||||||
|
const screen = getProjectedScreenPosition(sprite)
|
||||||
|
let ancestor: THREE.Object3D | null = sprite
|
||||||
|
while (ancestor) {
|
||||||
|
if (!ancestor.visible) return []
|
||||||
|
ancestor = ancestor.parent
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!poi
|
||||||
|
|| !screen
|
||||||
|
|| screen.x < 0
|
||||||
|
|| screen.x > rect.width
|
||||||
|
|| screen.y < 0
|
||||||
|
|| screen.y > rect.height
|
||||||
|
) return []
|
||||||
|
|
||||||
|
// Match the production fallback hit-test so diagnostics only expose coordinates
|
||||||
|
// that a tap can resolve to this exact current marker.
|
||||||
|
const hit = findNearestPoiMarkerByScreenPoint({
|
||||||
|
clientX: rect.left + screen.x,
|
||||||
|
clientY: rect.top + screen.y
|
||||||
|
} as PointerEvent, rect)
|
||||||
|
if (hit !== sprite) return []
|
||||||
|
|
||||||
|
return [{
|
||||||
|
poiId: poi.id,
|
||||||
|
floorId: poi.floorId,
|
||||||
|
name: poi.name,
|
||||||
|
kind: poi.kind,
|
||||||
|
primaryCategory: poi.primaryCategory,
|
||||||
|
screen: {
|
||||||
|
x: rect.left + screen.x,
|
||||||
|
y: rect.top + screen.y
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const getVisualStabilityReport = () => ({
|
const getVisualStabilityReport = () => ({
|
||||||
camera: serializeCameraSnapshot(captureCameraSnapshot()),
|
camera: serializeCameraSnapshot(captureCameraSnapshot()),
|
||||||
modelRoot: getModelRootTransformAudit(activeModel),
|
modelRoot: getModelRootTransformAudit(activeModel),
|
||||||
@@ -3674,6 +3719,8 @@ const installVisualStabilityDiagnostics = () => {
|
|||||||
}
|
}
|
||||||
diagnosticsWindow.__GUIDE_3D_VISUAL_STABILITY__ = {
|
diagnosticsWindow.__GUIDE_3D_VISUAL_STABILITY__ = {
|
||||||
getReport: getVisualStabilityReport,
|
getReport: getVisualStabilityReport,
|
||||||
|
isInitialModelReady: () => initialModelSettled && Boolean(initialGuideState && activeModel),
|
||||||
|
getVisiblePoiScreenPositions,
|
||||||
getFloors: () => floorIndex.value.map((floor) => ({
|
getFloors: () => floorIndex.value.map((floor) => ({
|
||||||
floorId: floor.floorId,
|
floorId: floor.floorId,
|
||||||
label: floor.label,
|
label: floor.label,
|
||||||
@@ -3696,6 +3743,8 @@ const installVisualStabilityDiagnostics = () => {
|
|||||||
poiId: poi.id,
|
poiId: poi.id,
|
||||||
floorId: poi.floorId,
|
floorId: poi.floorId,
|
||||||
name: poi.name,
|
name: poi.name,
|
||||||
|
kind: poi.kind,
|
||||||
|
primaryCategory: poi.primaryCategory,
|
||||||
positionGltf: poi.positionGltf!
|
positionGltf: poi.positionGltf!
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
@@ -5610,6 +5659,18 @@ const showMultiFloor = async () => {
|
|||||||
// Restores business and camera state on the existing scene. It never initializes WebGL or
|
// Restores business and camera state on the existing scene. It never initializes WebGL or
|
||||||
// reloads the model package, so closing a detail page cannot remount ThreeMap.
|
// reloads the model package, so closing a detail page cannot remount ThreeMap.
|
||||||
const resetToViewBaseline = async (options: ResetViewBaselineOptions) => {
|
const resetToViewBaseline = async (options: ResetViewBaselineOptions) => {
|
||||||
|
// Do not invalidate the first model load while the renderer is still being initialized.
|
||||||
|
// The page-level guide model state will replay only its latest not-ready request.
|
||||||
|
const baseline = initialGuideState
|
||||||
|
if (!baseline || !scene || !camera || !controls) return 'not-ready' as const
|
||||||
|
if (loadError.value) return 'failed' as const
|
||||||
|
|
||||||
|
if (options.view === 'floor') {
|
||||||
|
if (!options.floorId || !floorIndex.value.some((item) => item.floorId === options.floorId)) {
|
||||||
|
return 'invalid-target' as const
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
clearTargetFocus()
|
clearTargetFocus()
|
||||||
invalidateModelLoads()
|
invalidateModelLoads()
|
||||||
adjacentPreloadSeq += 1
|
adjacentPreloadSeq += 1
|
||||||
@@ -5627,25 +5688,22 @@ const resetToViewBaseline = async (options: ResetViewBaselineOptions) => {
|
|||||||
autoSwitchStateMachine.reset(options.view)
|
autoSwitchStateMachine.reset(options.view)
|
||||||
resetInteractionGateState()
|
resetInteractionGateState()
|
||||||
|
|
||||||
const baseline = initialGuideState
|
|
||||||
if (!baseline || !scene || !camera || !controls || loadError.value) return false
|
|
||||||
|
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
try {
|
try {
|
||||||
if (options.view === 'floor') {
|
if (options.view === 'floor') {
|
||||||
const floorId = options.floorId
|
const floorId = options.floorId
|
||||||
if (!floorId) return false
|
if (!floorId) return 'invalid-target' as const
|
||||||
const floor = floorIndex.value.find((item) => item.floorId === floorId)
|
const floor = floorIndex.value.find((item) => item.floorId === floorId)
|
||||||
if (!floor) return false
|
if (!floor) return 'invalid-target' as const
|
||||||
|
|
||||||
if (activeView.value !== 'floor' || currentFloor.value !== floorId || activeModel?.userData.floorId !== floorId) {
|
if (activeView.value !== 'floor' || currentFloor.value !== floorId || activeModel?.userData.floorId !== floorId) {
|
||||||
await loadFloor(floorId, { applyFloorBaseline: true, detachPoiBeforeLoad: true })
|
await loadFloor(floorId, { applyFloorBaseline: true, detachPoiBeforeLoad: true })
|
||||||
} else {
|
} else {
|
||||||
const floorBaseline = getFloorViewBaseline(floorId, activeModel, floor.modelUrl)
|
const floorBaseline = getFloorViewBaseline(floorId, activeModel, floor.modelUrl)
|
||||||
if (!floorBaseline) return false
|
if (!floorBaseline) return 'failed' as const
|
||||||
restoreCameraSnapshot(floorBaseline.camera)
|
restoreCameraSnapshot(floorBaseline.camera)
|
||||||
}
|
}
|
||||||
if (!scene || !isCurrentModelLoad(modelLoadVersion)) return false
|
if (!scene || !isCurrentModelLoad(modelLoadVersion)) return 'stale' as const
|
||||||
activeView.value = 'floor'
|
activeView.value = 'floor'
|
||||||
currentFloor.value = floorId
|
currentFloor.value = floorId
|
||||||
autoSwitchStateMachine.reset('floor')
|
autoSwitchStateMachine.reset('floor')
|
||||||
@@ -5654,7 +5712,7 @@ const resetToViewBaseline = async (options: ResetViewBaselineOptions) => {
|
|||||||
if (activeView.value !== 'overview') {
|
if (activeView.value !== 'overview') {
|
||||||
await loadOverview({ cameraSnapshot: baseline.camera })
|
await loadOverview({ cameraSnapshot: baseline.camera })
|
||||||
}
|
}
|
||||||
if (!scene || !isCurrentModelLoad(modelLoadVersion)) return false
|
if (!scene || !isCurrentModelLoad(modelLoadVersion)) return 'stale' as const
|
||||||
restoreCameraSnapshot(baseline.camera)
|
restoreCameraSnapshot(baseline.camera)
|
||||||
currentFloor.value = baseline.floorId
|
currentFloor.value = baseline.floorId
|
||||||
activeView.value = 'overview'
|
activeView.value = 'overview'
|
||||||
@@ -5665,12 +5723,13 @@ const resetToViewBaseline = async (options: ResetViewBaselineOptions) => {
|
|||||||
autoSwitchStateMachine.setFloorInitialDistance(controls.getDistance())
|
autoSwitchStateMachine.setFloorInitialDistance(controls.getDistance())
|
||||||
}
|
}
|
||||||
refreshPoiVisibilityByDistance()
|
refreshPoiVisibilityByDistance()
|
||||||
return true
|
return 'applied' as const
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (isStaleModelLoadError(error)) return 'stale' as const
|
||||||
if (!isStaleModelLoadError(error)) {
|
if (!isStaleModelLoadError(error)) {
|
||||||
console.error('恢复馆内导览初始状态失败:', error)
|
console.error('恢复馆内导览初始状态失败:', error)
|
||||||
}
|
}
|
||||||
return false
|
return 'failed' as const
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ import { computed, ref, watch } from 'vue'
|
|||||||
import TencentMap from '@/components/map/TencentMap.vue'
|
import TencentMap from '@/components/map/TencentMap.vue'
|
||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
import ThreeMap from '@/components/map/ThreeMap.vue'
|
import ThreeMap from '@/components/map/ThreeMap.vue'
|
||||||
|
import type { ResetViewBaselineResult } from '@/components/map/ThreeMap.vue'
|
||||||
// #endif
|
// #endif
|
||||||
import type {
|
import type {
|
||||||
GuideModelSource,
|
GuideModelSource,
|
||||||
@@ -464,8 +465,8 @@ const indoorRendererRef = ref<{
|
|||||||
view: 'overview' | 'floor'
|
view: 'overview' | 'floor'
|
||||||
floorId?: string
|
floorId?: string
|
||||||
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
|
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
|
||||||
}) => Promise<boolean> | boolean
|
}) => Promise<ResetViewBaselineResult> | ResetViewBaselineResult
|
||||||
resetToInitialState?: () => Promise<boolean> | boolean
|
resetToInitialState?: () => Promise<ResetViewBaselineResult> | ResetViewBaselineResult
|
||||||
disableAutoSwitchTemporarily?: (durationMs: number) => void
|
disableAutoSwitchTemporarily?: (durationMs: number) => void
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
@@ -790,8 +791,8 @@ defineExpose({
|
|||||||
view: 'overview' | 'floor'
|
view: 'overview' | 'floor'
|
||||||
floorId?: string
|
floorId?: string
|
||||||
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
|
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
|
||||||
}) => indoorRendererRef.value?.resetToViewBaseline?.(options) || false,
|
}) => indoorRendererRef.value?.resetToViewBaseline?.(options) || 'not-ready',
|
||||||
resetToInitialState: () => indoorRendererRef.value?.resetToInitialState?.() || false
|
resetToInitialState: () => indoorRendererRef.value?.resetToInitialState?.() || 'not-ready'
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ export type PoiDetailReturnContext = Readonly<{
|
|||||||
requestId: number
|
requestId: number
|
||||||
}>
|
}>
|
||||||
|
|
||||||
|
export type GuideModelResetReason = 'poi-detail-close' | 'floor-reset'
|
||||||
|
|
||||||
|
export type GuideModelResetRequest = Readonly<{
|
||||||
|
floorId: string
|
||||||
|
requestId: number
|
||||||
|
rendererReason: GuideModelResetReason
|
||||||
|
}>
|
||||||
|
|
||||||
type GuideModelReadyState = Readonly<{
|
type GuideModelReadyState = Readonly<{
|
||||||
view: 'overview' | 'floor' | 'multi'
|
view: 'overview' | 'floor' | 'multi'
|
||||||
floorId: string
|
floorId: string
|
||||||
@@ -52,6 +60,7 @@ const initialState = ref<GuideModelInitialState>(GUIDE_MODEL_INITIAL_STATE)
|
|||||||
const pendingModelResetAfterPoiDetail = ref<PoiDetailReturnContext | null>(null)
|
const pendingModelResetAfterPoiDetail = ref<PoiDetailReturnContext | null>(null)
|
||||||
const confirmedPoiDetailRequestId = ref<number | null>(null)
|
const confirmedPoiDetailRequestId = ref<number | null>(null)
|
||||||
const requestGeneration = ref(0)
|
const requestGeneration = ref(0)
|
||||||
|
const pendingViewBaselineReset = ref<GuideModelResetRequest | null>(null)
|
||||||
let poiDetailRequestId = 0
|
let poiDetailRequestId = 0
|
||||||
|
|
||||||
export const useGuideModelState = () => {
|
export const useGuideModelState = () => {
|
||||||
@@ -107,15 +116,53 @@ export const useGuideModelState = () => {
|
|||||||
return requestGeneration.value
|
return requestGeneration.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const beginViewBaselineReset = ({ floorId, rendererReason }: {
|
||||||
|
floorId: string
|
||||||
|
rendererReason: GuideModelResetReason
|
||||||
|
}) => Object.freeze({
|
||||||
|
floorId,
|
||||||
|
rendererReason,
|
||||||
|
requestId: invalidatePendingRequests()
|
||||||
|
})
|
||||||
|
|
||||||
|
const isCurrentRequest = (requestId: number) => requestGeneration.value === requestId
|
||||||
|
|
||||||
|
// The renderer may not yet exist while the page has already received a return event.
|
||||||
|
// Keep exactly one latest request here so page lifecycle hooks cannot race each other.
|
||||||
|
const queueViewBaselineReset = (request: GuideModelResetRequest) => {
|
||||||
|
if (!isCurrentRequest(request.requestId)) return false
|
||||||
|
pendingViewBaselineReset.value = request
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const completeViewBaselineReset = (request: GuideModelResetRequest) => {
|
||||||
|
if (!isCurrentRequest(request.requestId)) return false
|
||||||
|
pendingViewBaselineReset.value = null
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const consumePendingViewBaselineReset = () => {
|
||||||
|
const request = pendingViewBaselineReset.value
|
||||||
|
if (!request || !isCurrentRequest(request.requestId)) return null
|
||||||
|
pendingViewBaselineReset.value = null
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initialState: readonly(initialState),
|
initialState: readonly(initialState),
|
||||||
pendingModelResetAfterPoiDetail: readonly(pendingModelResetAfterPoiDetail),
|
pendingModelResetAfterPoiDetail: readonly(pendingModelResetAfterPoiDetail),
|
||||||
requestGeneration: readonly(requestGeneration),
|
requestGeneration: readonly(requestGeneration),
|
||||||
|
pendingViewBaselineReset: readonly(pendingViewBaselineReset),
|
||||||
initializeModel,
|
initializeModel,
|
||||||
beginPoiDetailReturn,
|
beginPoiDetailReturn,
|
||||||
confirmPoiDetailReturn,
|
confirmPoiDetailReturn,
|
||||||
cancelPoiDetailReturn,
|
cancelPoiDetailReturn,
|
||||||
consumePoiDetailReturn,
|
consumePoiDetailReturn,
|
||||||
invalidatePendingRequests
|
invalidatePendingRequests,
|
||||||
|
beginViewBaselineReset,
|
||||||
|
isCurrentRequest,
|
||||||
|
queueViewBaselineReset,
|
||||||
|
completeViewBaselineReset,
|
||||||
|
consumePendingViewBaselineReset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
<view class="poi-card-soft-action" @tap="collapsePoiCard">
|
<view class="poi-card-soft-action" @tap="collapsePoiCard">
|
||||||
<text class="poi-card-soft-text">收起</text>
|
<text class="poi-card-soft-text">收起</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="poi-card-close" @tap="handleGuideSelectionClear">
|
<view class="poi-card-close" @tap="handleGuidePoiCardClose">
|
||||||
<text class="poi-card-close-text">×</text>
|
<text class="poi-card-close-text">×</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -327,7 +327,12 @@ import type {
|
|||||||
import {
|
import {
|
||||||
type TargetPoiFocusRequestViewModel
|
type TargetPoiFocusRequestViewModel
|
||||||
} from '@/view-models/guideViewModels'
|
} from '@/view-models/guideViewModels'
|
||||||
import { useGuideModelState, type PoiDetailReturnContext } from '@/composables/useGuideModelState'
|
import {
|
||||||
|
useGuideModelState,
|
||||||
|
type GuideModelResetReason,
|
||||||
|
type GuideModelResetRequest
|
||||||
|
} from '@/composables/useGuideModelState'
|
||||||
|
import type { ResetViewBaselineResult } from '@/components/map/ThreeMap.vue'
|
||||||
import {
|
import {
|
||||||
ARRIVAL_TARGETS_BY_TYPE,
|
ARRIVAL_TARGETS_BY_TYPE,
|
||||||
type ArrivalSearchTarget,
|
type ArrivalSearchTarget,
|
||||||
@@ -422,7 +427,7 @@ const guideMapShellRef = ref<{
|
|||||||
view: 'overview' | 'floor'
|
view: 'overview' | 'floor'
|
||||||
floorId?: string
|
floorId?: string
|
||||||
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
|
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
|
||||||
}) => Promise<boolean> | boolean
|
}) => Promise<ResetViewBaselineResult> | ResetViewBaselineResult
|
||||||
resetToInitialState?: () => Promise<boolean> | boolean
|
resetToInitialState?: () => Promise<boolean> | boolean
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const homeSearchPanelRef = ref<{
|
const homeSearchPanelRef = ref<{
|
||||||
@@ -435,8 +440,6 @@ const homeSearchExpanded = ref(false)
|
|||||||
const homeCategoryModeActive = ref(false)
|
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 pendingGuideModelResetUntilReady: PoiDetailReturnContext | null = null
|
|
||||||
|
|
||||||
type PoiSearchOverlayHistoryState = {
|
type PoiSearchOverlayHistoryState = {
|
||||||
museumGuideOverlay?: 'poi-search'
|
museumGuideOverlay?: 'poi-search'
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
@@ -578,6 +581,14 @@ const indoorNavAssetBaseUrl = guideUseCase.getAssetBaseUrl()
|
|||||||
const indoorModelSource = guideUseCase.getModelSource()
|
const indoorModelSource = guideUseCase.getModelSource()
|
||||||
const guideFloors = ref<MuseumFloor[]>([])
|
const guideFloors = ref<MuseumFloor[]>([])
|
||||||
const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloorId(labelOrId)
|
const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloorId(labelOrId)
|
||||||
|
const getPreferredInitialGuideFloorId = (floors: MuseumFloor[]) => (
|
||||||
|
floors.find((floor) => (
|
||||||
|
normalizeGuideFloorId(floor.id) === 'L1'
|
||||||
|
|| normalizeGuideFloorId(floor.label) === 'L1'
|
||||||
|
))?.id
|
||||||
|
|| floors[0]?.id
|
||||||
|
|| ''
|
||||||
|
)
|
||||||
const getGuideFloorById = (floorId: string) => (
|
const getGuideFloorById = (floorId: string) => (
|
||||||
guideFloors.value.find((floor) => floor.id === floorId)
|
guideFloors.value.find((floor) => floor.id === floorId)
|
||||||
)
|
)
|
||||||
@@ -923,7 +934,7 @@ const loadGuideFloors = async () => {
|
|||||||
? normalizeGuideFloorId(activeGuideFloor.value)
|
? normalizeGuideFloorId(activeGuideFloor.value)
|
||||||
: ''
|
: ''
|
||||||
activeGuideFloor.value = floors.find((floor) => floor.id === normalizedActiveFloorId)?.id
|
activeGuideFloor.value = floors.find((floor) => floor.id === normalizedActiveFloorId)?.id
|
||||||
|| floors[0]?.id
|
|| getPreferredInitialGuideFloorId(floors)
|
||||||
|| activeGuideFloor.value
|
|| activeGuideFloor.value
|
||||||
renderedFloorId.value = activeGuideFloor.value
|
renderedFloorId.value = activeGuideFloor.value
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1048,10 +1059,13 @@ const handleInitialModelReady = (event: { view: GuideIndoorView; floorId?: strin
|
|||||||
guideModelState.initializeModel({ view: event.view, floorId: readyFloorId })
|
guideModelState.initializeModel({ view: event.view, floorId: readyFloorId })
|
||||||
}
|
}
|
||||||
settleLaunchOverlayByModel('ready', event)
|
settleLaunchOverlayByModel('ready', event)
|
||||||
if (pendingGuideModelResetUntilReady) {
|
const context = guideModelState.consumePendingViewBaselineReset()
|
||||||
const context = pendingGuideModelResetUntilReady
|
if (context) {
|
||||||
pendingGuideModelResetUntilReady = null
|
void resetGuideModelToFloorBaseline({
|
||||||
void resetGuideModelToInitial({ reason: 'queued-poi-detail-close', floorId: context.floorId })
|
rendererReason: context.rendererReason,
|
||||||
|
floorId: context.floorId,
|
||||||
|
request: context
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1086,6 +1100,19 @@ const handleGuideSelectionClear = () => {
|
|||||||
isPoiCardCollapsed.value = false
|
isPoiCardCollapsed.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleGuidePoiCardClose = () => {
|
||||||
|
const floorId = selectedGuidePoi.value?.floorId
|
||||||
|
if (!floorId) {
|
||||||
|
handleGuideSelectionClear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetGuideModelToFloorBaseline({
|
||||||
|
rendererReason: 'floor-reset',
|
||||||
|
floorId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const collapsePoiCard = () => {
|
const collapsePoiCard = () => {
|
||||||
isPoiCardCollapsed.value = true
|
isPoiCardCollapsed.value = true
|
||||||
}
|
}
|
||||||
@@ -1507,15 +1534,17 @@ const closeHomeSearchDock = () => {
|
|||||||
homeSearchExpanded.value = false
|
homeSearchExpanded.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetGuideModelToInitial = async ({
|
const resetGuideModelToFloorBaseline = async ({
|
||||||
reason,
|
rendererReason,
|
||||||
floorId
|
floorId,
|
||||||
|
request
|
||||||
}: {
|
}: {
|
||||||
reason: 'poi-detail-close' | 'queued-poi-detail-close'
|
rendererReason: GuideModelResetReason
|
||||||
floorId: string
|
floorId: string
|
||||||
|
request?: GuideModelResetRequest
|
||||||
}) => {
|
}) => {
|
||||||
const initial = guideModelState.initialState.value
|
const initial = guideModelState.initialState.value
|
||||||
const resetRequestId = guideModelState.invalidatePendingRequests()
|
const resetRequest = request || guideModelState.beginViewBaselineReset({ floorId, rendererReason })
|
||||||
searchTargetFocusRequest.value = null
|
searchTargetFocusRequest.value = null
|
||||||
searchVisiblePoiIds.value = null
|
searchVisiblePoiIds.value = null
|
||||||
homeCategoryModeActive.value = false
|
homeCategoryModeActive.value = false
|
||||||
@@ -1537,32 +1566,32 @@ const resetGuideModelToInitial = async ({
|
|||||||
activeGuideFloor.value = floorId || initial.defaultActiveFloorId || activeGuideFloor.value
|
activeGuideFloor.value = floorId || initial.defaultActiveFloorId || activeGuideFloor.value
|
||||||
closeArrivalPanel()
|
closeArrivalPanel()
|
||||||
await homeSearchPanelRef.value?.resetSearchState?.()
|
await homeSearchPanelRef.value?.resetSearchState?.()
|
||||||
|
if (!guideModelState.isCurrentRequest(resetRequest.requestId)) return
|
||||||
closeHomeSearchDock()
|
closeHomeSearchDock()
|
||||||
|
|
||||||
const resetApplied = await guideMapShellRef.value?.resetToViewBaseline?.({
|
const resetResult = await guideMapShellRef.value?.resetToViewBaseline?.({
|
||||||
view: 'floor',
|
view: 'floor',
|
||||||
floorId: activeGuideFloor.value,
|
floorId: activeGuideFloor.value,
|
||||||
reason: 'poi-detail-close'
|
reason: rendererReason
|
||||||
})
|
})
|
||||||
if (resetApplied) {
|
if (!guideModelState.isCurrentRequest(resetRequest.requestId)) return
|
||||||
|
if (resetResult === 'applied') {
|
||||||
renderedFloorId.value = activeGuideFloor.value
|
renderedFloorId.value = activeGuideFloor.value
|
||||||
pendingGuideModelResetUntilReady = null
|
guideModelState.completeViewBaselineReset(resetRequest)
|
||||||
} else {
|
console.info('馆内三维模型已恢复楼层视觉基线:', { rendererReason })
|
||||||
// The renderer is still becoming ready; retain only the latest requested reset.
|
} else if (resetResult === 'not-ready' || resetResult === undefined) {
|
||||||
pendingGuideModelResetUntilReady = Object.freeze({
|
guideModelState.queueViewBaselineReset(resetRequest)
|
||||||
floorId: activeGuideFloor.value,
|
|
||||||
resetMode: 'floor-baseline',
|
|
||||||
requestId: resetRequestId
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
console.info('馆内三维模型已恢复统一初始状态:', { reason })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow(async () => {
|
onShow(async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
const detailReturnContext = guideModelState.consumePoiDetailReturn()
|
const detailReturnContext = guideModelState.consumePoiDetailReturn()
|
||||||
if (detailReturnContext) {
|
if (detailReturnContext) {
|
||||||
await resetGuideModelToInitial({ reason: 'poi-detail-close', floorId: detailReturnContext.floorId })
|
await resetGuideModelToFloorBaseline({
|
||||||
|
rendererReason: 'poi-detail-close',
|
||||||
|
floorId: detailReturnContext.floorId
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,11 +52,23 @@ interface PoiFocusCandidate {
|
|||||||
poiId: string
|
poiId: string
|
||||||
floorId: string
|
floorId: string
|
||||||
name: string
|
name: string
|
||||||
|
kind?: string
|
||||||
|
primaryCategory: string
|
||||||
positionGltf: [number, number, number]
|
positionGltf: [number, number, number]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface VisiblePoiScreenPosition {
|
||||||
|
poiId: string
|
||||||
|
floorId: string
|
||||||
|
name: string
|
||||||
|
kind?: string
|
||||||
|
primaryCategory: string
|
||||||
|
screen: { x: number; y: number }
|
||||||
|
}
|
||||||
|
|
||||||
interface VisualStabilityApi {
|
interface VisualStabilityApi {
|
||||||
getReport: () => VisualReport
|
getReport: () => VisualReport
|
||||||
|
isInitialModelReady: () => boolean
|
||||||
getFloors: () => FloorReport[]
|
getFloors: () => FloorReport[]
|
||||||
switchFloor: (floorId: string) => Promise<void>
|
switchFloor: (floorId: string) => Promise<void>
|
||||||
showOverview: () => Promise<void>
|
showOverview: () => Promise<void>
|
||||||
@@ -64,10 +76,11 @@ interface VisualStabilityApi {
|
|||||||
view: 'overview' | 'floor'
|
view: 'overview' | 'floor'
|
||||||
floorId?: string
|
floorId?: string
|
||||||
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
|
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
|
||||||
}) => Promise<boolean>
|
}) => Promise<'applied' | 'not-ready' | 'invalid-target' | 'failed' | 'stale'>
|
||||||
resetToInitialState: () => Promise<boolean>
|
resetToInitialState: () => Promise<'applied' | 'not-ready' | 'invalid-target' | 'failed' | 'stale'>
|
||||||
getFloorBaseline: (floorId: string) => CameraReport | null
|
getFloorBaseline: (floorId: string) => CameraReport | null
|
||||||
getFloorPois: (floorId: string) => Promise<PoiFocusCandidate[]>
|
getFloorPois: (floorId: string) => Promise<PoiFocusCandidate[]>
|
||||||
|
getVisiblePoiScreenPositions: () => VisiblePoiScreenPosition[]
|
||||||
focusTargetPoi: (request: {
|
focusTargetPoi: (request: {
|
||||||
requestId: number | string
|
requestId: number | string
|
||||||
poiId: string
|
poiId: string
|
||||||
@@ -103,6 +116,20 @@ const getReport = async (page: Page) => {
|
|||||||
return state!.report
|
return state!.report
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openGuide = async (page: Page) => {
|
||||||
|
await page.goto('/')
|
||||||
|
await page.waitForURL(/tab=guide/, { timeout: 30_000 })
|
||||||
|
await expect.poll(async () => (await getApiState(page)) !== null, { timeout: 180_000 }).toBe(true)
|
||||||
|
await expect.poll(async () => (await getApiState(page))?.floors.length ?? 0, { timeout: 180_000 })
|
||||||
|
.toBeGreaterThan(0)
|
||||||
|
await expect.poll(async () => page.evaluate(() => (
|
||||||
|
(window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi })
|
||||||
|
.__GUIDE_3D_VISUAL_STABILITY__?.isInitialModelReady() || false
|
||||||
|
)), { timeout: 180_000 }).toBe(true)
|
||||||
|
await expect.poll(async () => (await getApiState(page))?.report.modelRoot !== null, { timeout: 180_000 }).toBe(true)
|
||||||
|
await expect.poll(async () => (await getApiState(page))?.report.anchors.length ?? 0, { timeout: 180_000 }).toBe(3)
|
||||||
|
}
|
||||||
|
|
||||||
const getMaximumCameraDelta = (before: CameraReport, after: CameraReport) => Math.max(
|
const getMaximumCameraDelta = (before: CameraReport, after: CameraReport) => Math.max(
|
||||||
Math.hypot(
|
Math.hypot(
|
||||||
before.position.x - after.position.x,
|
before.position.x - after.position.x,
|
||||||
@@ -263,6 +290,13 @@ const getFloorPois = async (page: Page, floorId: string) => {
|
|||||||
return pois
|
return pois
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getVisiblePoiScreenPositions = async (page: Page) => page.evaluate(() => {
|
||||||
|
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi })
|
||||||
|
.__GUIDE_3D_VISUAL_STABILITY__
|
||||||
|
if (!api) throw new Error('ThreeMap visual stability diagnostics unavailable')
|
||||||
|
return api.getVisiblePoiScreenPositions()
|
||||||
|
})
|
||||||
|
|
||||||
const getVectorDelta = (left: { x: number; y: number; z: number }, right: [number, number, number]) => (
|
const getVectorDelta = (left: { x: number; y: number; z: number }, right: [number, number, number]) => (
|
||||||
Math.hypot(left.x - right[0], left.y - right[1], left.z - right[2])
|
Math.hypot(left.x - right[0], left.y - right[1], left.z - right[2])
|
||||||
)
|
)
|
||||||
@@ -290,12 +324,7 @@ const waitForPoiFocusToSettle = async (page: Page, testInfo: TestInfo, name: str
|
|||||||
}
|
}
|
||||||
|
|
||||||
test('ordinary exterior and floor switches preserve the GLB_METER camera projection', async ({ page }, testInfo) => {
|
test('ordinary exterior and floor switches preserve the GLB_METER camera projection', async ({ page }, testInfo) => {
|
||||||
await page.goto('/')
|
await openGuide(page)
|
||||||
await Promise.all([
|
|
||||||
page.waitForURL(/tab=guide/, { timeout: 30_000 }),
|
|
||||||
page.getByText('馆内', { exact: true }).click()
|
|
||||||
])
|
|
||||||
await expect.poll(async () => (await getApiState(page)) !== null, { timeout: 180_000 }).toBe(true)
|
|
||||||
await expect.poll(async () => (await getApiState(page))?.report.anchors.length ?? 0, {
|
await expect.poll(async () => (await getApiState(page))?.report.anchors.length ?? 0, {
|
||||||
timeout: 180_000
|
timeout: 180_000
|
||||||
}).toBe(3)
|
}).toBe(3)
|
||||||
@@ -337,12 +366,7 @@ test('ordinary exterior and floor switches preserve the GLB_METER camera project
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('queued cross-floor search POI focus keeps raw GLB_METER coordinates and does not rebound', async ({ page }, testInfo) => {
|
test('queued cross-floor search POI focus keeps raw GLB_METER coordinates and does not rebound', async ({ page }, testInfo) => {
|
||||||
await page.goto('/')
|
await openGuide(page)
|
||||||
await Promise.all([
|
|
||||||
page.waitForURL(/tab=guide/, { timeout: 30_000 }),
|
|
||||||
page.getByText('馆内', { exact: true }).click()
|
|
||||||
])
|
|
||||||
await expect.poll(async () => (await getApiState(page)) !== null, { timeout: 180_000 }).toBe(true)
|
|
||||||
|
|
||||||
const state = await getApiState(page)
|
const state = await getApiState(page)
|
||||||
expect(state).not.toBeNull()
|
expect(state).not.toBeNull()
|
||||||
@@ -410,12 +434,7 @@ test('queued cross-floor search POI focus keeps raw GLB_METER coordinates and do
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('reset invalidates an in-flight cross-floor focus before it can overwrite the initial state', async ({ page }) => {
|
test('reset invalidates an in-flight cross-floor focus before it can overwrite the initial state', async ({ page }) => {
|
||||||
await page.goto('/')
|
await openGuide(page)
|
||||||
await Promise.all([
|
|
||||||
page.waitForURL(/tab=guide/, { timeout: 30_000 }),
|
|
||||||
page.getByText('馆内', { exact: true }).click()
|
|
||||||
])
|
|
||||||
await expect.poll(async () => (await getApiState(page)) !== null, { timeout: 180_000 }).toBe(true)
|
|
||||||
await expect.poll(async () => (await getApiState(page))?.floors.length ?? 0, { timeout: 180_000 }).toBeGreaterThan(0)
|
await expect.poll(async () => (await getApiState(page))?.floors.length ?? 0, { timeout: 180_000 }).toBeGreaterThan(0)
|
||||||
const state = await getApiState(page)
|
const state = await getApiState(page)
|
||||||
expect(state).not.toBeNull()
|
expect(state).not.toBeNull()
|
||||||
@@ -440,11 +459,7 @@ test('reset invalidates an in-flight cross-floor focus before it can overwrite t
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('floor view baseline is deterministic across repeated resets', async ({ page }) => {
|
test('floor view baseline is deterministic across repeated resets', async ({ page }) => {
|
||||||
await page.goto('/')
|
await openGuide(page)
|
||||||
await Promise.all([
|
|
||||||
page.waitForURL(/tab=guide/, { timeout: 30_000 }),
|
|
||||||
page.getByText('馆内', { exact: true }).click()
|
|
||||||
])
|
|
||||||
await expect.poll(async () => (await getApiState(page))?.floors.length ?? 0, { timeout: 180_000 }).toBeGreaterThan(0)
|
await expect.poll(async () => (await getApiState(page))?.floors.length ?? 0, { timeout: 180_000 }).toBeGreaterThan(0)
|
||||||
const state = await getApiState(page)
|
const state = await getApiState(page)
|
||||||
expect(state).not.toBeNull()
|
expect(state).not.toBeNull()
|
||||||
@@ -478,3 +493,69 @@ test('floor view baseline is deterministic across repeated resets', async ({ pag
|
|||||||
expect(getMaximumCameraDelta(baseline!, second.camera)).toBeLessThan(1e-6)
|
expect(getMaximumCameraDelta(baseline!, second.camera)).toBeLessThan(1e-6)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.describe('mobile POI card close restores the floor baseline', () => {
|
||||||
|
test.use({ viewport: { width: 390, height: 844 } })
|
||||||
|
|
||||||
|
test('real canvas clicks on a facility and hall close back to their floor baselines', async ({ page }) => {
|
||||||
|
await openGuide(page)
|
||||||
|
const state = await getApiState(page)
|
||||||
|
expect(state).not.toBeNull()
|
||||||
|
|
||||||
|
const floorId = state!.report.floorId
|
||||||
|
await page.evaluate(async (targetFloorId) => {
|
||||||
|
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi })
|
||||||
|
.__GUIDE_3D_VISUAL_STABILITY__
|
||||||
|
if (!api) throw new Error('ThreeMap visual stability diagnostics unavailable')
|
||||||
|
await api.resetToViewBaseline({ view: 'floor', floorId: targetFloorId, reason: 'floor-reset' })
|
||||||
|
}, floorId)
|
||||||
|
await expect.poll(async () => getVisiblePoiScreenPositions(page), { timeout: 30_000 })
|
||||||
|
.not.toHaveLength(0)
|
||||||
|
const visibleMarkers = await getVisiblePoiScreenPositions(page)
|
||||||
|
const facility = visibleMarkers.find((poi) => (
|
||||||
|
poi.kind !== 'hall'
|
||||||
|
&& !poi.primaryCategory.startsWith('exhibition_hall')
|
||||||
|
))
|
||||||
|
const hall = visibleMarkers.find((poi) => (
|
||||||
|
poi.kind === 'hall'
|
||||||
|
|| poi.primaryCategory.startsWith('exhibition_hall')
|
||||||
|
))
|
||||||
|
expect(facility, 'expected a visible facility POI from the active guide data').toBeDefined()
|
||||||
|
expect(hall, 'expected a visible hall POI from the active guide data').toBeDefined()
|
||||||
|
|
||||||
|
for (const [kind, marker] of [['facility', facility!], ['hall', hall!]] as const) {
|
||||||
|
const baseline = await page.evaluate((floorId) => {
|
||||||
|
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi })
|
||||||
|
.__GUIDE_3D_VISUAL_STABILITY__
|
||||||
|
return api?.getFloorBaseline(floorId) || null
|
||||||
|
}, marker.floorId)
|
||||||
|
expect(baseline).not.toBeNull()
|
||||||
|
let modelRequestsAfterBaseline = 0
|
||||||
|
page.on('request', (request) => {
|
||||||
|
if (/\.(glb|gltf)(?:$|\?)/i.test(request.url())) modelRequestsAfterBaseline += 1
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.mouse.click(marker.screen.x, marker.screen.y)
|
||||||
|
await expect.poll(async () => (await getReport(page)).activeFocusPoiId, { timeout: 10_000 }).toBe(marker.poiId)
|
||||||
|
await expect(page.locator('.guide-poi-card')).toBeVisible()
|
||||||
|
if (kind === 'facility') {
|
||||||
|
await page.locator('.poi-card-collapsed').click()
|
||||||
|
}
|
||||||
|
await page.locator('.poi-card-close').click()
|
||||||
|
await expect(page.locator('.guide-poi-card')).toBeHidden()
|
||||||
|
await expect.poll(async () => (await getReport(page)).activeFocusPoiId, { timeout: 10_000 }).toBe('')
|
||||||
|
await expect.poll(async () => (await getReport(page)).focus, { timeout: 10_000 }).toBeNull()
|
||||||
|
await expect.poll(async () => (await getReport(page)).hasPendingTargetFocus, { timeout: 10_000 }).toBe(false)
|
||||||
|
await expect.poll(async () => (await getReport(page)).isCameraTweening, { timeout: 10_000 }).toBe(false)
|
||||||
|
|
||||||
|
const afterClose = await getReport(page)
|
||||||
|
expect(afterClose.activeView).toBe('floor')
|
||||||
|
expect(afterClose.floorId).toBe(marker.floorId)
|
||||||
|
expect(getMaximumCameraDelta(baseline!, afterClose.camera)).toBeLessThan(1e-6)
|
||||||
|
expect(modelRequestsAfterBaseline).toBe(0)
|
||||||
|
await page.waitForTimeout(300)
|
||||||
|
const afterIdle = await getReport(page)
|
||||||
|
expect(getMaximumCameraDelta(afterClose.camera, afterIdle.camera)).toBeLessThan(1e-6)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
34
tests/unit/GuideModelStateReset.spec.ts
Normal file
34
tests/unit/GuideModelStateReset.spec.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { beforeEach, describe, expect, it } from 'vitest'
|
||||||
|
import { useGuideModelState } from '@/composables/useGuideModelState'
|
||||||
|
|
||||||
|
describe('guide model floor baseline reset queue', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const state = useGuideModelState()
|
||||||
|
state.invalidatePendingRequests()
|
||||||
|
const pending = state.consumePendingViewBaselineReset()
|
||||||
|
if (pending) state.completeViewBaselineReset(pending)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps only the latest pre-ready reset and consumes it once', () => {
|
||||||
|
const state = useGuideModelState()
|
||||||
|
const first = state.beginViewBaselineReset({ floorId: 'L1', rendererReason: 'poi-detail-close' })
|
||||||
|
expect(state.queueViewBaselineReset(first)).toBe(true)
|
||||||
|
const second = state.beginViewBaselineReset({ floorId: 'L2', rendererReason: 'floor-reset' })
|
||||||
|
expect(state.queueViewBaselineReset(second)).toBe(true)
|
||||||
|
|
||||||
|
expect(state.consumePendingViewBaselineReset()).toEqual(second)
|
||||||
|
expect(state.consumePendingViewBaselineReset()).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not let an obsolete reset queue or commit after a newer request', () => {
|
||||||
|
const state = useGuideModelState()
|
||||||
|
const obsolete = state.beginViewBaselineReset({ floorId: 'L1', rendererReason: 'poi-detail-close' })
|
||||||
|
const latest = state.beginViewBaselineReset({ floorId: 'L2', rendererReason: 'floor-reset' })
|
||||||
|
|
||||||
|
expect(state.isCurrentRequest(obsolete.requestId)).toBe(false)
|
||||||
|
expect(state.queueViewBaselineReset(obsolete)).toBe(false)
|
||||||
|
expect(state.completeViewBaselineReset(obsolete)).toBe(false)
|
||||||
|
expect(state.queueViewBaselineReset(latest)).toBe(true)
|
||||||
|
expect(state.consumePendingViewBaselineReset()).toEqual(latest)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -94,7 +94,7 @@ const GuideMapShellStub = defineComponent({
|
|||||||
visiblePoiIds: { type: Array, default: null },
|
visiblePoiIds: { type: Array, default: null },
|
||||||
targetFocusRequest: { type: Object, default: null }
|
targetFocusRequest: { type: Object, default: null }
|
||||||
},
|
},
|
||||||
emits: ['floor-change', 'poi-click', 'selection-clear'],
|
emits: ['floor-change', 'poi-click', 'selection-clear', 'indoor-view-change'],
|
||||||
setup() {
|
setup() {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
testState.mapMountCount += 1
|
testState.mapMountCount += 1
|
||||||
@@ -363,6 +363,25 @@ describe('首页搜索与地图闭环', () => {
|
|||||||
expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
|
expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('楼层数据按高到低返回时,首次进入仍将 1F 作为自动缩放的默认目标', async () => {
|
||||||
|
const originalFloors = [...floors]
|
||||||
|
floors.splice(0, floors.length,
|
||||||
|
{ id: 'L5', label: '5F', order: 5 },
|
||||||
|
{ id: 'L2', label: '2F', order: 2 },
|
||||||
|
{ id: 'L1', label: '1F', order: 1 }
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const wrapper = await mountIndex()
|
||||||
|
const map = wrapper.getComponent(GuideMapShellStub)
|
||||||
|
|
||||||
|
expect(map.props('activeFloor')).toBe('L1')
|
||||||
|
expect(wrapper.getComponent(PoiSearchPanelStub).props('currentFloorId')).toBe('L1')
|
||||||
|
} finally {
|
||||||
|
floors.splice(0, floors.length, ...originalFloors)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('分类结果进入详情后恢复目标楼层标准视图', async () => {
|
it('分类结果进入详情后恢复目标楼层标准视图', async () => {
|
||||||
const wrapper = await mountIndex()
|
const wrapper = await mountIndex()
|
||||||
const search = wrapper.getComponent(PoiSearchPanelStub)
|
const search = wrapper.getComponent(PoiSearchPanelStub)
|
||||||
@@ -606,6 +625,74 @@ describe('首页搜索与地图闭环', () => {
|
|||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
expect(wrapper.getComponent(PoiSearchPanelStub).exists()).toBe(true)
|
expect(wrapper.getComponent(PoiSearchPanelStub).exists()).toBe(true)
|
||||||
expect(testState.searchMountCount).toBe(1)
|
expect(testState.searchMountCount).toBe(1)
|
||||||
|
expect(testState.resetToViewBaselineCalls).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('直接点选普通设施后展开并关闭卡片,恢复该楼层视觉基线且不重挂载地图', async () => {
|
||||||
|
const wrapper = await mountIndex()
|
||||||
|
const map = wrapper.getComponent(GuideMapShellStub)
|
||||||
|
|
||||||
|
map.vm.$emit('poi-click', {
|
||||||
|
...poi,
|
||||||
|
primaryCategory: 'restroom',
|
||||||
|
primaryCategoryZh: '卫生间'
|
||||||
|
})
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.find('.poi-card-collapsed').exists()).toBe(true)
|
||||||
|
await wrapper.find('.poi-card-collapsed').trigger('tap')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
await wrapper.find('.poi-card-close').trigger('tap')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(testState.resetToViewBaselineCalls).toEqual([{
|
||||||
|
view: 'floor',
|
||||||
|
floorId: poi.floorId,
|
||||||
|
reason: 'floor-reset'
|
||||||
|
}])
|
||||||
|
expect(wrapper.find('.guide-poi-card').exists()).toBe(false)
|
||||||
|
expect(map.props('visiblePoiIds')).toBeNull()
|
||||||
|
expect(map.props('targetFocusRequest')).toBeNull()
|
||||||
|
expect(testState.mapMountCount).toBe(1)
|
||||||
|
expect(testState.mapUnmountCount).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('直接点选展厅后关闭卡片,复用楼层基线复位而非详情返回语义', async () => {
|
||||||
|
const wrapper = await mountIndex()
|
||||||
|
const map = wrapper.getComponent(GuideMapShellStub)
|
||||||
|
|
||||||
|
map.vm.$emit('poi-click', {
|
||||||
|
...poi,
|
||||||
|
kind: 'hall',
|
||||||
|
primaryCategory: 'exhibition_hall',
|
||||||
|
primaryCategoryZh: '展厅'
|
||||||
|
})
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.find('.poi-card-close').exists()).toBe(true)
|
||||||
|
await wrapper.find('.poi-card-close').trigger('tap')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(testState.resetToViewBaselineCalls).toEqual([{
|
||||||
|
view: 'floor',
|
||||||
|
floorId: poi.floorId,
|
||||||
|
reason: 'floor-reset'
|
||||||
|
}])
|
||||||
|
expect(wrapper.find('.guide-poi-card').exists()).toBe(false)
|
||||||
|
expect(testState.mapMountCount).toBe(1)
|
||||||
|
expect(testState.mapUnmountCount).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renderer 清选和返回建筑外观不会触发楼层复位', async () => {
|
||||||
|
const wrapper = await mountIndex()
|
||||||
|
const map = wrapper.getComponent(GuideMapShellStub)
|
||||||
|
|
||||||
|
map.vm.$emit('poi-click', { ...poi, primaryCategory: 'restroom' })
|
||||||
|
map.vm.$emit('selection-clear')
|
||||||
|
map.vm.$emit('indoor-view-change', 'overview')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(testState.resetToViewBaselineCalls).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('单结果分类写入 resultCount=1,主动取消恢复默认标记并清除目标', async () => {
|
it('单结果分类写入 resultCount=1,主动取消恢复默认标记并清除目标', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user