Files
video-gen/video-gen-app/src/components/SliderCaptcha.tsx
T
2026-05-25 17:08:18 +08:00

117 lines
4.2 KiB
TypeScript

import { useState, useRef, useCallback, useEffect } from 'react';
import { Typography } from 'antd';
interface SliderCaptchaProps {
onVerify: (x: number) => Promise<boolean>;
onSuccess: () => void;
width?: number;
}
const SliderCaptcha: React.FC<SliderCaptchaProps> = ({ onVerify, onSuccess, width = 320 }) => {
const [dragging, setDragging] = useState(false);
const [x, setX] = useState(0);
const [verified, setVerified] = useState(false);
const [failed, setFailed] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const sliderW = 44;
const maxDrag = width - sliderW;
const handleMove = useCallback((clientX: number) => {
if (!containerRef.current || !dragging) return;
const rect = containerRef.current.getBoundingClientRect();
const raw = clientX - rect.left - sliderW / 2;
setX(Math.max(0, Math.min(raw, maxDrag)));
}, [dragging, maxDrag]);
const handleEnd = useCallback(async () => {
if (!dragging) return;
setDragging(false);
const ratio = x / maxDrag;
const pixelX = Math.round(ratio * 260);
const ok = await onVerify(pixelX);
if (ok) {
setVerified(true);
onSuccess();
} else {
setFailed(true);
setX(0);
setTimeout(() => setFailed(false), 600);
}
}, [dragging, x, maxDrag, onVerify, onSuccess]);
useEffect(() => {
if (!dragging) return;
const onMouseMove = (e: MouseEvent) => handleMove(e.clientX);
const onTouchMove = (e: TouchEvent) => handleMove(e.touches[0].clientX);
const onUp = () => handleEnd();
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onUp);
window.addEventListener('touchmove', onTouchMove);
window.addEventListener('touchend', onUp);
return () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onUp);
window.removeEventListener('touchmove', onTouchMove);
window.removeEventListener('touchend', onUp);
};
}, [dragging, handleMove, handleEnd]);
return (
<div ref={containerRef} style={{
width, height: 44, borderRadius: 8, position: 'relative', userSelect: 'none',
background: verified ? '#ecfdf5' : failed ? '#fef2f2' : '#f1f5f9',
border: `1px solid ${verified ? '#10b981' : failed ? '#ef4444' : '#e2e8f0'}`,
transition: 'all 0.3s',
overflow: 'hidden',
}}>
{/* Track text */}
<div style={{
position: 'absolute', inset: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: verified ? '#10b981' : failed ? '#ef4444' : '#94a3b8',
fontSize: 13, fontWeight: 500,
transition: 'color 0.3s',
}}>
{verified ? '验证通过' : failed ? '验证失败,请重试' : '请按住滑块,拖动到最右边'}
</div>
{/* Slider */}
<div
onMouseDown={() => !verified && setDragging(true)}
onTouchStart={() => !verified && setDragging(true)}
style={{
position: 'absolute', left: x, top: 0,
width: sliderW, height: '100%',
borderRadius: 8,
background: verified
? 'linear-gradient(135deg, #10b981, #059669)'
: failed
? 'linear-gradient(135deg, #ef4444, #dc2626)'
: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: verified ? 'default' : 'grab',
boxShadow: dragging ? '0 4px 12px rgba(99,102,241,0.3)' : 'none',
transition: dragging ? 'none' : 'left 0.3s ease, background 0.3s',
}}
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M6 3l5 5-5 5" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
{/* Progress fill */}
{!verified && (
<div style={{
position: 'absolute', left: 0, top: 0, bottom: 0,
width: x + sliderW / 2,
background: failed ? 'rgba(239,68,68,0.06)' : 'rgba(99,102,241,0.06)',
borderRadius: '8px 0 0 8px',
transition: dragging ? 'none' : 'width 0.3s',
}} />
)}
</div>
);
};
export default SliderCaptcha;