This commit is contained in:
@@ -69,6 +69,7 @@ const copyStaticFile = (fileName) => {
|
|||||||
|
|
||||||
copyStaticSubtree('nav-assets')
|
copyStaticSubtree('nav-assets')
|
||||||
copyStaticSubtree('guide-data')
|
copyStaticSubtree('guide-data')
|
||||||
|
copyStaticSubtree('guide')
|
||||||
copyStaticSubtree('icons')
|
copyStaticSubtree('icons')
|
||||||
// 讲解业务单元封面由 H5 运行时按 /static/explain/... 动态加载。
|
// 讲解业务单元封面由 H5 运行时按 /static/explain/... 动态加载。
|
||||||
copyStaticSubtree('explain')
|
copyStaticSubtree('explain')
|
||||||
|
|||||||
@@ -300,6 +300,8 @@ const emit = defineEmits<{
|
|||||||
targetFocus: [result: TargetPoiFocusResult]
|
targetFocus: [result: TargetPoiFocusResult]
|
||||||
autoSwitch: [event: { from: 'overview' | 'floor'; to: 'overview' | 'floor'; trigger: 'zoom-in' | 'zoom-out'; distance: number }]
|
autoSwitch: [event: { from: 'overview' | 'floor'; to: 'overview' | 'floor'; trigger: 'zoom-in' | 'zoom-out'; distance: number }]
|
||||||
autoSwitchBlocked: [reason: { reason: 'loading-locked' | 'cooldown' | 'disabled' }]
|
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<ContainerRef>(null)
|
const containerRef = ref<ContainerRef>(null)
|
||||||
@@ -4726,6 +4728,11 @@ const init3DScene = async () => {
|
|||||||
|
|
||||||
setProgress(100, '馆内三维模型加载完成')
|
setProgress(100, '馆内三维模型加载完成')
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
|
emit('initialModelReady', {
|
||||||
|
view: activeView.value,
|
||||||
|
floorId: currentFloor.value,
|
||||||
|
elapsedMs: firstModelLoadStartedAt ? Math.round(getNow() - firstModelLoadStartedAt) : undefined
|
||||||
|
})
|
||||||
await syncRequestedIndoorView()
|
await syncRequestedIndoorView()
|
||||||
queueTargetFocus(pendingTargetFocus || props.targetFocus || null)
|
queueTargetFocus(pendingTargetFocus || props.targetFocus || null)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -4734,7 +4741,14 @@ const init3DScene = async () => {
|
|||||||
console.error('馆内 3D 模型资源加载失败:', error)
|
console.error('馆内 3D 模型资源加载失败:', error)
|
||||||
loadError.value = true
|
loadError.value = true
|
||||||
isLoading.value = false
|
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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@
|
|||||||
@selection-clear="handleSelectionClear"
|
@selection-clear="handleSelectionClear"
|
||||||
@target-focus="handleTargetFocus"
|
@target-focus="handleTargetFocus"
|
||||||
@auto-switch="handleAutoSwitch"
|
@auto-switch="handleAutoSwitch"
|
||||||
|
@initial-model-ready="handleInitialModelReady"
|
||||||
|
@initial-model-failed="handleInitialModelFailed"
|
||||||
/>
|
/>
|
||||||
<!-- #endif -->
|
<!-- #endif -->
|
||||||
<!-- #ifndef H5 -->
|
<!-- #ifndef H5 -->
|
||||||
@@ -401,6 +403,8 @@ const emit = defineEmits<{
|
|||||||
selectionClear: []
|
selectionClear: []
|
||||||
targetFocus: [result: TargetPoiFocusResult]
|
targetFocus: [result: TargetPoiFocusResult]
|
||||||
autoSwitch: [event: { from: 'overview' | 'floor'; to: 'overview' | 'floor'; trigger: string; distance: number }]
|
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 }]
|
mapTap: [location: { latitude: number; longitude: number }]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -706,6 +710,14 @@ const handleAutoSwitch = (event: { from: 'overview' | 'floor'; to: 'overview' |
|
|||||||
emit('autoSwitch', event)
|
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 handleMapTap = (location: { latitude?: number; longitude?: number }) => {
|
||||||
const { latitude, longitude } = location
|
const { latitude, longitude } = location
|
||||||
if (latitude !== undefined && longitude !== undefined) {
|
if (latitude !== undefined && longitude !== undefined) {
|
||||||
|
|||||||
@@ -52,6 +52,8 @@
|
|||||||
@poi-click="handleGuidePoiClick"
|
@poi-click="handleGuidePoiClick"
|
||||||
@selection-clear="handleGuideSelectionClear"
|
@selection-clear="handleGuideSelectionClear"
|
||||||
@auto-switch="handleAutoSwitch"
|
@auto-switch="handleAutoSwitch"
|
||||||
|
@initial-model-ready="handleInitialModelReady"
|
||||||
|
@initial-model-failed="handleInitialModelFailed"
|
||||||
@tool-click="handleIndoorToolClick"
|
@tool-click="handleIndoorToolClick"
|
||||||
@map-tap="handleMapTapForOutdoorNav"
|
@map-tap="handleMapTapForOutdoorNav"
|
||||||
>
|
>
|
||||||
@@ -217,6 +219,32 @@
|
|||||||
@back="handleExplainBack"
|
@back="handleExplainBack"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
v-if="launchOverlayVisible"
|
||||||
|
class="app-launch-overlay"
|
||||||
|
:class="{ leaving: launchOverlayLeaving }"
|
||||||
|
>
|
||||||
|
<view class="app-launch-stage">
|
||||||
|
<image
|
||||||
|
class="app-launch-image"
|
||||||
|
src="/static/guide/app-launch-loading.webp"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<view class="app-launch-shade"></view>
|
||||||
|
<view class="app-launch-brand">
|
||||||
|
<text class="app-launch-kicker">探索万物生息</text>
|
||||||
|
<text class="app-launch-title">深圳自然博物馆</text>
|
||||||
|
<text class="app-launch-subtitle">Shenzhen Natural History Museum</text>
|
||||||
|
</view>
|
||||||
|
<view class="app-launch-status">
|
||||||
|
<text class="app-launch-loading-text">正在进入自然导览</text>
|
||||||
|
<view class="app-launch-progress">
|
||||||
|
<view class="app-launch-progress-bar" :style="launchProgressStyle"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</GuidePageFrame>
|
</GuidePageFrame>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -300,6 +328,14 @@ const topTabFromHash = () => {
|
|||||||
return isGuideTopTab(tab) ? tab : null
|
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'
|
const initialTopTab = (): GuideTopTab => topTabFromHash() || 'guide'
|
||||||
|
|
||||||
// 3D 模式状态
|
// 3D 模式状态
|
||||||
@@ -316,6 +352,10 @@ const selectedGuidePoi = ref<GuideRenderPoi | null>(null)
|
|||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
const currentTab = ref<GuideTopTab>(initialTopTab())
|
const currentTab = ref<GuideTopTab>(initialTopTab())
|
||||||
|
const launchOverlayVisible = ref(shouldShowLaunchOverlay())
|
||||||
|
const launchOverlayLeaving = ref(false)
|
||||||
|
const launchProgress = ref(18)
|
||||||
|
const launchInitialModelSettled = ref(false)
|
||||||
const showRoutePlanner = ref(false)
|
const showRoutePlanner = ref(false)
|
||||||
const routeStartPoint = ref<RoutePointOption | null>(null)
|
const routeStartPoint = ref<RoutePointOption | null>(null)
|
||||||
const routeEndPoint = ref<RoutePointOption | null>(null)
|
const routeEndPoint = ref<RoutePointOption | null>(null)
|
||||||
@@ -335,6 +375,84 @@ const guideMapShellRef = ref<{
|
|||||||
clearRoute?: () => void
|
clearRoute?: () => void
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
|
let launchOverlayFallbackTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let launchOverlayFadeTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let launchProgressTimer: ReturnType<typeof setInterval> | 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 isOutdoorGuidePanelEnabled = false
|
||||||
const showOutdoorNavPanel = ref(false)
|
const showOutdoorNavPanel = ref(false)
|
||||||
@@ -550,6 +668,7 @@ const syncTopTabFromHash = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
startLaunchOverlay()
|
||||||
showIndoorHint()
|
showIndoorHint()
|
||||||
|
|
||||||
if (typeof window === 'undefined') return
|
if (typeof window === 'undefined') return
|
||||||
@@ -559,6 +678,8 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
clearLaunchTimers()
|
||||||
|
|
||||||
if (indoorGestureHintTimer) {
|
if (indoorGestureHintTimer) {
|
||||||
clearTimeout(indoorGestureHintTimer)
|
clearTimeout(indoorGestureHintTimer)
|
||||||
indoorGestureHintTimer = null
|
indoorGestureHintTimer = null
|
||||||
@@ -713,6 +834,14 @@ const handleAutoSwitch = (event: { from: 'overview' | 'floor'; to: 'overview' |
|
|||||||
showIndoorHint(event.to === 'floor' ? '已进入单层视图,可点选标记查看位置' : '已返回建筑外观', 3600)
|
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) => {
|
const handleIndoorToolClick = (tool: string) => {
|
||||||
if (!is3DMode.value) return
|
if (!is3DMode.value) return
|
||||||
|
|
||||||
@@ -1312,6 +1441,157 @@ const handleExplainBack = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
.app-launch-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 5000;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #e9e8dc;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.36s ease, transform 0.36s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-launch-overlay.leaving {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(1.015);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-launch-stage {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 430px;
|
||||||
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
min-height: 560px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f5f5ed;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-launch-image {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-launch-shade {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(245, 245, 237, 0.92) 0%, rgba(245, 245, 237, 0.62) 24%, rgba(10, 12, 9, 0.08) 52%, rgba(0, 0, 0, 0.56) 100%),
|
||||||
|
linear-gradient(180deg, rgba(224, 225, 0, 0.28) 0, rgba(224, 225, 0, 0) 138px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-launch-brand {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(env(safe-area-inset-top) + 58px);
|
||||||
|
left: 24px;
|
||||||
|
right: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-launch-kicker {
|
||||||
|
width: fit-content;
|
||||||
|
padding: 4px 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #000000;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--museum-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-launch-title {
|
||||||
|
font-size: 31px;
|
||||||
|
line-height: 39px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0;
|
||||||
|
color: #000000;
|
||||||
|
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.46);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-launch-subtitle {
|
||||||
|
max-width: 300px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 19px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #424754;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-launch-status {
|
||||||
|
position: absolute;
|
||||||
|
left: 24px;
|
||||||
|
right: 24px;
|
||||||
|
bottom: calc(env(safe-area-inset-bottom) + 56px);
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: rgba(0, 0, 0, 0.88);
|
||||||
|
border: 1px solid rgba(224, 225, 0, 0.34);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-launch-loading-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 21px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-launch-progress {
|
||||||
|
height: 4px;
|
||||||
|
margin-top: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-launch-progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
width: 18%;
|
||||||
|
background: var(--museum-accent);
|
||||||
|
transition: width 0.22s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 360px) {
|
||||||
|
.app-launch-brand {
|
||||||
|
top: calc(env(safe-area-inset-top) + 48px);
|
||||||
|
left: 20px;
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-launch-title {
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-launch-subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-launch-status {
|
||||||
|
left: 20px;
|
||||||
|
right: 20px;
|
||||||
|
bottom: calc(env(safe-area-inset-bottom) + 42px);
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 680px) {
|
||||||
|
.app-launch-brand {
|
||||||
|
top: calc(env(safe-area-inset-top) + 38px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-launch-status {
|
||||||
|
bottom: calc(env(safe-area-inset-bottom) + 32px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.explain-page {
|
.explain-page {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
BIN
static/guide/app-launch-loading.webp
Normal file
BIN
static/guide/app-launch-loading.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
Reference in New Issue
Block a user