- 在AppBootResourceGate组件中集成react-i18next实现资源加载文本的国际化 - 修改DesktopAnimal组件中的loading dots key以提高渲染性能 - 在DesktopControl组件中添加useRef和useEffect钩子管理定时器清理逻辑 - 将DesktopTitle组件重构为MessageBroadcast组件并增强其响应式设计 - 更新DesktopSupportModal组件中的客户服务文本为国际化格式 - 在AuthSession模块中实现本地存储数据清理时保留关键偏好设置 - 调整多个游戏组件的样式类以改进移动端适配效果 - 移除未使用的桌面提取功能相关代码文件 - 更新GitNexus索引统计数据反映最新的代码变更
163 lines
5.9 KiB
TypeScript
163 lines
5.9 KiB
TypeScript
import { type PropsWithChildren, useEffect, useMemo, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
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 }) {
|
|
const { t } = useTranslation()
|
|
|
|
return (
|
|
<div
|
|
role="status"
|
|
aria-live="polite"
|
|
aria-label="Loading"
|
|
className="fixed inset-0 z-50 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)]">
|
|
{t('commonUi.boot.loading')}
|
|
</div>
|
|
<div className="text-design-14 text-[rgba(181,242,247,0.72)]">
|
|
{t('commonUi.boot.syncing')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function AppBootResourceGate({ children }: PropsWithChildren) {
|
|
const { isReady, progress } = useBootResourceLoader()
|
|
|
|
if (!isReady) {
|
|
return <AppLoadingOverlay progress={progress} />
|
|
}
|
|
|
|
return children
|
|
}
|