1+ import React , { useEffect , useRef } from 'react' ;
2+ import styled from 'styled-components' ;
3+ import * as d3 from 'd3' ;
4+
5+ // 楼梯容器,增强3D效果
6+ const StairsContainer = styled . div `
7+ position: relative;
8+ width: 100%;
9+ height: 100%;
10+ display: flex;
11+ justify-content: center;
12+ align-items: center;
13+ perspective: 1200px;
14+ overflow: hidden;
15+ background: radial-gradient(circle at 50% 50%, rgba(250, 250, 250, 0.3) 0%, rgba(200, 200, 200, 0.1) 100%);
16+ ` ;
17+
18+ // 楼梯包装器,负责3D旋转
19+ const StairsWrapper = styled . div `
20+ position: relative;
21+ transform-style: preserve-3d;
22+ transform: rotateX(30deg) rotateY(-5deg);
23+ transition: transform 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275);
24+
25+ &:hover {
26+ transform: rotateX(35deg) rotateY(-8deg);
27+ }
28+ ` ;
29+
30+ // 单个阶梯样式,增强3D质感和交互效果
31+ interface StairStepProps {
32+ index : number ;
33+ totalSteps : number ;
34+ status : 'uncalculated' | 'calculating' | 'calculated' ;
35+ isCurrentStep : boolean ;
36+ }
37+
38+ const StairStep = styled . div < StairStepProps > `
39+ position: absolute;
40+ transform-style: preserve-3d;
41+ transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
42+ width: ${ props => props . totalSteps <= 5 ? '160px' : props . totalSteps <= 10 ? '140px' : '120px' } ;
43+ height: ${ props => props . totalSteps <= 5 ? '40px' : props . totalSteps <= 10 ? '30px' : '20px' } ;
44+ background-color: ${ props => {
45+ if ( props . isCurrentStep ) return '#4CAF50' ;
46+ switch ( props . status ) {
47+ case 'calculated' : return '#81C784' ;
48+ case 'calculating' : return '#A5D6A7' ;
49+ default : return '#E0E0E0' ;
50+ }
51+ } } ;
52+ opacity: ${ props => props . status === 'uncalculated' ? 0.7 : 1 } ;
53+ box-shadow: ${ props =>
54+ props . isCurrentStep
55+ ? '0 0 20px #A5D6A7, 0 8px 20px rgba(0,0,0,0.4)'
56+ : props . status === 'calculated'
57+ ? '0 0 15px rgba(129, 199, 132, 0.5), 0 8px 15px rgba(0,0,0,0.3)'
58+ : '0 8px 15px rgba(0,0,0,0.2)'
59+ } ;
60+ border: ${ props => props . isCurrentStep ? '2px solid #A5D6A7' : 'none' } ;
61+ filter: ${ props => props . isCurrentStep ? 'saturate(1.8) brightness(1.2)' : props . status === 'calculated' ? 'saturate(1.3)' : 'saturate(0.5)' } ;
62+ transform: ${ props => `translateY(${ - props . index * ( props . totalSteps <= 5 ? 60 : props . totalSteps <= 10 ? 50 : 40 ) } px) translateZ(${ props . index * 2 } px)` } ;
63+ animation: ${ props => props . isCurrentStep ? 'pulseBorder 1.5s infinite' : props . index < 3 ? 'none' : 'float 3s ease-in-out infinite' } ;
64+
65+ &::before {
66+ content: '';
67+ position: absolute;
68+ width: 100%;
69+ height: ${ props => ( props . totalSteps <= 5 ? 20 : props . totalSteps <= 10 ? 15 : 10 ) } px;
70+ background-color: ${ props => {
71+ const baseColor = props . isCurrentStep ? '#4CAF50' :
72+ props . status === 'calculated' ? '#81C784' :
73+ props . status === 'calculating' ? '#A5D6A7' : '#E0E0E0' ;
74+ // 侧面比正面暗30%
75+ return d3 . color ( baseColor ) ?. darker ( 0.8 ) . toString ( ) || baseColor ;
76+ } } ;
77+ transform: translateY(100%) rotateX(-90deg);
78+ transform-origin: top;
79+ box-shadow: inset 0 -3px 8px rgba(0,0,0,0.2);
80+ }
81+
82+ &::after {
83+ content: '';
84+ position: absolute;
85+ width: 20px;
86+ height: 100%;
87+ right: -20px;
88+ background-color: ${ props => {
89+ const baseColor = props . isCurrentStep ? '#4CAF50' :
90+ props . status === 'calculated' ? '#81C784' :
91+ props . status === 'calculating' ? '#A5D6A7' : '#E0E0E0' ;
92+ // 右侧面比正面暗20%
93+ return d3 . color ( baseColor ) ?. darker ( 0.5 ) . toString ( ) || baseColor ;
94+ } } ;
95+ transform: rotateY(90deg);
96+ transform-origin: left;
97+ box-shadow: inset 3px 0 8px rgba(0,0,0,0.1);
98+ }
99+
100+ @keyframes pulseBorder {
101+ 0%, 100% { border-color: rgba(165, 214, 167, 0.7); box-shadow: 0 0 20px rgba(165, 214, 167, 0.5), 0 8px 20px rgba(0,0,0,0.4); }
102+ 50% { border-color: rgba(165, 214, 167, 1); box-shadow: 0 0 30px rgba(165, 214, 167, 0.8), 0 8px 20px rgba(0,0,0,0.4); }
103+ }
104+
105+ animation: ${ props => props . isCurrentStep ? 'pulseBorder 1.5s infinite' : props . index < 3 ? 'none' : 'float 3s ease-in-out infinite' } ;
106+
107+ &:hover {
108+ filter: brightness(1.1);
109+ z-index: 10;
110+ }
111+ ` ;
112+
113+ // 优化的编号标签样式
114+ const StepNumber = styled . div < { isCurrentStep : boolean } > `
115+ position: absolute;
116+ left: -35px;
117+ top: 50%;
118+ transform: translateY(-50%);
119+ background-color: rgba(0, 0, 0, 0.8);
120+ color: white;
121+ width: 30px;
122+ height: 30px;
123+ border-radius: 50%;
124+ display: flex;
125+ justify-content: center;
126+ align-items: center;
127+ font-size: 14px;
128+ font-weight: ${ props => props . isCurrentStep ? 'bold' : 'normal' } ;
129+ box-shadow: ${ props => props . isCurrentStep ? '0 0 12px rgba(255, 255, 255, 0.8)' : '0 2px 5px rgba(0,0,0,0.3)' } ;
130+ text-shadow: ${ props => props . isCurrentStep ? '0 0 3px white' : 'none' } ;
131+ transition: all 0.3s ease;
132+
133+ &:hover {
134+ transform: translateY(-50%) scale(1.1);
135+ box-shadow: 0 0 15px rgba(255, 255, 255, 0.9);
136+ }
137+ ` ;
138+
139+ // 增强计数器样式
140+ const ValueCounter = styled . div < { status : 'uncalculated' | 'calculating' | 'calculated' , isCurrentStep : boolean } > `
141+ position: absolute;
142+ right: -35px;
143+ top: 50%;
144+ transform: translateY(-50%);
145+ background: ${ props => {
146+ if ( props . status === 'uncalculated' ) return 'radial-gradient(circle, #F5F5F5, #E0E0E0)' ;
147+ if ( props . isCurrentStep ) return 'radial-gradient(circle, #4CAF50, #2E7D32)' ;
148+ return 'radial-gradient(circle, #81C784, #43A047)' ;
149+ } } ;
150+ color: ${ props => props . status === 'uncalculated' ? '#9E9E9E' : 'white' } ;
151+ width: 36px;
152+ height: 36px;
153+ border-radius: 50%;
154+ display: flex;
155+ justify-content: center;
156+ align-items: center;
157+ font-size: 16px;
158+ font-weight: bold;
159+ border: 3px solid ${ props => props . status === 'uncalculated' ? '#BDBDBD' : props . isCurrentStep ? '#2E7D32' : '#43A047' } ;
160+ box-shadow:
161+ inset 0 0 8px rgba(0, 0, 0, 0.2),
162+ 0 0 ${ props => props . isCurrentStep ? '15px rgba(76, 175, 80, 0.7)' : '8px rgba(0, 0, 0, 0.2)' } ;
163+ transition: all 0.3s ease;
164+ animation: ${ props => props . isCurrentStep ? 'pulse 2s infinite' : 'none' } ;
165+
166+ @keyframes pulse {
167+ 0% { transform: translateY(-50%) scale(1); }
168+ 50% { transform: translateY(-50%) scale(1.08); }
169+ 100% { transform: translateY(-50%) scale(1); }
170+ }
171+
172+ &:hover {
173+ transform: translateY(-50%) scale(1.1);
174+ box-shadow:
175+ inset 0 0 8px rgba(0, 0, 0, 0.2),
176+ 0 0 ${ props => props . isCurrentStep ? '20px rgba(76, 175, 80, 0.9)' : '12px rgba(0, 0, 0, 0.3)' } ;
177+ }
178+ ` ;
179+
180+ // 增强目标标识样式
181+ const TargetMarker = styled . div `
182+ position: absolute;
183+ width: 45px;
184+ height: 45px;
185+ top: 50%;
186+ left: 50%;
187+ transform: translate(-50%, -50%);
188+ color: gold;
189+ display: flex;
190+ justify-content: center;
191+ align-items: center;
192+ filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.8));
193+ animation: rotateStar 6s infinite linear, pulseStar 3s infinite ease-in-out;
194+
195+ svg {
196+ width: 100%;
197+ height: 100%;
198+ }
199+
200+ @keyframes rotateStar {
201+ from { transform: translate(-50%, -50%) rotate(0deg); }
202+ to { transform: translate(-50%, -50%) rotate(360deg); }
203+ }
204+
205+ @keyframes pulseStar {
206+ 0%, 100% { transform: translate(-50%, -50%) scale(1); filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.8)); }
207+ 50% { transform: translate(-50%, -50%) scale(1.2); filter: drop-shadow(0 0 20px rgba(255, 215, 0, 0.9)); }
208+ }
209+ ` ;
210+
211+ // 路径箭头组件
212+ const PathArrow = styled . div < { type : 'one' | 'two' } > `
213+ position: absolute;
214+ left: ${ props => props . type === 'one' ? '50%' : '30%' } ;
215+ bottom: 100%;
216+ width: 4px;
217+ height: ${ props => props . type === 'one' ? '50px' : '80px' } ;
218+ background-color: ${ props => props . type === 'one' ? '#2196F3' : '#FF9800' } ;
219+ opacity: 0.7;
220+ transform: translateX(-50%);
221+
222+ &::before {
223+ content: '';
224+ position: absolute;
225+ top: 0;
226+ left: 50%;
227+ transform: translate(-50%, -50%) rotate(45deg);
228+ width: 12px;
229+ height: 12px;
230+ border-left: 4px solid ${ props => props . type === 'one' ? '#2196F3' : '#FF9800' } ;
231+ border-top: 4px solid ${ props => props . type === 'one' ? '#2196F3' : '#FF9800' } ;
232+ }
233+
234+ &::after {
235+ content: '${ props => props . type === 'one' ? '爬1阶' : '爬2阶' } ';
236+ position: absolute;
237+ top: 50%;
238+ left: 10px;
239+ transform: translateY(-50%);
240+ color: ${ props => props . type === 'one' ? '#2196F3' : '#FF9800' } ;
241+ background-color: rgba(255, 255, 255, 0.7);
242+ padding: 2px 5px;
243+ border-radius: 10px;
244+ font-size: 12px;
245+ white-space: nowrap;
246+ }
247+ ` ;
248+
249+ // 单独定义Float动画组件
250+ const floatAnimation = ( index : number , totalSteps : number ) => {
251+ const baseY = - index * ( totalSteps <= 5 ? 60 : totalSteps <= 10 ? 50 : 40 ) ;
252+ const baseZ = index * 2 ;
253+
254+ return `
255+ @keyframes float${ index } {
256+ 0%, 100% { transform: translateY(${ baseY } px) translateZ(${ baseZ } px); }
257+ 50% { transform: translateY(${ baseY - 5 } px) translateZ(${ baseZ } px); }
258+ }
259+ ` ;
260+ } ;
261+
262+ interface StairsComponentProps {
263+ n : number ;
264+ currentStep : number ;
265+ stepStatuses : ( 'uncalculated' | 'calculating' | 'calculated' ) [ ] ;
266+ values : number [ ] ;
267+ }
268+
269+ const StairsComponent : React . FC < StairsComponentProps > = ( {
270+ n,
271+ currentStep,
272+ stepStatuses,
273+ values
274+ } ) => {
275+ const containerRef = useRef < HTMLDivElement > ( null ) ;
276+
277+ // 调整楼梯大小和位置
278+ useEffect ( ( ) => {
279+ if ( containerRef . current ) {
280+ // 适配容器大小的逻辑
281+ const container = containerRef . current ;
282+ const wrapper = container . querySelector ( '[data-stairs-wrapper]' ) as HTMLElement ;
283+
284+ if ( wrapper ) {
285+ // 根据n调整3D透视效果
286+ const perspective = Math . min ( 1500 , Math . max ( 800 , 1000 + n * 50 ) ) ;
287+ container . style . perspective = `${ perspective } px` ;
288+
289+ // 调整旋转角度使整个楼梯可见
290+ const rotateX = Math . min ( 45 , Math . max ( 15 , 30 - n * 0.5 ) ) ;
291+ const rotateY = Math . min ( 0 , Math . max ( - 15 , - 5 - n * 0.2 ) ) ;
292+ wrapper . style . transform = `rotateX(${ rotateX } deg) rotateY(${ rotateY } deg)` ;
293+ }
294+ }
295+ } , [ containerRef , n ] ) ;
296+
297+ // 渲染楼梯路径
298+ const renderPaths = ( stepIndex : number ) => {
299+ // 只为第3步及以上显示路径
300+ if ( stepIndex < 3 ) return null ;
301+
302+ return (
303+ < >
304+ < PathArrow type = "one" />
305+ < PathArrow type = "two" />
306+ </ >
307+ ) ;
308+ } ;
309+
310+ return (
311+ < StairsContainer ref = { containerRef } >
312+ < StairsWrapper data-stairs-wrapper >
313+ { /* 起点平台(0阶) */ }
314+ < StairStep
315+ index = { 0 }
316+ totalSteps = { n }
317+ status = { stepStatuses [ 0 ] || 'uncalculated' }
318+ isCurrentStep = { currentStep === 0 }
319+ >
320+ < StepNumber isCurrentStep = { currentStep === 0 } > 0</ StepNumber >
321+ < ValueCounter
322+ status = { stepStatuses [ 0 ] || 'uncalculated' }
323+ isCurrentStep = { currentStep === 0 }
324+ >
325+ { values [ 0 ] || '?' }
326+ </ ValueCounter >
327+ </ StairStep >
328+
329+ { /* 动态生成1至n阶楼梯 */ }
330+ { Array . from ( { length : n } ) . map ( ( _ , i ) => {
331+ const stepIndex = i + 1 ; // 实际阶梯编号从1开始
332+ const isTargetStep = stepIndex === n ;
333+
334+ return (
335+ < StairStep
336+ key = { stepIndex }
337+ index = { stepIndex }
338+ totalSteps = { n }
339+ status = { stepStatuses [ stepIndex ] || 'uncalculated' }
340+ isCurrentStep = { currentStep === stepIndex }
341+ >
342+ < StepNumber isCurrentStep = { currentStep === stepIndex } > { stepIndex } </ StepNumber >
343+ < ValueCounter
344+ status = { stepStatuses [ stepIndex ] || 'uncalculated' }
345+ isCurrentStep = { currentStep === stepIndex }
346+ >
347+ { values [ stepIndex ] !== undefined ? values [ stepIndex ] : '?' }
348+ </ ValueCounter >
349+
350+ { /* 路径箭头 */ }
351+ { renderPaths ( stepIndex ) }
352+
353+ { /* 目标标识星标 */ }
354+ { isTargetStep && (
355+ < TargetMarker >
356+ < svg viewBox = "0 0 24 24" >
357+ < path fill = "currentColor" d = "M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z" />
358+ </ svg >
359+ </ TargetMarker >
360+ ) }
361+ </ StairStep >
362+ ) ;
363+ } ) }
364+ </ StairsWrapper >
365+ </ StairsContainer >
366+ ) ;
367+ } ;
368+
369+ export default StairsComponent ;
0 commit comments