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);