index.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import React, { useEffect, useLayoutEffect, useState, useMemo, useRef, useCallback } from 'react'
  2. import history from '@/utils/history'
  3. import styles from './index.module.scss'
  4. import A0_home_1 from '@/assets/img/A0_home_1.jpg'
  5. import A0_home_2 from '@/assets/img/A0_home_2.jpg'
  6. import A0_home_3 from '@/assets/img/A0_home_3.jpg'
  7. import homeLogo from '@/assets/img/A0_home_logo.png'
  8. import { baseOssUrl } from '@/utils/http'
  9. const VIDEO_URL = `${baseOssUrl}myData/media/siji.mp4`
  10. const MIN_STAY_MS = 3000 // 至少停留3秒
  11. const EARLY_NAVIGATE_PROGRESS = 50 // 实际下载到50%即可尝试跳转(后台继续下载完整视频)
  12. function A0base() {
  13. const [currentIndex, setCurrentIndex] = useState(0)
  14. const [progress, setProgress] = useState(0)
  15. const [assetsReady, setAssetsReady] = useState(false)
  16. const images = useMemo(() => [A0_home_1, A0_home_2, A0_home_3], [])
  17. const slide0Ref = useRef<HTMLImageElement>(null)
  18. const slide1Ref = useRef<HTMLImageElement>(null)
  19. const slide2Ref = useRef<HTMLImageElement>(null)
  20. const logoRef = useRef<HTMLImageElement>(null)
  21. const pendingRef = useRef(4)
  22. const hasNavigatedRef = useRef(false)
  23. const markOneReady = useCallback(() => {
  24. pendingRef.current -= 1
  25. if (pendingRef.current <= 0) setAssetsReady(true)
  26. }, [])
  27. // 图片加载
  28. useDomImageReady(slide0Ref, markOneReady)
  29. useDomImageReady(slide1Ref, markOneReady)
  30. useDomImageReady(slide2Ref, markOneReady)
  31. useDomImageReady(logoRef, markOneReady)
  32. useEffect(() => {
  33. if (!assetsReady) return
  34. const abortController = new AbortController()
  35. let rafId: number | null = null
  36. let timeoutId: number | null = null
  37. let minStayReached = false
  38. let reachedNavigateProgress = false
  39. const startTime = performance.now()
  40. const goHomeOnce = () => {
  41. if (hasNavigatedRef.current) return
  42. hasNavigatedRef.current = true
  43. history.push('/home')
  44. }
  45. const tryGoHome = () => {
  46. if (minStayReached && reachedNavigateProgress) goHomeOnce()
  47. }
  48. // 最小停留时间
  49. timeoutId = window.setTimeout(() => {
  50. minStayReached = true
  51. tryGoHome()
  52. }, MIN_STAY_MS)
  53. // 3秒后平滑动画到100%
  54. const animateTo100 = () => {
  55. const elapsed = performance.now() - startTime
  56. if (elapsed >= MIN_STAY_MS) {
  57. setProgress(100)
  58. return
  59. }
  60. const next = Math.min(99, Math.round(60 + (elapsed / MIN_STAY_MS) * 40))
  61. setProgress(next)
  62. rafId = requestAnimationFrame(animateTo100)
  63. }
  64. const preloadFullVideo = async () => {
  65. try {
  66. const response = await fetch(VIDEO_URL, {
  67. signal: abortController.signal,
  68. cache: 'force-cache'
  69. })
  70. if (!response.ok) throw new Error('Fetch failed')
  71. const contentLength = response.headers.get('content-length')
  72. const total = contentLength ? parseInt(contentLength, 10) : 0
  73. if (!total) {
  74. handleFallback()
  75. return
  76. }
  77. const reader = response.body?.getReader()
  78. if (!reader) throw new Error('No reader')
  79. let loaded = 0
  80. while (true) {
  81. const { done, value } = await reader.read()
  82. if (done) break
  83. loaded += value.length
  84. const realProgress = Math.floor((loaded / total) * 100)
  85. // 显示进度策略:
  86. // 前3秒内最多显示60%,之后显示真实下载进度
  87. const displayProgress =
  88. performance.now() - startTime < MIN_STAY_MS ? Math.min(realProgress, 60) : realProgress
  89. setProgress(displayProgress)
  90. // 下载到50%即可尝试跳转(但后台继续完整下载)
  91. if (realProgress >= EARLY_NAVIGATE_PROGRESS) {
  92. reachedNavigateProgress = true
  93. tryGoHome()
  94. }
  95. }
  96. // 完整下载完成
  97. reachedNavigateProgress = true
  98. const elapsed = performance.now() - startTime
  99. if (elapsed < MIN_STAY_MS) {
  100. setProgress(60)
  101. rafId = requestAnimationFrame(animateTo100)
  102. } else {
  103. setProgress(100)
  104. }
  105. tryGoHome()
  106. } catch (err: any) {
  107. if (err.name !== 'AbortError') {
  108. console.error('Video preload failed:', err)
  109. }
  110. setProgress(100)
  111. reachedNavigateProgress = true
  112. tryGoHome()
  113. }
  114. }
  115. const handleFallback = () => {
  116. setProgress(60)
  117. rafId = requestAnimationFrame(animateTo100)
  118. reachedNavigateProgress = true
  119. tryGoHome()
  120. }
  121. preloadFullVideo()
  122. return () => {
  123. abortController.abort()
  124. if (timeoutId) clearTimeout(timeoutId)
  125. if (rafId) cancelAnimationFrame(rafId)
  126. }
  127. }, [assetsReady])
  128. // 进度切换背景图
  129. useEffect(() => {
  130. if (progress >= 65) setCurrentIndex(2)
  131. else if (progress >= 30) setCurrentIndex(1)
  132. else setCurrentIndex(0)
  133. }, [progress])
  134. return (
  135. <div className={styles.A0base}>
  136. {images.map((src, index) => (
  137. <img
  138. key={src}
  139. ref={index === 0 ? slide0Ref : index === 1 ? slide1Ref : slide2Ref}
  140. src={src}
  141. alt={`slide ${index + 1}`}
  142. className={`${styles.slide} ${index === currentIndex ? styles.active : ''}`}
  143. />
  144. ))}
  145. <div className={styles.homeLogo}>
  146. <img ref={logoRef} src={homeLogo} alt='home logo' />
  147. <div className='process'>{Math.floor(progress)}%</div>
  148. </div>
  149. </div>
  150. )
  151. }
  152. function useDomImageReady(ref: React.RefObject<HTMLImageElement | null>, onReady: () => void) {
  153. const onReadyRef = useRef(onReady)
  154. onReadyRef.current = onReady
  155. const firedRef = useRef(false)
  156. useLayoutEffect(() => {
  157. const el = ref.current
  158. if (!el) return
  159. const fire = () => {
  160. if (firedRef.current) return
  161. firedRef.current = true
  162. void (async () => {
  163. try {
  164. if (typeof el.decode === 'function') await el.decode()
  165. } catch {}
  166. requestAnimationFrame(() => requestAnimationFrame(() => onReadyRef.current()))
  167. })()
  168. }
  169. if (el.complete && el.naturalWidth > 0) {
  170. fire()
  171. } else {
  172. el.addEventListener('load', fire, { once: true })
  173. el.addEventListener('error', fire, { once: true })
  174. }
  175. return () => {
  176. el.removeEventListener('load', fire)
  177. el.removeEventListener('error', fire)
  178. }
  179. }, [ref])
  180. }
  181. const MemoA0base = React.memo(A0base)
  182. export default MemoA0base