import React, { useEffect, useState } from 'react'...
Created on: June 3, 2025
Created on: June 3, 2025
import React, { useEffect, useState } from 'react';
import './index.scss';
interface ICircularProgressBarProps {
strokeWidth?: number;
progress?: number;
rotationDeg?: number;
}
const CircularProgressBar: React.FC<ICircularProgressBarProps> = (props) => {
const { strokeWidth = 11, progress = 10, rotationDeg = 180 } = props;
const size = 375;
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const offset = circumference * (1 - progress / 100);
return (
<div className="circular-progress-bar-container">
<svg viewBox={0 0 ${size} ${size}
} style={{ transform: rotate(${rotationDeg}deg)
, transition: 'transform 0.5s linear' }}>
<circle cx={size / 2} cy={size / 2} r={radius} stroke="#eee" strokeWidth={strokeWidth} fill="none" />
<circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke="url(#gradient)"
strokeWidth={strokeWidth}
fill="none"
strokeDasharray={circumference}
strokeDashoffset={offset}
strokeLinecap="round"
style={{ transition: 'stroke-dashoffset 0.5s linear' }}
/>
<defs>
<linearGradient id="gradient" x1="1" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#FFE555" />
<stop offset="100%" stopColor="#FF732D" />
</linearGradient>
</defs>
</svg>
</div>
);
};
interface FerrisWheelProps {
pointCount: number;
targetIndex: number;
rotationSpeed?: number;
children: React.ReactNode[];
onRotationEnd?: () => void;
onAngleChange?: (deg: number) => void;
onActiveIndexChange?: (index: number) => void;
}
const FerrisWheel: React.FC<FerrisWheelProps> = ({
pointCount,
targetIndex,
rotationSpeed = 90,
children,
onRotationEnd,
onAngleChange,
onActiveIndexChange,
}) => {
const [totalDeg, setTotalDeg] = useState(0);
const [transitionDuration, setTransitionDuration] = useState(0);
useEffect(() => {
const anglePerIndex = 360 / pointCount;
const targetAngle = -anglePerIndex * targetIndex;
const currentAngle = totalDeg % 360;
let delta = targetAngle - currentAngle;
while (delta > 180) delta -= 360;
while (delta < -180) delta += 360;
const duration = Math.abs(delta) / rotationSpeed;
if (duration === 0) return;
setTransitionDuration(duration);
setTotalDeg((prev) => {
const next = prev + delta;
onAngleChange?.(next);
const idx = (((-next / anglePerIndex) % pointCount) + pointCount) % pointCount;
onActiveIndexChange?.(Math.round(idx));
return next;
});
}, [targetIndex, pointCount, rotationSpeed]);
const handleTransitionEnd = () => {
onRotationEnd?.();
};
const radiusPercent = 50 - 6;
const anglePerIndex = 360 / pointCount;
const points = children.map((child, i) => {
const angle = anglePerIndex * i;
const rad = (angle * Math.PI) / 180;
const x = 50 + radiusPercent * Math.cos(rad);
const y = 50 + radiusPercent * Math.sin(rad);
return (
<div
key={i}
style={{
position: 'absolute',
left: ${x}%
,
top: ${y}%
,
transform: 'translate(-50%, -50%)',
userSelect: 'none',
}}
>
<div style={{ transform: rotate(${-totalDeg}deg)
}}>{child}</div>
</div>
);
});
return (
<div className="ferris-wheel-container">
<div
className="ferris-wheel-wapper"
style={{
transition: transform ${transitionDuration}s linear
,
transform: rotate(${totalDeg}deg)
,
}}
onTransitionEnd={handleTransitionEnd}
>
{points}
</div>
</div>
);
};
const CircularProgress = () => {
const [targetIndex, setTargetIndex] = useState(0);
const [currentAngle, setCurrentAngle] = useState(0);
const [activeIndex, setActiveIndex] = useState(0);
const points = ['点1', '点2', '点3', '点4', '点5', '点6', '点7', '点8'];
const nextPoint = () => {
setTargetIndex((idx) => (idx + 1) % points.length);
};
const backPoint = () => {
setTargetIndex((idx) => (idx - 1 + points.length) % points.length);
};
return (
<div className="circular-progress-container">
<button onClick={nextPoint} style={{ position: 'relative', zIndex: 9999 }}>
前进
</button>
<button onClick={backPoint} style={{ position: 'relative', zIndex: 9999 }}>
后退
</button>
<CircularProgressBar rotationDeg={-currentAngle} progress={30} />
<FerrisWheel
pointCount={points.length}
targetIndex={targetIndex}
onAngleChange={(deg) => setCurrentAngle(deg)}
onActiveIndexChange={(i) => setActiveIndex(i)}
>
{points.map((label, i) => (
<div
key={i}
style={{
width: 40,
height: 40,
borderRadius: '50%',
background: i === activeIndex ? '#FF732D' : '#ccc',
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
userSelect: 'none',
cursor: 'pointer',
}}
>
{label}
</div>
))}
</FerrisWheel>
</div>
);
};
export default CircularProgress;
You've created a well-structured React component that combines a circular progress bar with a rotating Ferris wheel of elements. Here's a breakdown of how everything works and a few suggestions for improvement:
CircularProgressBar
linearGradient
for a smooth and stylish progress effect.FerrisWheel
CircularProgress
CircularProgressBar
and FerrisWheel
.Animation Smoothing (Ease-In-Out)
linear
transitions for rotation. Consider using ease-in-out
for more natural motion:
tsxtransition: `transform ${transitionDuration}s ease-in-out`,
Responsiveness
size = 375
in CircularProgressBar
. Consider making it responsive using props.size
or a parent container’s dimensions.Accessibility
aria-label
s or role attributes. Add for better screen reader support:
tsx<button aria-label="Rotate Forward">前进</button>
SCSS Optimization
.scss
has layout definitions like:
scss.circular-progress-bar-container, .ferris-wheel-container { position: relative; width: 375px; // or 100% if responsive height: 375px; } .ferris-wheel-wapper { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
Callback Stability
onAngleChange
, onActiveIndexChange
) in useCallback
to avoid unnecessary re-renders.progress
prop dynamically to the activeIndex
.If you want code examples for any of these improvements, just let me know!