117 lines
4.2 KiB
TypeScript
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;
|