同步加载页与首页模型加载状态
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
<text class="app-launch-subtitle">Shenzhen Natural History Museum</text>
|
||||
</view>
|
||||
<view class="app-launch-status">
|
||||
<text class="app-launch-loading-text">正在进入自然导览</text>
|
||||
<text class="app-launch-loading-text">{{ launchLoadingText }}</text>
|
||||
<view class="app-launch-progress">
|
||||
<view class="app-launch-progress-bar" :style="launchProgressStyle"></view>
|
||||
</view>
|
||||
<view v-if="launchOverlayRecoverable" class="app-launch-actions">
|
||||
<view class="app-launch-action secondary" @tap="handleLaunchRetry">
|
||||
<text class="app-launch-action-text">重新加载</text>
|
||||
</view>
|
||||
<view class="app-launch-action primary" @tap="handleLaunchContinue">
|
||||
<text class="app-launch-action-text">继续进入</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -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<GuideRenderPoi | null>(null)
|
||||
const currentTab = ref<GuideTopTab>(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<RoutePointOption | null>(null)
|
||||
const routeEndPoint = ref<RoutePointOption | null>(null)
|
||||
@@ -396,7 +408,8 @@ const homeSearchExpanded = ref(false)
|
||||
|
||||
let launchOverlayFallbackTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let launchOverlayFadeTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let launchProgressTimer: ReturnType<typeof setInterval> | null = null
|
||||
let launchOverlayHideDelayTimer: ReturnType<typeof setTimeout> | 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);
|
||||
|
||||
Reference in New Issue
Block a user