feat(app): 添加应用启动资源预加载功能
- 实现了 AppBootResourceGate 组件用于管理应用启动时的资源加载 - 集成了图片和字体资源的预加载机制 - 添加了启动时的加载进度指示器和动画效果 - 创建了 DataLoadingIndicator 组件用于显示数据加载状态 - 在多个组件中替换原有的加载提示为新的 DataLoadingIndicator - 更新了样式文件以支持新的加载动画和视觉效果
This commit is contained in:
159
src/components/app-boot-resource-gate.tsx
Normal file
159
src/components/app-boot-resource-gate.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { type PropsWithChildren, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
const bootImageModules = import.meta.glob(
|
||||
'../assets/**/*.{jpg,jpeg,png,svg,webp}',
|
||||
{
|
||||
eager: true,
|
||||
import: 'default',
|
||||
},
|
||||
) as Record<string, string>
|
||||
|
||||
const BOOT_MIN_VISIBLE_MS = 900
|
||||
const particleLayout = Array.from({ length: 36 }, (_, index) => ({
|
||||
delayMs: (index % 9) * 180,
|
||||
durationMs: 3200 + (index % 7) * 260,
|
||||
left: `${(index * 17 + 9) % 100}%`,
|
||||
size: 2 + (index % 4),
|
||||
top: `${(index * 29 + 13) % 100}%`,
|
||||
}))
|
||||
|
||||
function preloadImage(url: string) {
|
||||
return new Promise<void>((resolve) => {
|
||||
const image = new Image()
|
||||
|
||||
image.onload = () => resolve()
|
||||
image.onerror = () => resolve()
|
||||
image.src = url
|
||||
})
|
||||
}
|
||||
|
||||
function waitForFonts() {
|
||||
if (typeof document === 'undefined' || !('fonts' in document)) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return document.fonts.ready.then(() => undefined)
|
||||
}
|
||||
|
||||
function useBootResourceLoader() {
|
||||
const imageUrls = useMemo(
|
||||
() => Array.from(new Set(Object.values(bootImageModules))).filter(Boolean),
|
||||
[],
|
||||
)
|
||||
const [progress, setProgress] = useState(0)
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
const startedAt = performance.now()
|
||||
const totalSteps = imageUrls.length + 1
|
||||
let completedSteps = 0
|
||||
|
||||
const syncProgress = () => {
|
||||
completedSteps += 1
|
||||
if (!cancelled) {
|
||||
setProgress(
|
||||
Math.min(100, Math.round((completedSteps / totalSteps) * 100)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const waitForMinimumDuration = async () => {
|
||||
const elapsedMs = performance.now() - startedAt
|
||||
const remainingMs = Math.max(0, BOOT_MIN_VISIBLE_MS - elapsedMs)
|
||||
|
||||
if (remainingMs <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
await new Promise((resolve) => window.setTimeout(resolve, remainingMs))
|
||||
}
|
||||
|
||||
void Promise.all([
|
||||
waitForFonts().finally(syncProgress),
|
||||
...imageUrls.map((url) => preloadImage(url).finally(syncProgress)),
|
||||
])
|
||||
.then(waitForMinimumDuration)
|
||||
.then(() => {
|
||||
if (!cancelled) {
|
||||
setProgress(100)
|
||||
setIsReady(true)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [imageUrls])
|
||||
|
||||
return { isReady, progress }
|
||||
}
|
||||
|
||||
function AppLoadingOverlay({ progress }: { progress: number }) {
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label="Loading"
|
||||
className="fixed inset-0 z-[9999] flex items-center justify-center overflow-hidden bg-[#020913] text-[#D5FBFF]"
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_38%,rgba(35,221,255,0.18),transparent_30%),linear-gradient(180deg,#061827_0%,#020913_54%,#01050C_100%)]" />
|
||||
<div className="boot-grid absolute inset-0 opacity-50" />
|
||||
<div className="boot-scanline absolute inset-0 opacity-35" />
|
||||
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
{particleLayout.map((particle) => (
|
||||
<span
|
||||
key={`${particle.left}-${particle.top}`}
|
||||
className="boot-particle absolute rounded-full bg-[#71FFF7] shadow-[0_0_12px_rgba(113,255,247,0.88)]"
|
||||
style={{
|
||||
animationDelay: `${particle.delayMs}ms`,
|
||||
animationDuration: `${particle.durationMs}ms`,
|
||||
height: `calc(var(--design-unit) * ${particle.size})`,
|
||||
left: particle.left,
|
||||
top: particle.top,
|
||||
width: `calc(var(--design-unit) * ${particle.size})`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="relative flex w-[min(calc(var(--design-unit)*720),86vw)] flex-col items-center gap-design-24 px-design-24">
|
||||
<div className="relative flex h-design-190 w-design-190 min-h-32 min-w-32 items-center justify-center md:h-design-260 md:w-design-260">
|
||||
<div className="boot-ring absolute inset-0 rounded-full border border-[rgba(105,255,250,0.38)]" />
|
||||
<div className="boot-ring boot-ring-delay absolute inset-[calc(var(--design-unit)*18)] rounded-full border border-[rgba(255,224,117,0.34)] md:inset-[calc(var(--design-unit)*24)]" />
|
||||
<div className="absolute h-[64%] w-[64%] rounded-full border border-[rgba(113,255,247,0.26)] bg-[radial-gradient(circle,rgba(18,95,113,0.68),rgba(2,13,22,0.96))] shadow-[inset_0_0_calc(var(--design-unit)*32)_rgba(113,255,247,0.18),0_0_calc(var(--design-unit)*52)_rgba(65,232,255,0.22)]" />
|
||||
<span className="relative text-design-34 font-bold leading-none text-[#FEE889] [text-shadow:0_0_calc(var(--design-unit)*16)_rgba(254,232,137,0.46)] md:text-design-50 md:[text-shadow:0_0_calc(var(--design-unit)*22)_rgba(254,232,137,0.56)]">
|
||||
{progress}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full overflow-hidden rounded-[calc(var(--design-unit)*10)] border border-[rgba(113,255,247,0.38)] bg-[rgba(4,20,31,0.82)] p-design-2 shadow-[0_0_calc(var(--design-unit)*22)_rgba(65,232,255,0.16),inset_0_0_calc(var(--design-unit)*10)_rgba(113,255,247,0.1)]">
|
||||
<div
|
||||
className="h-design-10 rounded-[calc(var(--design-unit)*8)] bg-[linear-gradient(90deg,#34E7FF,#7BFFF6,#FEE889)] shadow-[0_0_calc(var(--design-unit)*16)_rgba(113,255,247,0.58)] transition-[width] duration-300 ease-out"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-design-8 text-center">
|
||||
<div className="text-design-24 font-bold text-[#E8FFFF] [text-shadow:0_0_calc(var(--design-unit)*14)_rgba(113,255,247,0.38)]">
|
||||
资源加载中
|
||||
</div>
|
||||
<div className="text-design-14 text-[rgba(181,242,247,0.72)]">
|
||||
正在同步字花图鉴与游戏界面
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function AppBootResourceGate({ children }: PropsWithChildren) {
|
||||
const { isReady, progress } = useBootResourceLoader()
|
||||
|
||||
if (!isReady) {
|
||||
return <AppLoadingOverlay progress={progress} />
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
Reference in New Issue
Block a user