From 4a6a7f9c2963fd0b177c077d37cbf9c223b67989 Mon Sep 17 00:00:00 2001
From: lyf <2514544224@qq.com>
Date: Mon, 6 Jul 2026 10:11:20 +0800
Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E5=8A=A0=E8=BD=BD=E9=A1=B5?=
=?UTF-8?q?=E4=B8=8E=E9=A6=96=E9=A1=B5=E6=A8=A1=E5=9E=8B=E5=8A=A0=E8=BD=BD?=
=?UTF-8?q?=E7=8A=B6=E6=80=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/map/ThreeMap.vue | 26 +++-
src/components/navigation/GuideMapShell.vue | 14 +++
src/pages/index/index.vue | 125 ++++++++++++++++++--
3 files changed, 151 insertions(+), 14 deletions(-)
diff --git a/src/components/map/ThreeMap.vue b/src/components/map/ThreeMap.vue
index e9666d5..a582b5b 100644
--- a/src/components/map/ThreeMap.vue
+++ b/src/components/map/ThreeMap.vue
@@ -75,6 +75,14 @@ type CameraPreset = 'top' | 'oblique'
type TouchGestureMode = 'orbit' | 'pan'
type ZoomCameraSource = 'button' | 'gesture'
+interface InitialModelProgressEvent {
+ progress: number
+ message: string
+ view: ViewMode
+ floorId?: string
+ elapsedMs?: number
+}
+
const CANVAS_FONT_FAMILY = '"鸿蒙黑体", "HarmonyOS Sans SC", "HarmonyOS Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
const FLOOR_CAMERA_DISTANCE_FACTOR = 0.78
const SGS_VISUAL_RENDER_CONFIG = {
@@ -300,6 +308,7 @@ 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' }]
+ initialModelProgress: [event: InitialModelProgressEvent]
initialModelReady: [event: { view: ViewMode; floorId?: string; elapsedMs?: number }]
initialModelFailed: [event: { view: ViewMode; floorId?: string; message: string; elapsedMs?: number }]
}>()
@@ -385,6 +394,7 @@ let cameraTween: CameraTweenState | null = null
let adjacentPreloadSeq = 0
let firstModelLoadStartedAt = 0
let firstModelVisibleReported = false
+let initialModelSettled = false
const cameraTweenEnabled = true
const cameraTweenDurationMs = 480
@@ -1442,8 +1452,19 @@ const handleControlEnd = () => {
}
const setProgress = (progress: number, message: string) => {
- loadingProgress.value = Math.min(100, Math.max(0, progress))
+ const normalizedProgress = Math.min(100, Math.max(0, progress))
+ loadingProgress.value = normalizedProgress
loadingMessage.value = message
+
+ if (firstModelLoadStartedAt && !initialModelSettled) {
+ emit('initialModelProgress', {
+ progress: normalizedProgress,
+ message,
+ view: activeView.value,
+ floorId: currentFloor.value,
+ elapsedMs: Math.round(getNow() - firstModelLoadStartedAt)
+ })
+ }
}
const markFirstModelVisible = (
@@ -4713,6 +4734,7 @@ const init3DScene = async () => {
isDisposed = false
firstModelLoadStartedAt = getNow()
firstModelVisibleReported = false
+ initialModelSettled = false
isLoading.value = true
loadError.value = false
selectedPOI.value = null
@@ -4727,6 +4749,7 @@ const init3DScene = async () => {
await loadModelPackage()
setProgress(100, '馆内三维模型加载完成')
+ initialModelSettled = true
isLoading.value = false
emit('initialModelReady', {
view: activeView.value,
@@ -4743,6 +4766,7 @@ const init3DScene = async () => {
isLoading.value = false
const message = error instanceof Error ? error.message : '请检查模型资源或网络状态后重试'
setProgress(0, message)
+ initialModelSettled = true
emit('initialModelFailed', {
view: activeView.value,
floorId: currentFloor.value,
diff --git a/src/components/navigation/GuideMapShell.vue b/src/components/navigation/GuideMapShell.vue
index 4aa18a1..034eb9e 100644
--- a/src/components/navigation/GuideMapShell.vue
+++ b/src/components/navigation/GuideMapShell.vue
@@ -26,6 +26,7 @@
@selection-clear="handleSelectionClear"
@target-focus="handleTargetFocus"
@auto-switch="handleAutoSwitch"
+ @initial-model-progress="handleInitialModelProgress"
@initial-model-ready="handleInitialModelReady"
@initial-model-failed="handleInitialModelFailed"
/>
@@ -289,6 +290,14 @@ type IndoorViewMode = 'overview' | 'floor' | 'multi'
type LayerDisplayMode = 'single' | 'multi'
type TouchGestureMode = 'orbit' | 'pan'
+interface InitialModelProgressEvent {
+ progress: number
+ message: string
+ view: IndoorViewMode
+ floorId?: string
+ elapsedMs?: number
+}
+
const props = withDefaults(defineProps<{
searchText?: string
activeMode?: '2d' | '3d'
@@ -403,6 +412,7 @@ const emit = defineEmits<{
selectionClear: []
targetFocus: [result: TargetPoiFocusResult]
autoSwitch: [event: { from: 'overview' | 'floor'; to: 'overview' | 'floor'; trigger: string; distance: number }]
+ initialModelProgress: [event: InitialModelProgressEvent]
initialModelReady: [event: { view: IndoorViewMode; floorId?: string; elapsedMs?: number }]
initialModelFailed: [event: { view: IndoorViewMode; floorId?: string; message: string; elapsedMs?: number }]
mapTap: [location: { latitude: number; longitude: number }]
@@ -710,6 +720,10 @@ const handleAutoSwitch = (event: { from: 'overview' | 'floor'; to: 'overview' |
emit('autoSwitch', event)
}
+const handleInitialModelProgress = (event: InitialModelProgressEvent) => {
+ emit('initialModelProgress', event)
+}
+
const handleInitialModelReady = (event: { view: IndoorViewMode; floorId?: string; elapsedMs?: number }) => {
emit('initialModelReady', event)
}
diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue
index 8ec17db..7063859 100644
--- a/src/pages/index/index.vue
+++ b/src/pages/index/index.vue
@@ -51,6 +51,7 @@
@poi-click="handleGuidePoiClick"
@selection-clear="handleGuideSelectionClear"
@auto-switch="handleAutoSwitch"
+ @initial-model-progress="handleInitialModelProgress"
@initial-model-ready="handleInitialModelReady"
@initial-model-failed="handleInitialModelFailed"
@tool-click="handleIndoorToolClick"
@@ -255,10 +256,18 @@
Shenzhen Natural History Museum
- 正在进入自然导览
+ {{ launchLoadingText }}
+
+
+ 重新加载
+
+
+ 继续进入
+
+
@@ -346,6 +355,7 @@ const topTabFromHash = () => {
return isGuideTopTab(tab) ? tab : null
}
+const launchOverlayMinDisplayMs = 700
const launchOverlayGuideTimeoutMs = 12000
const launchOverlayNonGuideTimeoutMs = 900
@@ -372,8 +382,10 @@ const selectedGuidePoi = ref(null)
const currentTab = ref(initialTopTab())
const launchOverlayVisible = ref(shouldShowLaunchOverlay())
const launchOverlayLeaving = ref(false)
-const launchProgress = ref(18)
+const launchProgress = ref(8)
+const launchLoadingText = ref('正在初始化三维场景')
const launchInitialModelSettled = ref(false)
+const launchOverlayRecoverable = ref(false)
const showRoutePlanner = ref(false)
const routeStartPoint = ref(null)
const routeEndPoint = ref(null)
@@ -396,7 +408,8 @@ const homeSearchExpanded = ref(false)
let launchOverlayFallbackTimer: ReturnType | null = null
let launchOverlayFadeTimer: ReturnType | null = null
-let launchProgressTimer: ReturnType | null = null
+let launchOverlayHideDelayTimer: ReturnType | null = null
+let launchOverlayStartedAt = 0
const launchProgressStyle = computed(() => ({
width: `${launchProgress.value}%`
@@ -413,13 +426,13 @@ const clearLaunchTimers = () => {
launchOverlayFadeTimer = null
}
- if (launchProgressTimer) {
- clearInterval(launchProgressTimer)
- launchProgressTimer = null
+ if (launchOverlayHideDelayTimer) {
+ clearTimeout(launchOverlayHideDelayTimer)
+ launchOverlayHideDelayTimer = null
}
}
-const hideLaunchOverlay = () => {
+const completeHideLaunchOverlay = () => {
if (!launchOverlayVisible.value) return
launchProgress.value = 100
@@ -433,6 +446,24 @@ const hideLaunchOverlay = () => {
}, 360)
}
+const hideLaunchOverlay = () => {
+ if (!launchOverlayVisible.value || launchOverlayLeaving.value) return
+
+ const elapsedMs = launchOverlayStartedAt ? Date.now() - launchOverlayStartedAt : launchOverlayMinDisplayMs
+ const waitMs = Math.max(0, launchOverlayMinDisplayMs - elapsedMs)
+
+ if (waitMs > 0) {
+ if (launchOverlayHideDelayTimer) clearTimeout(launchOverlayHideDelayTimer)
+ launchOverlayHideDelayTimer = setTimeout(() => {
+ launchOverlayHideDelayTimer = null
+ completeHideLaunchOverlay()
+ }, waitMs)
+ return
+ }
+
+ completeHideLaunchOverlay()
+}
+
const shouldWaitForLaunchModel = () => (
currentTab.value === 'guide'
&& is3DMode.value
@@ -443,10 +474,12 @@ const startLaunchOverlay = () => {
clearLaunchTimers()
launchInitialModelSettled.value = false
-
- launchProgressTimer = setInterval(() => {
- launchProgress.value = Math.min(92, launchProgress.value + 11)
- }, 180)
+ launchOverlayRecoverable.value = false
+ launchOverlayStartedAt = Date.now()
+ launchProgress.value = shouldWaitForLaunchModel() ? 8 : 68
+ launchLoadingText.value = shouldWaitForLaunchModel()
+ ? '正在初始化三维场景'
+ : '正在进入自然导览'
const timeoutMs = shouldWaitForLaunchModel()
? launchOverlayGuideTimeoutMs
@@ -454,24 +487,53 @@ const startLaunchOverlay = () => {
launchOverlayFallbackTimer = setTimeout(() => {
if (shouldWaitForLaunchModel() && !launchInitialModelSettled.value) {
- console.warn(`首页前置加载页等待模型超过 ${timeoutMs}ms,交给页面自身加载态接管`)
+ launchOverlayRecoverable.value = true
+ launchProgress.value = Math.min(96, Math.max(launchProgress.value, 92))
+ launchLoadingText.value = '三维场景加载较慢,仍在等待模型就绪'
+ console.warn(`首页前置加载页等待模型超过 ${timeoutMs}ms,继续等待真实模型 ready`)
+ return
}
hideLaunchOverlay()
}, timeoutMs)
}
+const handleInitialModelProgress = (event: { progress: number; message: string; view: GuideIndoorView; floorId?: string; elapsedMs?: number }) => {
+ if (!launchOverlayVisible.value || launchInitialModelSettled.value || !shouldWaitForLaunchModel()) return
+
+ launchOverlayRecoverable.value = false
+ launchProgress.value = Math.min(99, Math.max(8, event.progress))
+ launchLoadingText.value = event.message || '正在加载三维场景'
+}
+
const settleLaunchOverlayByModel = (status: 'ready' | 'failed', event?: { view: GuideIndoorView; floorId?: string; message?: string; elapsedMs?: number }) => {
launchInitialModelSettled.value = true
+ launchOverlayRecoverable.value = status === 'failed'
if (status === 'failed') {
- console.warn('首页前置加载页收到初始模型加载失败,交给 ThreeMap 错误态接管:', event)
+ launchProgress.value = Math.min(launchProgress.value, 96)
+ launchLoadingText.value = event?.message || '三维场景加载失败'
+ console.warn('首页前置加载页收到初始模型加载失败,等待用户选择重试或进入降级首页:', event)
+ return
} else {
+ launchProgress.value = 100
+ launchLoadingText.value = '三维场景已就绪'
console.log('首页前置加载页收到初始模型加载完成:', event)
}
hideLaunchOverlay()
}
+const handleLaunchRetry = () => {
+ if (typeof window !== 'undefined') {
+ window.location.reload()
+ }
+}
+
+const handleLaunchContinue = () => {
+ launchOverlayRecoverable.value = false
+ hideLaunchOverlay()
+}
+
// 馆外导航相关状态
const isOutdoorGuidePanelEnabled = false
const showOutdoorNavPanel = ref(false)
@@ -1588,6 +1650,43 @@ const handleExplainBack = () => {
transition: width 0.22s ease;
}
+.app-launch-actions {
+ margin-top: 14px;
+ display: flex;
+ gap: 10px;
+}
+
+.app-launch-action {
+ height: 36px;
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ border-radius: 8px;
+}
+
+.app-launch-action.secondary {
+ background: rgba(255, 255, 255, 0.12);
+ border: 1px solid rgba(255, 255, 255, 0.38);
+}
+
+.app-launch-action.primary {
+ background: var(--museum-accent);
+ border: 1px solid var(--museum-accent);
+}
+
+.app-launch-action-text {
+ font-size: 13px;
+ line-height: 18px;
+ font-weight: 700;
+ color: #ffffff;
+}
+
+.app-launch-action.primary .app-launch-action-text {
+ color: #151713;
+}
+
@media (max-width: 360px) {
.app-launch-brand {
top: calc(env(safe-area-inset-top) + 48px);