fix: 统一馆内三维模型返回初始状态

This commit is contained in:
lyf
2026-07-14 21:57:31 +08:00
parent d813f0b375
commit 84fbbc0694
8 changed files with 319 additions and 168 deletions

View File

@@ -18,7 +18,7 @@
:active-floor="activeFloor"
map-type="indoor"
:tools="[]"
:indoor-model-source="indoorModelSource"
:indoor-model-source="modelSource"
:floors="guideFloors"
:normalize-floor-id="normalizeGuideFloorId"
:target-focus-request="targetFocusRequest"
@@ -105,18 +105,17 @@ import type {
MuseumPoi
} from '@/domain/museum'
import type {
GuideModelSource,
GuideRenderPoi
} from '@/domain/guideModel'
const indoorModelSource: GuideModelSource = guideUseCase.getModelSource()
const modelSource = guideUseCase.getModelSource()
const guideFloors = ref<MuseumFloor[]>([])
const normalizeGuideFloorId = (labelOrId: string) =>
guideFloors.value.find((floor) => floor.id === labelOrId || floor.label === labelOrId)?.id
|| guideUseCase.normalizeFloorId(labelOrId)
const requestedDetailFloorId = ref('')
const activeMode = ref<'2d' | '3d'>('3d')
const activeFloor = ref('1F')
const requestedTargetFloorId = ref('')
const activeFloor = ref('')
const targetFocusRequest = ref<TargetPoiFocusRequestViewModel | null>(null)
const targetFocusRequestId = ref(0)
const locationErrorMessage = ref('')
@@ -146,68 +145,13 @@ const searchContext = ref<FacilityDetailSearchContext>(
const loadGuideFloors = async () => {
try {
guideFloors.value = await guideUseCase.getFloors()
const floors = guideFloors.value
const normalizedActiveFloorId = activeFloor.value
? normalizeGuideFloorId(activeFloor.value)
: ''
const defaultFloor = floors.find((floor) => floor.id === normalizedActiveFloorId)?.id
|| floors.find((floor) => floor.label === '1F')?.id
|| floors[0]?.id
|| activeFloor.value
activeFloor.value = defaultFloor
if (!activeFloor.value) activeFloor.value = guideFloors.value[0]?.id || ''
} catch (error) {
console.error('加载导览楼层失败:', error)
guideFloors.value = []
}
}
const focusFacilityPoi = (poi: MuseumPoi) => {
if (!poi.floorId) {
locationErrorMessage.value = '该点位缺少楼层信息,暂时无法在地图中定位。'
return
}
locationErrorMessage.value = ''
activeMode.value = '3d'
activeFloor.value = poi.floorId
targetFocusRequestId.value += 1
targetFocusRequest.value = toTargetFocusRequestViewModel({
poiId: poi.id,
name: poi.name,
floorId: poi.floorId,
floorLabel: poi.floorLabel,
primaryCategoryZh: poi.primaryCategory.label,
positionGltf: poi.positionGltf,
sourceObjectName: poi.sourceObjectName
}, targetFocusRequestId.value)
}
const focusRenderPoi = (poi: GuideRenderPoi) => {
if (!poi.floorId) {
locationErrorMessage.value = '该点位缺少楼层信息,暂时无法在地图中定位。'
return
}
locationErrorMessage.value = ''
const floorLabel = guideFloors.value.find((floor) => floor.id === poi.floorId)?.label || poi.floorId
activeMode.value = '3d'
activeFloor.value = poi.floorId
targetFocusRequestId.value += 1
targetFocusRequest.value = toTargetFocusRequestViewModel({
poiId: poi.id,
name: poi.name,
floorId: poi.floorId,
floorLabel,
primaryCategoryZh: poi.primaryCategoryZh,
positionGltf: poi.positionGltf,
sourceObjectName: poi.sourceObjectName,
kind: poi.kind,
hallId: poi.hallId,
hallName: poi.hallName,
entrances: poi.entrances
}, targetFocusRequestId.value)
}
const getRenderPoiFloorLabel = (poi: GuideRenderPoi) =>
guideFloors.value.find((floor) => floor.id === poi.floorId)?.label || poi.floorId
@@ -224,6 +168,50 @@ const applyRenderPoiToFacility = (poi: GuideRenderPoi) => {
focusRenderPoi(poi)
}
const focusFacilityPoi = (poi: MuseumPoi) => {
if (!poi.floorId) {
locationErrorMessage.value = '该点位缺少楼层信息,暂时无法提供位置预览。'
return
}
locationErrorMessage.value = ''
activeFloor.value = poi.floorId
targetFocusRequestId.value += 1
targetFocusRequest.value = toTargetFocusRequestViewModel({
poiId: poi.id,
name: poi.name,
floorId: poi.floorId,
floorLabel: poi.floorLabel,
primaryCategoryZh: poi.primaryCategory.label,
positionGltf: poi.positionGltf,
sourceObjectName: poi.sourceObjectName
}, targetFocusRequestId.value)
}
const focusRenderPoi = (poi: GuideRenderPoi) => {
if (!poi.floorId) {
locationErrorMessage.value = '该点位缺少楼层信息,暂时无法提供位置预览。'
return
}
locationErrorMessage.value = ''
activeFloor.value = poi.floorId
targetFocusRequestId.value += 1
targetFocusRequest.value = toTargetFocusRequestViewModel({
poiId: poi.id,
name: poi.name,
floorId: poi.floorId,
floorLabel: getRenderPoiFloorLabel(poi),
primaryCategoryZh: poi.primaryCategoryZh,
positionGltf: poi.positionGltf,
sourceObjectName: poi.sourceObjectName,
kind: poi.kind,
hallId: poi.hallId,
hallName: poi.hallName,
entrances: poi.entrances
}, targetFocusRequestId.value)
}
const normalizePoiName = (value: string) => value
.trim()
.replace(/[\s()【】[\]_-]/g, '')
@@ -242,7 +230,7 @@ const decodeRouteText = (value: unknown) => (
)
const isPoiOnRequestedFloor = (poi: Pick<MuseumPoi | GuideRenderPoi, 'floorId'>) => (
!requestedTargetFloorId.value || poi.floorId === requestedTargetFloorId.value
!requestedDetailFloorId.value || poi.floorId === requestedDetailFloorId.value
)
const resolvePoiFromHall = async () => {
@@ -269,7 +257,7 @@ const resolveFacilityPoi = async () => {
if (!normalizedTarget) return null
const candidates = await guideUseCase.searchPois(facility.value.name)
const floorMatchedCandidates = requestedTargetFloorId.value
const floorMatchedCandidates = requestedDetailFloorId.value
? candidates.filter(isPoiOnRequestedFloor)
: candidates
const candidatesToMatch = floorMatchedCandidates.length ? floorMatchedCandidates : candidates
@@ -289,16 +277,16 @@ const resolveRenderPoi = async () => {
const normalizedTarget = normalizePoiName(facility.value.name)
if (!resolveByExactId && !normalizedTarget) return null
const modelPackage = await indoorModelSource.loadPackage()
const floorsToSearch = requestedTargetFloorId.value
? modelPackage.floors.filter((floor) => floor.floorId === requestedTargetFloorId.value)
const modelPackage = await modelSource.loadPackage()
const floorsToSearch = requestedDetailFloorId.value
? modelPackage.floors.filter((floor) => floor.floorId === requestedDetailFloorId.value)
: modelPackage.floors
for (const floor of floorsToSearch) {
const floorId = floor.floorId
let pois: GuideRenderPoi[] = []
try {
pois = await indoorModelSource.loadFloorPois(floorId)
pois = await modelSource.loadFloorPois(floorId)
} catch (error) {
resolutionLoadFailed.value = true
console.warn(`楼层 ${floorId} 的模型点位加载失败:`, error)
@@ -362,10 +350,8 @@ onLoad(async (options: Record<string, unknown> = {}) => {
const requestedFloorId = decodeRouteText(options.floorId)
|| searchContext.value.searchFloorId
if (requestedFloorId) {
requestedTargetFloorId.value = normalizeGuideFloorId(requestedFloorId)
if (requestedTargetFloorId.value) {
activeFloor.value = requestedTargetFloorId.value
}
requestedDetailFloorId.value = normalizeGuideFloorId(requestedFloorId)
activeFloor.value = requestedDetailFloorId.value || activeFloor.value
}
const requestedFloorLabel = decodeRouteText(options.floorLabel)
@@ -403,7 +389,7 @@ onLoad(async (options: Record<string, unknown> = {}) => {
console.error('加载设施位置详情失败:', error)
locationErrorMessage.value = '位置数据加载失败,请稍后重试。'
} finally {
// 目标楼层与聚焦请求准备完成后再挂载 ThreeMap避免默认楼层的中间帧。
// Wait for the target context so the preview does not flash an unrelated floor.
mapContextReady.value = true
}
})
@@ -419,8 +405,8 @@ const handleTargetFocus = (result: TargetPoiFocusResult) => {
}
locationErrorMessage.value = result.status === 'missing'
? '该点位缺少可用的位置数据,暂时无法在地图中定位。'
: '地图定位暂不可用,请稍后重试。'
? '该点位缺少可用的位置数据,暂时无法提供位置预览。'
: '位置预览暂不可用,请稍后重试。'
}
const handleCloseDetailPanel = () => {
@@ -432,21 +418,7 @@ const handleTopTabChange = (tab: GuideTopTab) => {
}
const handlePageBack = () => {
if (searchContext.value.source === 'search' && searchContext.value.resultCount === 1) {
uni.reLaunch({
url: '/pages/index/index'
})
return
}
uni.navigateBack({
delta: 1,
fail: () => {
uni.reLaunch({
url: '/pages/index/index'
})
}
})
uni.navigateBack({ delta: 1 })
}
</script>

View File

@@ -325,9 +325,9 @@ import type {
PoiSearchContext
} from '@/domain/poiSearch'
import {
toTargetFocusRequestViewModel,
type TargetPoiFocusRequestViewModel
} from '@/view-models/guideViewModels'
import { useGuideModelState } from '@/composables/useGuideModelState'
import {
ARRIVAL_TARGETS_BY_TYPE,
type ArrivalSearchTarget,
@@ -344,6 +344,8 @@ import type {
type GuideIndoorView = 'overview' | 'floor' | 'multi'
type LayerDisplayMode = 'single' | 'multi'
const guideModelState = useGuideModelState()
const isIndexHashRoute = () => {
if (typeof window === 'undefined') return false
@@ -416,6 +418,7 @@ const returningToOverview = ref(false)
const guideMapShellRef = ref<{
clearRoute?: () => void
showOverview?: () => Promise<void> | void
resetToInitialState?: () => Promise<boolean> | boolean
} | null>(null)
const homeSearchPanelRef = ref<{
collapseHomePanel?: () => void
@@ -427,9 +430,7 @@ const homeSearchExpanded = ref(false)
const homeCategoryModeActive = ref(false)
const searchVisiblePoiIds = ref<string[] | null>(null)
const searchTargetFocusRequest = ref<TargetPoiFocusRequestViewModel | null>(null)
let searchTargetFocusRequestId = 0
let pendingCollapseSearchAfterDetail = false
let pendingSearchDetailFocusRequest: TargetPoiFocusRequestViewModel | null = null
let pendingGuideModelResetUntilReady = false
type PoiSearchOverlayHistoryState = {
museumGuideOverlay?: 'poi-search'
@@ -916,6 +917,7 @@ const loadGuideFloors = async () => {
|| floors[0]?.id
|| activeGuideFloor.value
renderedFloorId.value = activeGuideFloor.value
guideModelState.initializeModel(activeGuideFloor.value)
} catch (error) {
console.error('加载导览楼层失败:', error)
guideFloors.value = []
@@ -1031,7 +1033,12 @@ const handleAutoSwitch = (event: { from: 'overview' | 'floor'; to: 'overview' |
}
const handleInitialModelReady = (event: { view: GuideIndoorView; floorId?: string; elapsedMs?: number }) => {
guideModelState.initializeModel(event.floorId || activeGuideFloor.value)
settleLaunchOverlayByModel('ready', event)
if (pendingGuideModelResetUntilReady) {
pendingGuideModelResetUntilReady = false
void resetGuideModelToInitial({ reason: 'queued-poi-detail-close' })
}
}
const handleInitialModelFailed = (event: { view: GuideIndoorView; floorId?: string; message: string; elapsedMs?: number }) => {
@@ -1449,27 +1456,45 @@ const closeHomeSearchDock = () => {
homeSearchExpanded.value = false
}
const resetGuideModelToInitial = async ({ reason }: { reason: 'poi-detail-close' | 'queued-poi-detail-close' }) => {
const initial = guideModelState.initialState.value
guideModelState.invalidatePendingRequests()
searchTargetFocusRequest.value = null
searchVisiblePoiIds.value = null
homeCategoryModeActive.value = false
selectedGuidePoi.value = null
isPoiCardCollapsed.value = false
showRoutePlanner.value = false
activeRoutePreview.value = null
routeStartPoint.value = null
routeEndPoint.value = null
routePlanError.value = ''
isSimulatingRoute.value = false
requestedFloorId.value = ''
requestedFloorLabel.value = ''
loadingFloorId.value = ''
failedFloorId.value = ''
is3DMode.value = true
guideOutdoorState.value = 'home'
indoorView.value = initial.indoorView
activeGuideFloor.value = initial.defaultActiveFloorId || activeGuideFloor.value
renderedFloorId.value = initial.loadedFloorId || activeGuideFloor.value
closeArrivalPanel()
await homeSearchPanelRef.value?.resetSearchState?.()
closeHomeSearchDock()
const resetApplied = await guideMapShellRef.value?.resetToInitialState?.()
if (!resetApplied) {
// The renderer is still becoming ready; retain only the latest requested reset.
pendingGuideModelResetUntilReady = true
}
console.info('馆内三维模型已恢复统一初始状态:', { reason })
}
onShow(async () => {
await nextTick()
if (pendingCollapseSearchAfterDetail) {
pendingCollapseSearchAfterDetail = false
const pendingFocusRequest = pendingSearchDetailFocusRequest
pendingSearchDetailFocusRequest = null
await homeSearchPanelRef.value?.returnToCollapsedAfterDetail?.()
homeSearchExpanded.value = false
homeCategoryModeActive.value = false
searchVisiblePoiIds.value = null
if (pendingFocusRequest) {
closeArrivalPanel()
showRoutePlanner.value = false
isSimulatingRoute.value = false
selectedGuidePoi.value = null
isPoiCardCollapsed.value = false
is3DMode.value = true
indoorView.value = 'floor'
activeGuideFloor.value = pendingFocusRequest.floorId
searchTargetFocusRequest.value = pendingFocusRequest
}
if (guideModelState.consumePoiDetailReturn()) {
await resetGuideModelToInitial({ reason: 'poi-detail-close' })
return
}
@@ -1569,29 +1594,13 @@ const createSearchDetailUrl = (poi: MuseumPoi, context: PoiSearchContext) => {
}
const handleHomeSearchResultTap = async (poi: MuseumPoi, context: PoiSearchContext) => {
searchTargetFocusRequestId += 1
pendingSearchDetailFocusRequest = toTargetFocusRequestViewModel({
poiId: poi.id,
name: poi.name,
floorId: poi.floorId,
floorLabel: poi.floorLabel,
primaryCategoryZh: poi.primaryCategory.label,
positionGltf: poi.positionGltf,
sourceObjectName: poi.sourceObjectName,
kind: poi.kind,
hallId: poi.hallId,
hallName: poi.hallName,
entrances: poi.entrances
}, searchTargetFocusRequestId)
// 详情页会一次性挂载目标楼层;首页不再先启动并销毁 ThreeMap。
// The homepage keeps its renderer mounted; detail close is consumed by onShow once.
searchTargetFocusRequest.value = null
pendingCollapseSearchAfterDetail = true
guideModelState.beginPoiDetailReturn()
uni.navigateTo({
url: createSearchDetailUrl(poi, context),
fail: () => {
pendingCollapseSearchAfterDetail = false
pendingSearchDetailFocusRequest = null
guideModelState.cancelPoiDetailReturn()
uni.showToast({
title: '点位详情打开失败,请重试',
icon: 'none'