diff --git a/design-qa.md b/design-qa.md
new file mode 100644
index 0000000..ec1fdab
--- /dev/null
+++ b/design-qa.md
@@ -0,0 +1,40 @@
+# 加载页设计 QA
+
+- source visual truth path: `C:\Users\ADMINI~1\AppData\Local\Temp\codex-clipboard-6d038678-2d48-4ee0-b4fc-152d414e7184.png`
+- implementation screenshot path: `C:\Users\Administrator\AppData\Local\Temp\museum-launch-390x844.png`
+- comparison evidence path: `C:\Users\Administrator\AppData\Local\Temp\museum-launch-comparison.png`
+- viewport: `360x800`、`390x844`、`430x932`
+- state: 首页首次进入,加载层可见,加载进度进行中
+
+## Full-view Comparison Evidence
+
+白色全屏背景中仅保留彩色 Logo、英文馆名、中文馆名和原生进度动效。实现层级、内容顺序和居中关系与参考图一致,没有继续使用全屏背景图、蒙层、口号、状态卡或加载说明文案。
+
+## Focused Region Comparison Evidence
+
+对 Logo 与馆名区域进行了聚焦对比。Logo 使用参考图中的真实彩色图形资产,馆名使用页面文字以减少图片体积。360px、390px、430px 三档视口中 Logo 均完整加载,中英文馆名均未换行或溢出,进度动效可见。
+
+## Fidelity Surfaces
+
+- Fonts and typography: 英文使用无衬线字体栈并保持单行;中文使用宋体字体栈,字重和留白接近参考图。
+- Spacing and layout rhythm: Logo、英文、中文和进度条垂直居中排列,三档移动视口无裁切或重叠。
+- Colors and visual tokens: 白色背景、黑色馆名和博物馆黄绿色加载色符合现有品牌色。
+- Image quality and asset fidelity: Logo 来自参考图真实图形区域,WebP 尺寸为 `738x106`,约 12 KB;未使用 CSS 绘图、SVG 或占位图替代。
+- Copy and content: 英文馆名为 `SHENZHEN NATURAL HISTORY MUSEUM`,中文馆名为 `深圳自然博物馆`。
+
+## Findings
+
+无 P0、P1 或 P2 问题。
+
+## Patches Made
+
+- 移除旧的两张全屏加载图、蒙层、口号、状态卡、加载说明和恢复按钮。
+- 增加彩色 Logo 图形资产,馆名改为页面文字。
+- 保留现有加载进度、最短展示时间、模型完成、超时和淡出逻辑。
+- 修复本地验证环境中的旧 Vite 进程,确认新静态资源可正常加载。
+
+## Follow-up Polish
+
+无阻塞项。
+
+final result: passed
diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue
index 3e18a2e..7535acc 100644
--- a/src/pages/index/index.vue
+++ b/src/pages/index/index.vue
@@ -263,38 +263,24 @@
:class="{ leaving: launchOverlayLeaving }"
>
-
-
-
-
- 探索万物生息
- 深圳自然博物馆
- Shenzhen Natural History Museum
-
-
- {{ launchLoadingText }}
-
-
-
-
-
- 重新加载
-
-
- 继续进入
-
-
+
+
+ SHENZHEN NATURAL HISTORY MUSEUM
+ 深圳自然博物馆
+
@@ -381,7 +367,6 @@ const topTabFromHash = () => {
const launchOverlayMinDisplayMs = 700
const launchOverlayGuideTimeoutMs = 12000
const launchOverlayNonGuideTimeoutMs = 900
-const launchOverlayDefaultText = '正在进入智能导览'
const shouldShowLaunchOverlay = () => {
if (typeof window === 'undefined') return false
@@ -407,11 +392,7 @@ const currentTab = ref(initialTopTab())
const launchOverlayVisible = ref(shouldShowLaunchOverlay())
const launchOverlayLeaving = ref(false)
const launchProgress = ref(8)
-const launchLoadingText = ref(launchOverlayDefaultText)
const launchInitialModelSettled = ref(false)
-const launchOverlayRecoverable = ref(false)
-const launchImageLoaded = ref(false)
-const launchImageFailed = ref(false)
const showRoutePlanner = ref(false)
const routeStartPoint = ref(null)
const routeEndPoint = ref(null)
@@ -448,10 +429,6 @@ let launchOverlayFadeTimer: ReturnType | null = null
let launchOverlayHideDelayTimer: ReturnType | null = null
let launchOverlayStartedAt = 0
-const launchProgressStyle = computed(() => ({
- width: `${launchProgress.value}%`
-}))
-
const clearLaunchTimers = () => {
if (launchOverlayFallbackTimer) {
clearTimeout(launchOverlayFallbackTimer)
@@ -515,8 +492,6 @@ const continueLaunchWithoutInitialModel = (reason: string, event?: { view: Guide
})
launchInitialModelSettled.value = true
- launchOverlayRecoverable.value = false
- launchLoadingText.value = launchOverlayDefaultText
launchProgress.value = Math.max(launchProgress.value, 96)
is3DMode.value = false
guideOutdoorState.value = 'home'
@@ -531,10 +506,8 @@ const startLaunchOverlay = () => {
clearLaunchTimers()
launchInitialModelSettled.value = false
- launchOverlayRecoverable.value = false
launchOverlayStartedAt = Date.now()
launchProgress.value = shouldWaitForLaunchModel() ? 8 : 68
- launchLoadingText.value = launchOverlayDefaultText
const timeoutMs = shouldWaitForLaunchModel()
? launchOverlayGuideTimeoutMs
@@ -552,52 +525,23 @@ const startLaunchOverlay = () => {
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 = launchOverlayDefaultText
}
const settleLaunchOverlayByModel = (status: 'ready' | 'failed', event?: { view: GuideIndoorView; floorId?: string; message?: string; elapsedMs?: number }) => {
launchInitialModelSettled.value = true
- launchOverlayRecoverable.value = status === 'failed'
if (status === 'failed') {
continueLaunchWithoutInitialModel('failed', event)
return
} else {
launchProgress.value = 100
- launchLoadingText.value = launchOverlayDefaultText
console.log('首页前置加载页收到初始模型加载完成:', event)
}
hideLaunchOverlay()
}
-const handleLaunchRetry = () => {
- if (typeof window !== 'undefined') {
- window.location.reload()
- }
-}
-
-const handleLaunchImageLoad = () => {
- launchImageLoaded.value = true
- launchImageFailed.value = false
-}
-
-const handleLaunchImageError = () => {
- launchImageLoaded.value = false
- launchImageFailed.value = true
-}
-
-const handleLaunchContinue = () => {
- launchOverlayRecoverable.value = false
- if (shouldWaitForLaunchModel() && !launchInitialModelSettled.value) {
- continueLaunchWithoutInitialModel('manual')
- return
- }
- hideLaunchOverlay()
-}
-
const showArrivalPanel = ref(false)
const showArrivalPanelCollapsed = ref(false)
const arrivalPanelRef = ref | null>(null)
@@ -1582,7 +1526,7 @@ const handleExplainBack = () => {
display: flex;
justify-content: center;
overflow: hidden;
- background: #e9e8dc;
+ background: #ffffff;
opacity: 1;
transition: opacity 0.36s ease, transform 0.36s ease;
}
@@ -1594,194 +1538,82 @@ const handleExplainBack = () => {
}
.app-launch-stage {
- position: relative;
width: 100%;
max-width: 430px;
height: 100vh;
height: 100dvh;
- min-height: 560px;
- overflow: hidden;
- background:
- radial-gradient(circle at 50% 20%, rgba(224, 225, 0, 0.24), rgba(224, 225, 0, 0) 34%),
- linear-gradient(180deg, #f5f5ed 0%, #e1e5d9 42%, #151713 100%);
- box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.04);
-}
-
-.app-launch-image {
- position: absolute;
- inset: 0;
- z-index: 0;
- width: 100%;
- height: 100%;
- transform-origin: center;
-}
-
-.app-launch-image-lqip {
- filter: blur(10px);
- transform: scale(1.08);
-}
-
-.app-launch-image-full {
- opacity: 0;
- transform: scale(1.01);
- transition: opacity 0.24s ease;
-}
-
-.app-launch-image-full.loaded {
- opacity: 1;
-}
-
-.app-launch-shade {
- position: absolute;
- inset: 0;
- z-index: 1;
- 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;
- z-index: 2;
- 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;
- z-index: 2;
- 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;
-}
-
-.app-launch-actions {
- margin-top: 14px;
- display: flex;
- gap: 10px;
-}
-
-.app-launch-action {
- height: 36px;
- flex: 1;
+ min-height: 480px;
display: flex;
align-items: center;
justify-content: center;
+ overflow: hidden;
+ background: #ffffff;
+}
+
+.app-launch-content {
+ width: 100%;
+ padding: 0 28px;
+ display: flex;
+ align-items: center;
+ flex-direction: column;
box-sizing: border-box;
- border-radius: 8px;
+ transform: translateY(-3vh);
}
-.app-launch-action.secondary {
- background: rgba(255, 255, 255, 0.12);
- border: 1px solid rgba(255, 255, 255, 0.38);
+.app-launch-logo {
+ width: min(84vw, 350px);
+ height: auto;
}
-.app-launch-action.primary {
- background: var(--museum-accent);
- border: 1px solid var(--museum-accent);
-}
-
-.app-launch-action-text {
+.app-launch-name-en {
+ margin-top: 20px;
+ white-space: nowrap;
+ font-family: 'HarmonyOS Sans SC', 'HarmonyOS Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
font-size: 13px;
line-height: 18px;
- font-weight: 700;
- color: #ffffff;
+ font-weight: 500;
+ letter-spacing: 0;
+ color: #262421;
}
-.app-launch-action.primary .app-launch-action-text {
- color: #151713;
+.app-launch-name-cn {
+ margin-top: 8px;
+ white-space: nowrap;
+ font-family: 'Songti SC', STSong, SimSun, serif;
+ font-size: 36px;
+ line-height: 48px;
+ font-weight: 500;
+ letter-spacing: 0;
+ color: #000000;
+}
+
+.app-launch-progress {
+ width: 112px;
+ margin-top: 32px;
}
@media (max-width: 360px) {
- .app-launch-brand {
- top: calc(env(safe-area-inset-top) + 48px);
- left: 20px;
- right: 20px;
+ .app-launch-content {
+ padding: 0 24px;
}
- .app-launch-title {
- font-size: 28px;
- line-height: 36px;
+ .app-launch-logo {
+ width: calc(100vw - 52px);
}
- .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;
+ .app-launch-name-cn {
+ font-size: 32px;
+ line-height: 43px;
}
}
@media (max-height: 680px) {
- .app-launch-brand {
- top: calc(env(safe-area-inset-top) + 38px);
+ .app-launch-content {
+ transform: translateY(-1vh);
}
- .app-launch-status {
- bottom: calc(env(safe-area-inset-bottom) + 32px);
+ .app-launch-progress {
+ margin-top: 26px;
}
}
diff --git a/static/guide/museum-brand-icons.webp b/static/guide/museum-brand-icons.webp
new file mode 100644
index 0000000..32250a4
Binary files /dev/null and b/static/guide/museum-brand-icons.webp differ
diff --git a/tests/unit/IndexLaunchOverlay.spec.ts b/tests/unit/IndexLaunchOverlay.spec.ts
new file mode 100644
index 0000000..eb7ee84
--- /dev/null
+++ b/tests/unit/IndexLaunchOverlay.spec.ts
@@ -0,0 +1,26 @@
+import { readFileSync } from 'node:fs'
+import { fileURLToPath, URL } from 'node:url'
+import { describe, expect, it } from 'vitest'
+
+const indexPageSource = readFileSync(
+ fileURLToPath(new URL('../../src/pages/index/index.vue', import.meta.url)),
+ 'utf8'
+)
+
+const launchOverlaySource = indexPageSource.slice(
+ indexPageSource.indexOf('v-if="launchOverlayVisible"'),
+ indexPageSource.indexOf('')
+)
+
+describe('首页精简加载页', () => {
+ it('仅展示图形标识、文字馆名和原生加载动效', () => {
+ expect(launchOverlaySource).toContain('/static/guide/museum-brand-icons.webp')
+ expect(launchOverlaySource).toContain('SHENZHEN NATURAL HISTORY MUSEUM')
+ expect(launchOverlaySource).toContain('深圳自然博物馆')
+ expect(launchOverlaySource).toContain('