69 lines
2.0 KiB
TypeScript
69 lines
2.0 KiB
TypeScript
interface SegmentedProgressBarProps {
|
|
totalSegments: number;
|
|
currentIndex: number;
|
|
segmentProgress: number;
|
|
onSeek: (ratio: number) => void;
|
|
}
|
|
|
|
export function SegmentedProgressBar({
|
|
totalSegments,
|
|
currentIndex,
|
|
segmentProgress,
|
|
onSeek,
|
|
}: SegmentedProgressBarProps) {
|
|
const seekFromClientX = (clientX: number, element: HTMLElement) => {
|
|
const rect = element.getBoundingClientRect();
|
|
onSeek((clientX - rect.left) / rect.width);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="relative h-1.5 cursor-pointer touch-none"
|
|
onClick={(e) => {
|
|
seekFromClientX(e.clientX, e.currentTarget as HTMLElement);
|
|
}}
|
|
onPointerDown={(e) => {
|
|
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
seekFromClientX(e.clientX, e.currentTarget);
|
|
}}
|
|
onPointerMove={(e) => {
|
|
if (!e.currentTarget.hasPointerCapture(e.pointerId)) return;
|
|
seekFromClientX(e.clientX, e.currentTarget);
|
|
}}
|
|
onPointerUp={(e) => {
|
|
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
}
|
|
}}
|
|
onPointerCancel={(e) => {
|
|
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
}
|
|
}}
|
|
>
|
|
<div className="flex gap-1.5 h-full">
|
|
{Array.from({ length: totalSegments }).map((_, i) => {
|
|
let fill = 0;
|
|
if (i < currentIndex) fill = 1;
|
|
else if (i === currentIndex) fill = segmentProgress;
|
|
return (
|
|
<div
|
|
key={i}
|
|
aria-label={`第 ${i + 1} 段`}
|
|
className="relative flex-1 h-full rounded-full bg-white/25 overflow-hidden"
|
|
>
|
|
<div
|
|
className="h-full origin-left bg-white"
|
|
style={{
|
|
transform: `scaleX(${Math.max(0, Math.min(1, fill))})`,
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|