1+ import React , { useEffect , useRef , useState } from 'react' ;
2+
3+ const SponsorList = ( { sponsors = [ ] } ) => {
4+ const [ visible , setVisible ] = useState ( [ ] ) ;
5+ const cardRefs = useRef ( [ ] ) ;
6+
7+ useEffect ( ( ) => {
8+ if ( ! ( 'IntersectionObserver' in window ) ) {
9+ setVisible ( sponsors . map ( ( ) => true ) ) ;
10+ return ;
11+ }
12+ const observer = new IntersectionObserver (
13+ ( entries ) => {
14+ entries . forEach ( ( entry ) => {
15+ const index = Number ( entry . target . dataset . index ) ;
16+ if ( entry . isIntersecting ) {
17+ setVisible ( ( prev ) => {
18+ const next = [ ...prev ] ;
19+ next [ index ] = true ;
20+ return next ;
21+ } ) ;
22+ observer . unobserve ( entry . target ) ;
23+ }
24+ } ) ;
25+ } ,
26+ { threshold : 0.15 , rootMargin : '0px 0px -20px 0px' }
27+ ) ;
28+ cardRefs . current . forEach ( ( el ) => el && observer . observe ( el ) ) ;
29+ return ( ) => observer . disconnect ( ) ;
30+ } , [ sponsors ] ) ;
31+
32+ useEffect ( ( ) => {
33+ setVisible ( sponsors . map ( ( ) => false ) ) ;
34+ const timer = setTimeout ( ( ) => {
35+ if ( cardRefs . current . length && 'IntersectionObserver' in window ) {
36+ const observer = new IntersectionObserver (
37+ ( entries ) => {
38+ entries . forEach ( ( entry ) => {
39+ const index = Number ( entry . target . dataset . index ) ;
40+ if ( entry . isIntersecting ) {
41+ setVisible ( ( prev ) => {
42+ const next = [ ...prev ] ;
43+ next [ index ] = true ;
44+ return next ;
45+ } ) ;
46+ observer . unobserve ( entry . target ) ;
47+ }
48+ } ) ;
49+ } ,
50+ { threshold : 0.15 , rootMargin : '0px 0px -20px 0px' }
51+ ) ;
52+ cardRefs . current . forEach ( ( el ) => el && observer . observe ( el ) ) ;
53+ return ( ) => observer . disconnect ( ) ;
54+ }
55+ } , 100 ) ;
56+ return ( ) => clearTimeout ( timer ) ;
57+ } , [ sponsors ] ) ;
58+
59+ return (
60+ < div className = "grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-6 max-w-6xl mx-auto my-8 px-4" >
61+ { sponsors . map ( ( sponsor , index ) => {
62+ const isVisible = visible [ index ] || false ;
63+ const delay = ( index % 6 ) * 60 ;
64+
65+ return (
66+ < div
67+ key = { index }
68+ ref = { ( el ) => ( cardRefs . current [ index ] = el ) }
69+ data-index = { index }
70+ className = 'bg-white dark:bg-[#1f262b] rounded-2xl p-5 text-center border border-gray-200 dark:border-[#354048] shadow-sm transition-all duration-300 ease-out hover:-translate-y-1.5 hover:shadow-lg hover:border-[#f08a7f]"}'
71+ style = { { transitionDelay : isVisible ? `${ delay } ms` : '0ms' } }
72+ >
73+ { /* 名称:强制单行,不换行,且留足空间 */ }
74+ < div className = "text-lg font-semibold text-gray-800 dark:text-gray-100 whitespace-nowrap overflow-visible" >
75+ { sponsor . name || '匿名赞助者' }
76+ </ div >
77+
78+ { /* 金额 + 日期 */ }
79+ { ( sponsor . amount || sponsor . date ) && (
80+ < div className = "flex justify-center gap-2 mt-1 flex-wrap" >
81+ { sponsor . amount && (
82+ < span className = "text-xs bg-gray-100 dark:bg-[#2a333a] text-gray-600 dark:text-gray-300 px-2 py-0.5 rounded-full" >
83+ { sponsor . amount }
84+ </ span >
85+ ) }
86+ { sponsor . date && (
87+ < span className = "text-xs bg-gray-100 dark:bg-[#2a333a] text-gray-600 dark:text-gray-300 px-2 py-0.5 rounded-full" >
88+ { sponsor . date }
89+ </ span >
90+ ) }
91+ </ div >
92+ ) }
93+
94+ { /* 留言 */ }
95+ { sponsor . message && (
96+ < div className = "text-sm text-gray-600 dark:text-gray-400 mt-2 static border-t border-dashed border-gray-200 dark:border-[#354048] pt-2" >
97+ “{ sponsor . message } ”
98+ </ div >
99+ ) }
100+ </ div >
101+ ) ;
102+ } ) }
103+ </ div >
104+ ) ;
105+ } ;
106+
107+ export default SponsorList ;
0 commit comments