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('