From 4b581da81d5fb10a7b3885da877ecac630113d95 Mon Sep 17 00:00:00 2001 From: lyf <2514544224@qq.com> Date: Sun, 5 Jul 2026 19:57:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=A6=96=E9=A1=B5=E5=89=8D?= =?UTF-8?q?=E7=BD=AE=E5=8A=A0=E8=BD=BD=E9=A1=B5=E5=B9=B6=E7=AD=89=E5=BE=85?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=B0=B1=E7=BB=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/copy-h5-nav-assets.cjs | 1 + src/components/map/ThreeMap.vue | 16 +- src/components/navigation/GuideMapShell.vue | 12 + src/pages/index/index.vue | 280 ++++++++++++++++++++ static/guide/app-launch-loading.webp | Bin 0 -> 127346 bytes 5 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 static/guide/app-launch-loading.webp diff --git a/scripts/copy-h5-nav-assets.cjs b/scripts/copy-h5-nav-assets.cjs index f8c5dc4..9a95ab6 100644 --- a/scripts/copy-h5-nav-assets.cjs +++ b/scripts/copy-h5-nav-assets.cjs @@ -69,6 +69,7 @@ const copyStaticFile = (fileName) => { copyStaticSubtree('nav-assets') copyStaticSubtree('guide-data') +copyStaticSubtree('guide') copyStaticSubtree('icons') // 讲解业务单元封面由 H5 运行时按 /static/explain/... 动态加载。 copyStaticSubtree('explain') diff --git a/src/components/map/ThreeMap.vue b/src/components/map/ThreeMap.vue index f80019a..e9666d5 100644 --- a/src/components/map/ThreeMap.vue +++ b/src/components/map/ThreeMap.vue @@ -300,6 +300,8 @@ const emit = defineEmits<{ targetFocus: [result: TargetPoiFocusResult] autoSwitch: [event: { from: 'overview' | 'floor'; to: 'overview' | 'floor'; trigger: 'zoom-in' | 'zoom-out'; distance: number }] autoSwitchBlocked: [reason: { reason: 'loading-locked' | 'cooldown' | 'disabled' }] + initialModelReady: [event: { view: ViewMode; floorId?: string; elapsedMs?: number }] + initialModelFailed: [event: { view: ViewMode; floorId?: string; message: string; elapsedMs?: number }] }>() const containerRef = ref(null) @@ -4726,6 +4728,11 @@ const init3DScene = async () => { setProgress(100, '馆内三维模型加载完成') isLoading.value = false + emit('initialModelReady', { + view: activeView.value, + floorId: currentFloor.value, + elapsedMs: firstModelLoadStartedAt ? Math.round(getNow() - firstModelLoadStartedAt) : undefined + }) await syncRequestedIndoorView() queueTargetFocus(pendingTargetFocus || props.targetFocus || null) } catch (error) { @@ -4734,7 +4741,14 @@ const init3DScene = async () => { console.error('馆内 3D 模型资源加载失败:', error) loadError.value = true isLoading.value = false - setProgress(0, error instanceof Error ? error.message : '请检查模型资源或网络状态后重试') + const message = error instanceof Error ? error.message : '请检查模型资源或网络状态后重试' + setProgress(0, message) + emit('initialModelFailed', { + view: activeView.value, + floorId: currentFloor.value, + message, + elapsedMs: firstModelLoadStartedAt ? Math.round(getNow() - firstModelLoadStartedAt) : undefined + }) } } diff --git a/src/components/navigation/GuideMapShell.vue b/src/components/navigation/GuideMapShell.vue index 68dbccc..4aa18a1 100644 --- a/src/components/navigation/GuideMapShell.vue +++ b/src/components/navigation/GuideMapShell.vue @@ -26,6 +26,8 @@ @selection-clear="handleSelectionClear" @target-focus="handleTargetFocus" @auto-switch="handleAutoSwitch" + @initial-model-ready="handleInitialModelReady" + @initial-model-failed="handleInitialModelFailed" /> @@ -401,6 +403,8 @@ const emit = defineEmits<{ selectionClear: [] targetFocus: [result: TargetPoiFocusResult] autoSwitch: [event: { from: 'overview' | 'floor'; to: 'overview' | 'floor'; trigger: string; distance: number }] + initialModelReady: [event: { view: IndoorViewMode; floorId?: string; elapsedMs?: number }] + initialModelFailed: [event: { view: IndoorViewMode; floorId?: string; message: string; elapsedMs?: number }] mapTap: [location: { latitude: number; longitude: number }] }>() @@ -706,6 +710,14 @@ const handleAutoSwitch = (event: { from: 'overview' | 'floor'; to: 'overview' | emit('autoSwitch', event) } +const handleInitialModelReady = (event: { view: IndoorViewMode; floorId?: string; elapsedMs?: number }) => { + emit('initialModelReady', event) +} + +const handleInitialModelFailed = (event: { view: IndoorViewMode; floorId?: string; message: string; elapsedMs?: number }) => { + emit('initialModelFailed', event) +} + const handleMapTap = (location: { latitude?: number; longitude?: number }) => { const { latitude, longitude } = location if (latitude !== undefined && longitude !== undefined) { diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue index a81e899..13d505e 100644 --- a/src/pages/index/index.vue +++ b/src/pages/index/index.vue @@ -52,6 +52,8 @@ @poi-click="handleGuidePoiClick" @selection-clear="handleGuideSelectionClear" @auto-switch="handleAutoSwitch" + @initial-model-ready="handleInitialModelReady" + @initial-model-failed="handleInitialModelFailed" @tool-click="handleIndoorToolClick" @map-tap="handleMapTapForOutdoorNav" > @@ -217,6 +219,32 @@ @back="handleExplainBack" /> + + + + + + + 探索万物生息 + 深圳自然博物馆 + Shenzhen Natural History Museum + + + 正在进入自然导览 + + + + + + @@ -300,6 +328,14 @@ const topTabFromHash = () => { return isGuideTopTab(tab) ? tab : null } +const launchOverlayGuideTimeoutMs = 12000 +const launchOverlayNonGuideTimeoutMs = 900 + +const shouldShowLaunchOverlay = () => { + if (typeof window === 'undefined') return false + return isIndexHashRoute() +} + const initialTopTab = (): GuideTopTab => topTabFromHash() || 'guide' // 3D 模式状态 @@ -316,6 +352,10 @@ const selectedGuidePoi = ref(null) // 状态 const currentTab = ref(initialTopTab()) +const launchOverlayVisible = ref(shouldShowLaunchOverlay()) +const launchOverlayLeaving = ref(false) +const launchProgress = ref(18) +const launchInitialModelSettled = ref(false) const showRoutePlanner = ref(false) const routeStartPoint = ref(null) const routeEndPoint = ref(null) @@ -335,6 +375,84 @@ const guideMapShellRef = ref<{ clearRoute?: () => void } | null>(null) +let launchOverlayFallbackTimer: ReturnType | null = null +let launchOverlayFadeTimer: ReturnType | null = null +let launchProgressTimer: ReturnType | null = null + +const launchProgressStyle = computed(() => ({ + width: `${launchProgress.value}%` +})) + +const clearLaunchTimers = () => { + if (launchOverlayFallbackTimer) { + clearTimeout(launchOverlayFallbackTimer) + launchOverlayFallbackTimer = null + } + + if (launchOverlayFadeTimer) { + clearTimeout(launchOverlayFadeTimer) + launchOverlayFadeTimer = null + } + + if (launchProgressTimer) { + clearInterval(launchProgressTimer) + launchProgressTimer = null + } +} + +const hideLaunchOverlay = () => { + if (!launchOverlayVisible.value) return + + launchProgress.value = 100 + launchOverlayLeaving.value = true + clearLaunchTimers() + + launchOverlayFadeTimer = setTimeout(() => { + launchOverlayVisible.value = false + launchOverlayLeaving.value = false + launchOverlayFadeTimer = null + }, 360) +} + +const shouldWaitForLaunchModel = () => ( + currentTab.value === 'guide' + && is3DMode.value +) + +const startLaunchOverlay = () => { + if (!launchOverlayVisible.value) return + + clearLaunchTimers() + launchInitialModelSettled.value = false + + launchProgressTimer = setInterval(() => { + launchProgress.value = Math.min(92, launchProgress.value + 11) + }, 180) + + const timeoutMs = shouldWaitForLaunchModel() + ? launchOverlayGuideTimeoutMs + : launchOverlayNonGuideTimeoutMs + + launchOverlayFallbackTimer = setTimeout(() => { + if (shouldWaitForLaunchModel() && !launchInitialModelSettled.value) { + console.warn(`首页前置加载页等待模型超过 ${timeoutMs}ms,交给页面自身加载态接管`) + } + hideLaunchOverlay() + }, timeoutMs) +} + +const settleLaunchOverlayByModel = (status: 'ready' | 'failed', event?: { view: GuideIndoorView; floorId?: string; message?: string; elapsedMs?: number }) => { + launchInitialModelSettled.value = true + + if (status === 'failed') { + console.warn('首页前置加载页收到初始模型加载失败,交给 ThreeMap 错误态接管:', event) + } else { + console.log('首页前置加载页收到初始模型加载完成:', event) + } + + hideLaunchOverlay() +} + // 馆外导航相关状态 const isOutdoorGuidePanelEnabled = false const showOutdoorNavPanel = ref(false) @@ -550,6 +668,7 @@ const syncTopTabFromHash = () => { } onMounted(() => { + startLaunchOverlay() showIndoorHint() if (typeof window === 'undefined') return @@ -559,6 +678,8 @@ onMounted(() => { }) onUnmounted(() => { + clearLaunchTimers() + if (indoorGestureHintTimer) { clearTimeout(indoorGestureHintTimer) indoorGestureHintTimer = null @@ -713,6 +834,14 @@ const handleAutoSwitch = (event: { from: 'overview' | 'floor'; to: 'overview' | showIndoorHint(event.to === 'floor' ? '已进入单层视图,可点选标记查看位置' : '已返回建筑外观', 3600) } +const handleInitialModelReady = (event: { view: GuideIndoorView; floorId?: string; elapsedMs?: number }) => { + settleLaunchOverlayByModel('ready', event) +} + +const handleInitialModelFailed = (event: { view: GuideIndoorView; floorId?: string; message: string; elapsedMs?: number }) => { + settleLaunchOverlayByModel('failed', event) +} + const handleIndoorToolClick = (tool: string) => { if (!is3DMode.value) return @@ -1312,6 +1441,157 @@ const handleExplainBack = () => {