|
|
@@ -1,136 +1,168 @@
|
|
|
-import React, { useEffect, useLayoutEffect, useState, useMemo, useRef, useCallback } from 'react';
|
|
|
-import history from '@/utils/history';// 如果用 react-router 跳转
|
|
|
-import styles from './index.module.scss';
|
|
|
-import A0_home_1 from '@/assets/img/A0_home_1.jpg';
|
|
|
-import A0_home_2 from '@/assets/img/A0_home_2.jpg';
|
|
|
-import A0_home_3 from '@/assets/img/A0_home_3.jpg';
|
|
|
-import homeLogo from '@/assets/img/A0_home_logo.png'; // 推荐直接 import 图片
|
|
|
-import { baseOssUrl } from '@/utils/http';
|
|
|
-
|
|
|
-/** 以「页面上真实 img」为准:load + decode + 双 rAF,避免与独立 new Image() 不同步 */
|
|
|
-function useDomImageReady(
|
|
|
- ref: React.RefObject<HTMLImageElement | null>,
|
|
|
- onReady: () => void,
|
|
|
-) {
|
|
|
- const onReadyRef = useRef(onReady);
|
|
|
- onReadyRef.current = onReady;
|
|
|
- const firedRef = useRef(false);
|
|
|
+import React, { useEffect, useLayoutEffect, useState, useMemo, useRef, useCallback } from 'react'
|
|
|
+import history from '@/utils/history'
|
|
|
+import styles from './index.module.scss'
|
|
|
|
|
|
- useLayoutEffect(() => {
|
|
|
- const el = ref.current;
|
|
|
- if (!el) return;
|
|
|
+import A0_home_1 from '@/assets/img/A0_home_1.jpg'
|
|
|
+import A0_home_2 from '@/assets/img/A0_home_2.jpg'
|
|
|
+import A0_home_3 from '@/assets/img/A0_home_3.jpg'
|
|
|
+import homeLogo from '@/assets/img/A0_home_logo.png'
|
|
|
+import { baseOssUrl } from '@/utils/http'
|
|
|
|
|
|
- const fire = () => {
|
|
|
- if (firedRef.current) return;
|
|
|
- firedRef.current = true;
|
|
|
- void (async () => {
|
|
|
- try {
|
|
|
- if (typeof el.decode === 'function') await el.decode();
|
|
|
- } catch {
|
|
|
- /* ignore */
|
|
|
- }
|
|
|
- requestAnimationFrame(() => {
|
|
|
- requestAnimationFrame(() => {
|
|
|
- onReadyRef.current();
|
|
|
- });
|
|
|
- });
|
|
|
- })();
|
|
|
- };
|
|
|
+const VIDEO_URL = `${baseOssUrl}myData/media/siji.mp4`
|
|
|
+const MIN_STAY_MS = 3000 // 至少停留3秒
|
|
|
+const EARLY_NAVIGATE_PROGRESS = 50 // 实际下载到50%即可尝试跳转(后台继续下载完整视频)
|
|
|
|
|
|
- if (el.complete && el.naturalWidth > 0) {
|
|
|
- fire();
|
|
|
- } else {
|
|
|
- el.addEventListener('load', fire, { once: true });
|
|
|
- el.addEventListener('error', fire, { once: true });
|
|
|
- }
|
|
|
+function A0base() {
|
|
|
+ const [currentIndex, setCurrentIndex] = useState(0)
|
|
|
+ const [progress, setProgress] = useState(0)
|
|
|
+ const [assetsReady, setAssetsReady] = useState(false)
|
|
|
|
|
|
- return () => {
|
|
|
- el.removeEventListener('load', fire);
|
|
|
- el.removeEventListener('error', fire);
|
|
|
- };
|
|
|
- }, [ref]);
|
|
|
-}
|
|
|
+ const images = useMemo(() => [A0_home_1, A0_home_2, A0_home_3], [])
|
|
|
|
|
|
-/** 与首页背景视频同源,直接预拉取 mp4 以便快速命中缓存 */
|
|
|
-function getIntroVideoMp4Url(): string {
|
|
|
- return `${baseOssUrl}myData/media/siji.mp4`;
|
|
|
-}
|
|
|
+ const slide0Ref = useRef<HTMLImageElement>(null)
|
|
|
+ const slide1Ref = useRef<HTMLImageElement>(null)
|
|
|
+ const slide2Ref = useRef<HTMLImageElement>(null)
|
|
|
+ const logoRef = useRef<HTMLImageElement>(null)
|
|
|
|
|
|
-function A0base() {
|
|
|
- const [currentIndex, setCurrentIndex] = useState(0);
|
|
|
- const [progress, setProgress] = useState(0); // 四图就绪后:0 ~ 100 为视频预加载字节进度
|
|
|
- const [assetsReady, setAssetsReady] = useState(false);
|
|
|
- const images = useMemo(() => [A0_home_1, A0_home_2, A0_home_3], []);
|
|
|
-
|
|
|
- const slide0Ref = useRef<HTMLImageElement>(null);
|
|
|
- const slide1Ref = useRef<HTMLImageElement>(null);
|
|
|
- const slide2Ref = useRef<HTMLImageElement>(null);
|
|
|
- const logoRef = useRef<HTMLImageElement>(null);
|
|
|
- const pendingRef = useRef(4);
|
|
|
- const hasNavigatedRef = useRef(false);
|
|
|
+ const pendingRef = useRef(4)
|
|
|
+ const hasNavigatedRef = useRef(false)
|
|
|
|
|
|
const markOneReady = useCallback(() => {
|
|
|
- if (pendingRef.current <= 0) return;
|
|
|
- pendingRef.current -= 1;
|
|
|
- if (pendingRef.current <= 0) setAssetsReady(true);
|
|
|
- }, []);
|
|
|
+ pendingRef.current -= 1
|
|
|
+ if (pendingRef.current <= 0) setAssetsReady(true)
|
|
|
+ }, [])
|
|
|
|
|
|
- useDomImageReady(slide0Ref, markOneReady);
|
|
|
- useDomImageReady(slide1Ref, markOneReady);
|
|
|
- useDomImageReady(slide2Ref, markOneReady);
|
|
|
- useDomImageReady(logoRef, markOneReady);
|
|
|
+ // 图片加载
|
|
|
+ useDomImageReady(slide0Ref, markOneReady)
|
|
|
+ useDomImageReady(slide1Ref, markOneReady)
|
|
|
+ useDomImageReady(slide2Ref, markOneReady)
|
|
|
+ useDomImageReady(logoRef, markOneReady)
|
|
|
|
|
|
useEffect(() => {
|
|
|
- if (!assetsReady) return;
|
|
|
-
|
|
|
- const url = getIntroVideoMp4Url();
|
|
|
- const xhr = new XMLHttpRequest();
|
|
|
- xhr.open('GET', url, true);
|
|
|
- xhr.responseType = 'blob';
|
|
|
-
|
|
|
- xhr.onprogress = (e) => {
|
|
|
- if (e.lengthComputable && e.total > 0) {
|
|
|
- const nextProgress = Math.min(100, Math.round((e.loaded / e.total) * 100));
|
|
|
- setProgress(nextProgress);
|
|
|
- if (nextProgress >= 70 && !hasNavigatedRef.current) {
|
|
|
- hasNavigatedRef.current = true;
|
|
|
- history.push('/home');
|
|
|
- }
|
|
|
- }
|
|
|
- };
|
|
|
+ if (!assetsReady) return
|
|
|
+
|
|
|
+ const abortController = new AbortController()
|
|
|
+ let rafId: number | null = null
|
|
|
+ let timeoutId: number | null = null
|
|
|
+ let minStayReached = false
|
|
|
+ let reachedNavigateProgress = false
|
|
|
+ const startTime = performance.now()
|
|
|
+
|
|
|
+ const goHomeOnce = () => {
|
|
|
+ if (hasNavigatedRef.current) return
|
|
|
+ hasNavigatedRef.current = true
|
|
|
+ history.push('/home')
|
|
|
+ }
|
|
|
+
|
|
|
+ const tryGoHome = () => {
|
|
|
+ if (minStayReached && reachedNavigateProgress) goHomeOnce()
|
|
|
+ }
|
|
|
|
|
|
- xhr.onload = () => {
|
|
|
- setProgress(100);
|
|
|
- if (!hasNavigatedRef.current) {
|
|
|
- hasNavigatedRef.current = true;
|
|
|
- history.push('/home');
|
|
|
+ // 最小停留时间
|
|
|
+ timeoutId = window.setTimeout(() => {
|
|
|
+ minStayReached = true
|
|
|
+ tryGoHome()
|
|
|
+ }, MIN_STAY_MS)
|
|
|
+
|
|
|
+ // 3秒后平滑动画到100%
|
|
|
+ const animateTo100 = () => {
|
|
|
+ const elapsed = performance.now() - startTime
|
|
|
+ if (elapsed >= MIN_STAY_MS) {
|
|
|
+ setProgress(100)
|
|
|
+ return
|
|
|
}
|
|
|
- };
|
|
|
+ const next = Math.min(99, Math.round(60 + (elapsed / MIN_STAY_MS) * 40))
|
|
|
+ setProgress(next)
|
|
|
+ rafId = requestAnimationFrame(animateTo100)
|
|
|
+ }
|
|
|
+
|
|
|
+ const preloadFullVideo = async () => {
|
|
|
+ try {
|
|
|
+ const response = await fetch(VIDEO_URL, {
|
|
|
+ signal: abortController.signal,
|
|
|
+ cache: 'force-cache'
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!response.ok) throw new Error('Fetch failed')
|
|
|
+
|
|
|
+ const contentLength = response.headers.get('content-length')
|
|
|
+ const total = contentLength ? parseInt(contentLength, 10) : 0
|
|
|
+
|
|
|
+ if (!total) {
|
|
|
+ handleFallback()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const reader = response.body?.getReader()
|
|
|
+ if (!reader) throw new Error('No reader')
|
|
|
+
|
|
|
+ let loaded = 0
|
|
|
+
|
|
|
+ while (true) {
|
|
|
+ const { done, value } = await reader.read()
|
|
|
+ if (done) break
|
|
|
+
|
|
|
+ loaded += value.length
|
|
|
+ const realProgress = Math.floor((loaded / total) * 100)
|
|
|
+
|
|
|
+ // 显示进度策略:
|
|
|
+ // 前3秒内最多显示60%,之后显示真实下载进度
|
|
|
+ const displayProgress =
|
|
|
+ performance.now() - startTime < MIN_STAY_MS ? Math.min(realProgress, 60) : realProgress
|
|
|
|
|
|
- xhr.onerror = () => {
|
|
|
- setProgress(100);
|
|
|
- if (!hasNavigatedRef.current) {
|
|
|
- hasNavigatedRef.current = true;
|
|
|
- history.push('/home');
|
|
|
+ setProgress(displayProgress)
|
|
|
+
|
|
|
+ // 下载到50%即可尝试跳转(但后台继续完整下载)
|
|
|
+ if (realProgress >= EARLY_NAVIGATE_PROGRESS) {
|
|
|
+ reachedNavigateProgress = true
|
|
|
+ tryGoHome()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 完整下载完成
|
|
|
+ reachedNavigateProgress = true
|
|
|
+ const elapsed = performance.now() - startTime
|
|
|
+
|
|
|
+ if (elapsed < MIN_STAY_MS) {
|
|
|
+ setProgress(60)
|
|
|
+ rafId = requestAnimationFrame(animateTo100)
|
|
|
+ } else {
|
|
|
+ setProgress(100)
|
|
|
+ }
|
|
|
+
|
|
|
+ tryGoHome()
|
|
|
+ } catch (err: any) {
|
|
|
+ if (err.name !== 'AbortError') {
|
|
|
+ console.error('Video preload failed:', err)
|
|
|
+ }
|
|
|
+ setProgress(100)
|
|
|
+ reachedNavigateProgress = true
|
|
|
+ tryGoHome()
|
|
|
}
|
|
|
- };
|
|
|
+ }
|
|
|
|
|
|
- xhr.send();
|
|
|
+ const handleFallback = () => {
|
|
|
+ setProgress(60)
|
|
|
+ rafId = requestAnimationFrame(animateTo100)
|
|
|
+ reachedNavigateProgress = true
|
|
|
+ tryGoHome()
|
|
|
+ }
|
|
|
|
|
|
- // 不 abort:避免 Strict Mode / 快速重挂载导致重复请求、进度异常
|
|
|
- return () => {};
|
|
|
- }, [assetsReady]);
|
|
|
+ preloadFullVideo()
|
|
|
|
|
|
- // 根据进度切换图片(进度条在四图就绪后才会从 0 增长)
|
|
|
- useEffect(() => {
|
|
|
- if (progress >= 65) {
|
|
|
- setCurrentIndex(2);
|
|
|
- } else if (progress >= 30) {
|
|
|
- setCurrentIndex(1);
|
|
|
- } else {
|
|
|
- setCurrentIndex(0);
|
|
|
+ return () => {
|
|
|
+ abortController.abort()
|
|
|
+ if (timeoutId) clearTimeout(timeoutId)
|
|
|
+ if (rafId) cancelAnimationFrame(rafId)
|
|
|
}
|
|
|
- }, [progress]);
|
|
|
+ }, [assetsReady])
|
|
|
+
|
|
|
+ // 进度切换背景图
|
|
|
+ useEffect(() => {
|
|
|
+ if (progress >= 65) setCurrentIndex(2)
|
|
|
+ else if (progress >= 30) setCurrentIndex(1)
|
|
|
+ else setCurrentIndex(0)
|
|
|
+ }, [progress])
|
|
|
|
|
|
return (
|
|
|
<div className={styles.A0base}>
|
|
|
@@ -145,15 +177,46 @@ function A0base() {
|
|
|
))}
|
|
|
|
|
|
<div className={styles.homeLogo}>
|
|
|
- <img ref={logoRef} src={homeLogo} alt="homeLogo" />
|
|
|
- <div className={'process'}>
|
|
|
- {progress}%
|
|
|
- </div>
|
|
|
+ <img ref={logoRef} src={homeLogo} alt='home logo' />
|
|
|
+ <div className='process'>{Math.floor(progress)}%</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- );
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
-const MemoA0base = React.memo(A0base);
|
|
|
+function useDomImageReady(ref: React.RefObject<HTMLImageElement | null>, onReady: () => void) {
|
|
|
+ const onReadyRef = useRef(onReady)
|
|
|
+ onReadyRef.current = onReady
|
|
|
+ const firedRef = useRef(false)
|
|
|
+
|
|
|
+ useLayoutEffect(() => {
|
|
|
+ const el = ref.current
|
|
|
+ if (!el) return
|
|
|
+
|
|
|
+ const fire = () => {
|
|
|
+ if (firedRef.current) return
|
|
|
+ firedRef.current = true
|
|
|
+ void (async () => {
|
|
|
+ try {
|
|
|
+ if (typeof el.decode === 'function') await el.decode()
|
|
|
+ } catch {}
|
|
|
+ requestAnimationFrame(() => requestAnimationFrame(() => onReadyRef.current()))
|
|
|
+ })()
|
|
|
+ }
|
|
|
+
|
|
|
+ if (el.complete && el.naturalWidth > 0) {
|
|
|
+ fire()
|
|
|
+ } else {
|
|
|
+ el.addEventListener('load', fire, { once: true })
|
|
|
+ el.addEventListener('error', fire, { once: true })
|
|
|
+ }
|
|
|
+
|
|
|
+ return () => {
|
|
|
+ el.removeEventListener('load', fire)
|
|
|
+ el.removeEventListener('error', fire)
|
|
|
+ }
|
|
|
+ }, [ref])
|
|
|
+}
|
|
|
|
|
|
-export default MemoA0base;
|
|
|
+const MemoA0base = React.memo(A0base)
|
|
|
+export default MemoA0base
|