@@ -30,17 +30,30 @@ const defaultSteps = [
3030 { label : 'Review & Submit' } ,
3131]
3232
33- const StepIndicator = ( { currentStep, steps } ) => {
33+ const StepIndicator = ( {
34+ currentStep,
35+ steps,
36+ activeColor : propActiveColor ,
37+ inactiveBgColor : propInactiveBgColor ,
38+ circleSize = 44 ,
39+ gradientColors = [ '#ff7a18' , '#af002d' , '#319197' ] ,
40+ showPercent = true ,
41+ } ) => {
3442 const { themeSettings } = useProThemeContext ( )
3543 const isMobile = useStore ( ( s ) => s . isMobile )
3644 const isDark = themeSettings . themeMode === 'dark'
3745
38- // 增强蓝色对比度
39- const activeColor = isDark ? '#40a9ff' : '#0958d9' // 更深的蓝色
46+ // 允许外部传入颜色;否则基于 theme 决定默认颜色
47+ const activeColor = propActiveColor || ( isDark ? '#40a9ff' : '#0958d9' )
4048 const activeTextColor = '#ffffff'
41- const inactiveBgColor = isDark ? '#262626' : '#f5f5f5'
49+ const inactiveBgColor = propInactiveBgColor || ( isDark ? '#262626' : '#f5f5f5' )
4250 const inactiveTextColor = isDark ? '#8c8c8c' : '#bfbfbf'
4351
52+ const desktopSize = circleSize
53+ const mobileSize = Math . max ( Math . round ( circleSize * 0.82 ) , 32 )
54+
55+ const progressPercent = Math . round ( ( currentStep / Math . max ( 1 , steps . length - 1 ) ) * 100 )
56+
4457 return (
4558 < div className = "relative w-full" >
4659 { isMobile ? (
@@ -50,11 +63,18 @@ const StepIndicator = ({ currentStep, steps }) => {
5063 < div key = { step . label } className = "flex items-center space-x-3" >
5164 < motion . div
5265 className = { clsx (
53- 'flex size-8 items-center justify-center rounded-full border-2 transition-all duration-300' ,
66+ 'flex items-center justify-center rounded-full border-2 transition-all duration-300' ,
5467 index <= currentStep ? 'border-transparent shadow-lg' : 'border-gray-300 dark:border-gray-600'
5568 ) }
56- animate = { { scale : index === currentStep ? 1.1 : 1 } }
69+ animate = { { scale : index === currentStep ? 1.06 : 1 } }
5770 style = { {
71+ width : mobileSize ,
72+ height : mobileSize ,
73+ minWidth : mobileSize ,
74+ minHeight : mobileSize ,
75+ display : 'flex' ,
76+ alignItems : 'center' ,
77+ justifyContent : 'center' ,
5878 backgroundColor : index <= currentStep ? activeColor : inactiveBgColor ,
5979 color : index <= currentStep ? activeTextColor : inactiveTextColor ,
6080 borderColor : index <= currentStep ? activeColor : undefined ,
@@ -83,78 +103,109 @@ const StepIndicator = ({ currentStep, steps }) => {
83103 >
84104 { step . label }
85105 </ div >
86- { index === currentStep && (
87- < div className = "mt-1 text-xs" style = { { color : activeColor } } >
88- 当前步骤
106+ { index === currentStep && showPercent && (
107+ < div className = "mt-1 text-xs font-semibold " style = { { color : activeColor } } >
108+ { progressPercent } %
89109 </ div >
90110 ) }
91111 </ div >
92112 </ div >
93113 ) ) }
94114 </ div >
95115 ) : (
96- // 桌面端水平布局
97- < div className = "flex items-center justify-between" >
98- { steps . map ( ( step , index ) => (
99- < React . Fragment key = { step . label } >
100- < div className = "flex flex-col items-center" >
101- < motion . div
102- className = { clsx (
103- 'flex size-8 items-center justify-center rounded-full border-2 transition-all duration-300' ,
104- index <= currentStep ? 'border-transparent shadow-lg' : 'border-gray-300 dark:border-gray-600'
105- ) }
106- animate = { { scale : index === currentStep ? 1.1 : 1 } }
107- style = { {
108- backgroundColor : index <= currentStep ? activeColor : inactiveBgColor ,
109- color : index <= currentStep ? activeTextColor : inactiveTextColor ,
110- borderColor : index <= currentStep ? activeColor : undefined ,
111- boxShadow : index <= currentStep ? `0 4px 12px ${ activeColor } 40` : undefined ,
112- } }
113- >
114- { index < currentStep ? (
115- < AnimatedIcon variant = "spin" mode = "hover" >
116- < CheckCircle size = { 16 } />
117- </ AnimatedIcon >
118- ) : (
119- < AnimatedIcon variant = "spin" mode = "hover" >
120- < Circle size = { 16 } fill = "currentColor" />
121- </ AnimatedIcon >
122- ) }
123- </ motion . div >
124- < div
125- className = { clsx (
126- 'mt-2 text-center text-xs font-medium transition-colors duration-300' ,
127- index <= currentStep ? '' : 'text-gray-500 dark:text-gray-400'
128- ) }
129- style = { {
130- color : index <= currentStep ? activeColor : undefined ,
131- } }
132- >
133- { step . label }
134- </ div >
135- </ div >
136- { index < steps . length - 1 && (
137- < div className = "relative mx-4 flex-grow" >
138- < div
139- className = "absolute top-1/2 h-1 w-full -translate-y-1/2 rounded-full"
140- style = { { backgroundColor : inactiveBgColor } }
141- />
116+ // 桌面端水平布局(居中、限制标签宽度、优化连线)
117+ < div className = "flex w-full justify-center" style = { { marginBottom : 12 } } >
118+ < div className = "flex w-full max-w-3xl items-center" >
119+ { steps . map ( ( step , index ) => (
120+ < React . Fragment key = { step . label } >
121+ < div className = "flex flex-col items-center" style = { { flex : '0 0 140px' , minWidth : 80 } } >
142122 < motion . div
143- className = "absolute top-1/2 h-1 w-full -translate-y-1/2 rounded-full"
123+ className = { clsx (
124+ 'flex items-center justify-center rounded-full border-2 transition-all duration-300' ,
125+ index <= currentStep ? 'border-transparent shadow-lg' : 'border-gray-300 dark:border-gray-600'
126+ ) }
127+ animate = { { scale : index === currentStep ? 1.06 : 1 } }
144128 style = { {
145- backgroundColor : activeColor ,
146- boxShadow : `0 0 8px ${ activeColor } 60` ,
129+ width : desktopSize ,
130+ height : desktopSize ,
131+ minWidth : desktopSize ,
132+ minHeight : desktopSize ,
133+ display : 'flex' ,
134+ alignItems : 'center' ,
135+ justifyContent : 'center' ,
136+ backgroundColor : index <= currentStep ? activeColor : inactiveBgColor ,
137+ color : index <= currentStep ? activeTextColor : inactiveTextColor ,
138+ borderColor : index <= currentStep ? activeColor : undefined ,
139+ boxShadow : index <= currentStep ? `0 6px 18px ${ activeColor } 30` : undefined ,
140+ zIndex : 10 ,
147141 } }
148- initial = { { width : '0%' } }
149- animate = { {
150- width : index < currentStep ? '100%' : '0%' ,
142+ >
143+ { index < currentStep ? (
144+ < AnimatedIcon variant = "spin" mode = "hover" >
145+ < CheckCircle size = { 18 } />
146+ </ AnimatedIcon >
147+ ) : (
148+ < AnimatedIcon variant = "spin" mode = "hover" >
149+ < Circle size = { 18 } fill = "currentColor" />
150+ </ AnimatedIcon >
151+ ) }
152+ </ motion . div >
153+
154+ < div
155+ className = { clsx (
156+ 'mt-2 text-center text-xs font-medium transition-colors duration-300' ,
157+ index <= currentStep ? '' : 'text-gray-500 dark:text-gray-400'
158+ ) }
159+ style = { {
160+ color : index <= currentStep ? activeColor : undefined ,
161+ maxWidth : 120 ,
162+ textOverflow : 'ellipsis' ,
163+ overflow : 'hidden' ,
164+ whiteSpace : 'nowrap' ,
151165 } }
152- transition = { { duration : 0.5 , ease : 'easeInOut' } }
153- />
166+ title = { step . label }
167+ >
168+ { step . label }
169+ </ div >
170+
171+ { index === currentStep && showPercent && (
172+ < div className = "mt-1 text-xs font-semibold" style = { { color : activeColor } } >
173+ { progressPercent } %
174+ </ div >
175+ ) }
154176 </ div >
155- ) }
156- </ React . Fragment >
157- ) ) }
177+
178+ { index < steps . length - 1 && (
179+ < div className = "relative flex-1 px-2" style = { { display : 'flex' , alignItems : 'center' } } >
180+ < div className = "h-1 w-full rounded-full" style = { { backgroundColor : inactiveBgColor , zIndex : 0 } } />
181+ < motion . div
182+ className = "absolute top-1/2 left-0 h-1 -translate-y-1/2 rounded-full"
183+ style = { {
184+ backgroundImage : `linear-gradient(90deg, ${ gradientColors . join ( ',' ) } )` ,
185+ backgroundSize : '200% 100%' ,
186+ boxShadow : `0 0 8px ${ activeColor } 60` ,
187+ zIndex : 1 ,
188+ } }
189+ initial = { {
190+ width : index < currentStep ? '100%' : index === currentStep ? '40%' : '0%' ,
191+ backgroundPosition : '0% 0%' ,
192+ } }
193+ animate = { {
194+ width : index < currentStep ? '100%' : index === currentStep ? '40%' : '0%' ,
195+ backgroundPosition : index < currentStep ? [ '0% 0%' , '100% 0%' ] : '0% 0%' ,
196+ } }
197+ transition = { {
198+ duration : 0.5 ,
199+ ease : 'easeInOut' ,
200+ backgroundPosition :
201+ index < currentStep ? { repeat : Infinity , duration : 3 , ease : 'linear' } : undefined ,
202+ } }
203+ />
204+ </ div >
205+ ) }
206+ </ React . Fragment >
207+ ) ) }
208+ </ div >
158209 </ div >
159210 ) }
160211 </ div >
@@ -164,6 +215,11 @@ const StepIndicator = ({ currentStep, steps }) => {
164215StepIndicator . propTypes = {
165216 currentStep : PropTypes . number . isRequired ,
166217 steps : PropTypes . array . isRequired ,
218+ activeColor : PropTypes . string ,
219+ inactiveBgColor : PropTypes . string ,
220+ circleSize : PropTypes . number ,
221+ gradientColors : PropTypes . array ,
222+ showPercent : PropTypes . bool ,
167223}
168224
169225const StepContent = ( { step } ) => {
@@ -216,25 +272,25 @@ StepContent.propTypes = {
216272 step : PropTypes . object . isRequired ,
217273}
218274
219- const NavigationButtons = ( { currentStep, totalSteps, handlePrev, handleNext } ) => {
275+ const NavigationButtons = ( { currentStep, totalSteps, handlePrev, handleNext, primaryColor : primaryColorProp } ) => {
220276 const { themeSettings } = useProThemeContext ( )
221277 const { token } = useToken ( )
222278 const isMobile = useStore ( ( s ) => s . isMobile )
223279 const isDark = themeSettings . themeMode === 'dark'
224280
225- const primaryColor = isDark ? '#40a9ff' : '#0958d9'
281+ const primaryColor = primaryColorProp || ( isDark ? '#40a9ff' : '#0958d9' )
226282 const secondaryBg = isDark ? token . colorBgContainer : '#f8f9fa'
227283 const secondaryBorder = isDark ? token . colorBorder : '#d9d9d9'
228284 const secondaryText = isDark ? token . colorTextSecondary : '#666666'
229285
230286 return (
231- < div className = { clsx ( 'flex gap-3' , isMobile ? 'flex-col' : 'justify-between' ) } >
287+ < div className = { clsx ( 'flex gap-3' , isMobile ? 'flex-col' : 'justify-between' ) } style = { { marginTop : 16 } } >
232288 < Button
233289 disabled = { currentStep === 0 }
234290 onClick = { handlePrev }
235291 className = "transition-all duration-200 hover:shadow-md"
236292 style = { {
237- visibility : currentStep === 0 ? 'hidden ' : 'visible ' ,
293+ display : currentStep === 0 ? 'none ' : 'inline-flex ' ,
238294 backgroundColor : secondaryBg ,
239295 borderColor : secondaryBorder ,
240296 color : secondaryText ,
@@ -249,7 +305,7 @@ const NavigationButtons = ({ currentStep, totalSteps, handlePrev, handleNext })
249305 onClick = { handleNext }
250306 className = "font-medium transition-all duration-200 hover:shadow-lg"
251307 style = { {
252- visibility : currentStep === totalSteps - 1 ? 'hidden ' : 'visible ' ,
308+ display : currentStep === totalSteps - 1 ? 'none ' : 'inline-flex ' ,
253309 backgroundColor : primaryColor ,
254310 borderColor : primaryColor ,
255311 boxShadow : `0 2px 8px ${ primaryColor } 30` ,
@@ -266,9 +322,18 @@ NavigationButtons.propTypes = {
266322 totalSteps : PropTypes . number . isRequired ,
267323 handlePrev : PropTypes . func . isRequired ,
268324 handleNext : PropTypes . func . isRequired ,
325+ primaryColor : PropTypes . string ,
269326}
270327
271- const DottedStepper = ( { steps = defaultSteps , className } ) => {
328+ const DottedStepper = ( {
329+ steps = defaultSteps ,
330+ className,
331+ activeColor,
332+ inactiveBgColor,
333+ circleSize = 44 ,
334+ gradientColors = [ '#ff7a18' , '#af002d' , '#319197' ] ,
335+ showPercent = true ,
336+ } ) => {
272337 const [ currentStep , setCurrentStep ] = useState ( 0 )
273338 const { token } = useToken ( )
274339
@@ -281,14 +346,26 @@ const DottedStepper = ({ steps = defaultSteps, className }) => {
281346 }
282347
283348 return (
284- < div className = { clsx ( 'mx-auto w-full max-w-3xl p-4 md:p-6' , className ) } style = { { color : token . colorText } } >
285- < StepIndicator currentStep = { currentStep } steps = { steps } />
349+ < div
350+ className = { clsx ( 'mx-auto w-full max-w-2xl p-6 md:max-w-3xl md:p-8' , className ) }
351+ style = { { color : token . colorText } }
352+ >
353+ < StepIndicator
354+ currentStep = { currentStep }
355+ steps = { steps }
356+ activeColor = { activeColor }
357+ inactiveBgColor = { inactiveBgColor }
358+ circleSize = { circleSize }
359+ gradientColors = { gradientColors }
360+ showPercent = { showPercent }
361+ />
286362 < StepContent step = { steps [ currentStep ] } />
287363 < NavigationButtons
288364 currentStep = { currentStep }
289365 handleNext = { handleNext }
290366 handlePrev = { handlePrev }
291367 totalSteps = { steps . length }
368+ primaryColor = { activeColor }
292369 />
293370 </ div >
294371 )
@@ -299,4 +376,17 @@ export default DottedStepper
299376DottedStepper . propTypes = {
300377 steps : PropTypes . array ,
301378 className : PropTypes . string ,
379+ activeColor : PropTypes . string ,
380+ inactiveBgColor : PropTypes . string ,
381+ circleSize : PropTypes . number ,
382+ gradientColors : PropTypes . array ,
383+ showPercent : PropTypes . bool ,
384+ }
385+
386+ DottedStepper . defaultProps = {
387+ activeColor : undefined ,
388+ inactiveBgColor : undefined ,
389+ circleSize : 44 ,
390+ gradientColors : [ '#ff7a18' , '#af002d' , '#319197' ] ,
391+ showPercent : true ,
302392}
0 commit comments