🎯 滑块验证组件 (Slider Captcha)
一个现代化、响应式的滑块验证组件,专为 React 应用设计,提供流畅的用户体验和强大的安全验证功能。
✨ 功能特性
🎮 核心功能
- 智能滑块拖拽 – 支持鼠标和触摸屏操作,响应灵敏
- 随机目标位置 – 每次验证生成不同的目标位置,提高安全性
- 实时位置验证 – 精确的位置检测,支持容错范围设置
- 状态反馈 – 清晰的视觉反馈,包括成功、错误和待验证状态
🎨 用户体验
- 流畅动画 – 平滑的过渡动画和微交互效果
- 响应式设计 – 完美适配桌面端和移动端
- 直观界面 – 简洁美观的 UI 设计,操作简单明了
- 即时反馈 – 实时显示验证状态和结果
🔧 技术特性
- React Hooks – 使用最新的 React Hooks API
- TypeScript 支持 – 完整的类型定义(可扩展)
- Tailwind CSS – 现代化的样式框架
- 事件处理 – 完善的鼠标和触摸事件处理
- 性能优化 – 使用 useCallback 优化事件处理函数
🌟 产品亮点
- 拖拽体验 – 滑块跟随鼠标/手指移动,提供真实的拖拽感
- 视觉引导 – 绿色指示器显示目标位置,用户一目了然
- 状态动画 – 成功时的庆祝动画,错误时的震动反馈
- 触摸支持 – 完美支持移动端触摸操作
- 响应式布局 – 自适应不同屏幕尺寸
- 浏览器兼容 – 支持主流浏览器
🎯 使用场景
1. 用户注册/登录
- 防止机器人注册 – 在用户注册时验证人类用户
- 登录安全 – 在敏感操作前进行身份验证
- 密码重置 – 重置密码流程中的安全验证
2. 表单提交
- 评论系统 – 防止垃圾评论和恶意提交
- 联系表单 – 保护网站免受垃圾邮件攻击
- 调查问卷 – 确保问卷数据的真实性
3. 内容保护
- 下载验证 – 下载敏感文件前的身份验证
- 内容访问 – 访问付费或限制内容前的验证
- API 调用 – 防止 API 接口被恶意调用
4. 电商应用
- 下单验证 – 防止恶意下单和刷单行为
- 优惠券领取 – 限制优惠券的领取频率
- 评价系统 – 确保评价的真实性
5. 管理后台
- 敏感操作 – 管理员执行危险操作前的确认
- 数据导出 – 导出大量数据前的安全验证
- 系统设置 – 修改关键系统设置时的验证
🚀 快速开始
使用组件
import SliderCaptcha from "./components/SliderCaptcha";
function App() {
return (
<div className="App">
<SliderCaptcha />
</div>
);
}
自定义配置
// 可以轻松扩展组件以支持自定义配置
const captchaConfig = {
tolerance: 15, // 容错范围
sliderWidth: 40, // 滑块宽度
trackHeight: 48, // 轨道高度
theme: "dark", // 主题样式
};
源码
import { useState, useRef, useEffect, useCallback } from "react";
export default function SliderCaptcha() {
const [isVerified, setIsVerified] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [sliderPosition, setSliderPosition] = useState(0);
const [startX, setStartX] = useState(0);
const [targetPosition, setTargetPosition] = useState(0);
const [showSuccess, setShowSuccess] = useState(false);
const [showError, setShowError] = useState(false);
const sliderRef = useRef(null);
const trackRef = useRef(null);
const containerRef = useRef(null);
// 生成随机目标位置
useEffect(() => {
const container = containerRef.current;
if (container) {
const containerWidth = container.offsetWidth;
const sliderWidth = 32; // 滑块宽度 (w-8 = 32px)
const maxPosition = containerWidth - sliderWidth;
const randomPosition = Math.random() * (maxPosition - 50) + 25; // 避免太靠近边缘
setTargetPosition(randomPosition);
}
}, []);
// 获取客户端坐标
const getClientX = useCallback((e) => {
return e.touches ? e.touches[0].clientX : e.clientX;
}, []);
// 鼠标/触摸事件处理
const handleStart = useCallback(
(e) => {
if (isVerified) return;
e.preventDefault();
setIsDragging(true);
setStartX(getClientX(e) - sliderPosition);
setShowError(false);
},
[isVerified, sliderPosition, getClientX]
);
const handleMove = useCallback(
(e) => {
if (!isDragging || isVerified) return;
e.preventDefault();
const container = containerRef.current;
if (!container) return;
const containerWidth = container.offsetWidth;
const sliderWidth = 32;
const maxPosition = containerWidth - sliderWidth;
let newPosition = getClientX(e) - startX;
newPosition = Math.max(0, Math.min(newPosition, maxPosition));
setSliderPosition(newPosition);
},
[isDragging, isVerified, startX, getClientX]
);
const handleEnd = useCallback(() => {
if (!isDragging || isVerified) return;
setIsDragging(false);
// 验证滑块位置
const tolerance = 10; // 允许的误差范围
const isCorrect = Math.abs(sliderPosition - targetPosition) <= tolerance;
if (isCorrect) {
setIsVerified(true);
setShowSuccess(true);
setTimeout(() => setShowSuccess(false), 2000);
} else {
setShowError(true);
// 重置滑块位置
setTimeout(() => {
setSliderPosition(0);
setShowError(false);
}, 1000);
}
}, [isDragging, isVerified, sliderPosition, targetPosition]);
// 重置验证
const handleReset = () => {
setIsVerified(false);
setIsDragging(false);
setSliderPosition(0);
setShowSuccess(false);
setShowError(false);
// 重新生成目标位置
const container = containerRef.current;
if (container) {
const containerWidth = container.offsetWidth;
const sliderWidth = 32;
const maxPosition = containerWidth - sliderWidth;
const randomPosition = Math.random() * (maxPosition - 50) + 25;
setTargetPosition(randomPosition);
}
};
// 添加全局鼠标/触摸事件监听
useEffect(() => {
if (isDragging) {
document.addEventListener("mousemove", handleMove);
document.addEventListener("mouseup", handleEnd);
document.addEventListener("touchmove", handleMove, { passive: false });
document.addEventListener("touchend", handleEnd);
return () => {
document.removeEventListener("mousemove", handleMove);
document.removeEventListener("mouseup", handleEnd);
document.removeEventListener("touchmove", handleMove);
document.removeEventListener("touchend", handleEnd);
};
}
}, [isDragging, handleMove, handleEnd]);
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-xl p-8 max-w-md w-full">
<h2 className="text-2xl font-bold text-gray-800 mb-6 text-center">
滑块验证
</h2>
<div className="mb-6">
<p className="text-gray-600 text-center mb-4">
{isVerified ? "✅ 验证成功!" : "请将滑块拖拽到正确位置完成验证"}
</p>
</div>
{/* 验证区域 */}
<div
ref={containerRef}
className="relative bg-gray-100 rounded-lg h-12 mb-6 overflow-hidden"
>
{/* 背景图片或图案 */}
<div className="absolute inset-0 bg-gradient-to-r from-blue-200 to-purple-200 opacity-30"></div>
{/* 目标位置指示器 */}
<div
className="absolute top-1 bottom-1 w-2 bg-green-600 rounded-lg shadow ring-2 ring-green-300 transition-opacity duration-300"
style={{
left: `${targetPosition}px`,
opacity: isVerified ? 0 : 1,
}}
></div>
{/* 滑块轨道 */}
<div
ref={trackRef}
className="absolute top-0 bottom-0 left-0 bg-blue-500 transition-all duration-300 ease-out"
style={{
width: `${sliderPosition}px`,
opacity: isVerified ? 0.8 : 0.6,
}}
></div>
{/* 滑块 */}
{!isVerified && (
<div
ref={sliderRef}
className={`absolute top-1 bottom-1 w-8 bg-white rounded-md shadow-lg cursor-pointer transition-all duration-200 ease-out flex items-center justify-center
${isDragging ? "shadow-xl scale-105" : ""}
hover:shadow-lg
${showError ? "animate-shake" : ""}
`}
style={{ left: `${sliderPosition}px` }}
onMouseDown={handleStart}
onTouchStart={handleStart}
>
<svg
className="w-4 h-4 text-gray-500"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</div>
)}
{/* 成功提示 */}
{showSuccess && (
<div className="absolute inset-0 bg-green-500 bg-opacity-20 flex items-center justify-center">
<div className="bg-white rounded-lg px-4 py-2 shadow-lg">
<span className="text-green-600 font-semibold">验证成功!</span>
</div>
</div>
)}
{/* 错误提示 */}
{showError && (
<div className="absolute inset-0 bg-red-500 bg-opacity-20 flex items-center justify-center">
<div className="bg-white rounded-lg px-4 py-2 shadow-lg">
<span className="text-red-600 font-semibold">
位置错误,请重试
</span>
</div>
</div>
)}
</div>
{/* 操作按钮 */}
<div className="flex gap-4">
<button
onClick={handleReset}
className="flex-1 bg-gray-500 hover:bg-gray-600 text-white font-semibold py-3 px-6 rounded-lg transition-colors duration-200"
>
重置验证
</button>
{isVerified && (
<button
onClick={() => alert("验证通过,可以继续操作!")}
className="flex-1 bg-green-500 hover:bg-green-600 text-white font-semibold py-3 px-6 rounded-lg transition-colors duration-200"
>
继续
</button>
)}
</div>
{/* 状态指示器 */}
<div className="mt-4 text-center">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full text-sm">
{isVerified ? (
<>
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="text-green-600">已验证</span>
</>
) : (
<>
<div className="w-2 h-2 bg-yellow-500 rounded-full animate-pulse"></div>
<span className="text-yellow-600">待验证</span>
</>
)}
</div>
</div>
</div>
</div>
);
}