news 2026/5/6 19:50:51

纯CSS+JS实现滑动拼图验证码:从零到一完整复刻(附源码)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
纯CSS+JS实现滑动拼图验证码:从零到一完整复刻(附源码)

纯CSS+JS实现滑动拼图验证码:从零到一完整复刻(附源码)

在个人项目或后台管理系统中,验证码是防止自动化攻击的重要手段。滑动拼图验证码因其直观的交互方式和良好的用户体验,成为许多开发者的首选。本文将带你从零开始,仅用纯CSS和JavaScript实现一个轻量级的滑动拼图验证码,无需依赖任何第三方库。

1. 基础结构与样式设计

首先,我们需要构建验证码的基本HTML结构。这个结构包含三个主要部分:验证区域、拼图块和滑动条。

<div class="captcha-container"> <div class="puzzle-area"> <div class="puzzle-hole"></div> <div class="puzzle-piece"></div> </div> <div class="slider-container"> <div class="slider-button"></div> <div class="slider-text">拖动滑块完成拼图验证</div> </div> </div>

接下来是CSS样式的设计,这里有几个关键点需要注意:

  • 相对定位与绝对定位的配合:验证区域使用相对定位,拼图块和缺口使用绝对定位
  • 背景图片的处理:拼图块需要继承验证区域的背景,但显示位置不同
  • 滑动条的交互设计:滑块需要有明显的视觉反馈
.captcha-container { width: 350px; margin: 50px auto; font-family: 'Segoe UI', sans-serif; } .puzzle-area { position: relative; height: 200px; background-image: url('background.jpg'); background-size: cover; border-radius: 8px; overflow: hidden; } .puzzle-hole { position: absolute; width: 50px; height: 50px; background: rgba(0, 0, 0, 0.3); border: 2px dashed #fff; border-radius: 4px; } .puzzle-piece { position: absolute; width: 50px; height: 50px; background-image: inherit; background-size: 350px 200px; border: 2px solid #fff; border-radius: 4px; cursor: grab; } .slider-container { height: 50px; background: #f5f5f5; margin-top: 15px; border-radius: 25px; position: relative; } .slider-button { width: 50px; height: 50px; background: #4CAF50; border-radius: 50%; position: absolute; left: 0; top: 0; cursor: pointer; z-index: 2; transition: background 0.3s; } .slider-button:active { background: #3e8e41; } .slider-text { position: absolute; width: 100%; text-align: center; line-height: 50px; color: #888; font-size: 14px; user-select: none; }

2. JavaScript交互逻辑实现

交互逻辑是滑动验证码的核心,主要包括以下几个功能点:

  1. 随机生成拼图位置
  2. 处理鼠标/触摸事件
  3. 计算滑动距离
  4. 验证结果判断
// 获取DOM元素 const puzzleArea = document.querySelector('.puzzle-area'); const puzzleHole = document.querySelector('.puzzle-hole'); const puzzlePiece = document.querySelector('.puzzle-piece'); const sliderButton = document.querySelector('.slider-button'); const sliderContainer = document.querySelector('.slider-container'); // 随机生成拼图位置 const maxX = puzzleArea.offsetWidth - puzzleHole.offsetWidth; const maxY = puzzleArea.offsetHeight - puzzleHole.offsetHeight; const randomX = Math.floor(Math.random() * maxX); const randomY = Math.floor(Math.floor(Math.random() * maxY)); // 设置拼图缺口和拼图块的位置 puzzleHole.style.left = `${randomX}px`; puzzleHole.style.top = `${randomY}px`; puzzlePiece.style.backgroundPosition = `-${randomX}px -${randomY}px`; puzzlePiece.style.left = '10px'; puzzlePiece.style.top = `${randomY}px`; // 初始化变量 let isDragging = false; let startX = 0; let currentX = 0; // 鼠标按下事件 sliderButton.addEventListener('mousedown', (e) => { isDragging = true; startX = e.clientX; sliderButton.style.transition = 'none'; e.preventDefault(); }); // 鼠标移动事件 document.addEventListener('mousemove', (e) => { if (!isDragging) return; const containerRect = sliderContainer.getBoundingClientRect(); const buttonRect = sliderButton.getBoundingClientRect(); currentX = e.clientX - startX; // 限制滑块移动范围 if (currentX < 0) currentX = 0; if (currentX > containerRect.width - buttonRect.width) { currentX = containerRect.width - buttonRect.width; } // 更新滑块位置 sliderButton.style.transform = `translateX(${currentX}px)`; // 同步移动拼图块 const puzzleMaxX = puzzleArea.offsetWidth - puzzlePiece.offsetWidth - 10; const puzzleX = (currentX / (containerRect.width - buttonRect.width)) * puzzleMaxX; puzzlePiece.style.left = `${10 + puzzleX}px`; }); // 鼠标释放事件 document.addEventListener('mouseup', () => { if (!isDragging) return; isDragging = false; // 验证结果 const tolerance = 5; // 允许的误差范围 const expectedX = randomX - 10; // 调整偏移量 const actualX = parseInt(puzzlePiece.style.left); if (Math.abs(actualX - expectedX) <= tolerance) { // 验证成功 sliderButton.style.backgroundColor = '#4CAF50'; sliderButton.style.transition = 'background 0.3s'; setTimeout(() => { alert('验证成功!'); resetCaptcha(); }, 300); } else { // 验证失败 sliderButton.style.backgroundColor = '#f44336'; setTimeout(() => { resetCaptcha(); }, 500); } }); // 重置验证码 function resetCaptcha() { sliderButton.style.transform = 'translateX(0)'; sliderButton.style.backgroundColor = '#4CAF50'; puzzlePiece.style.left = '10px'; currentX = 0; // 重新生成随机位置 const newX = Math.floor(Math.random() * maxX); const newY = Math.floor(Math.random() * maxY); puzzleHole.style.left = `${newX}px`; puzzleHole.style.top = `${newY}px`; puzzlePiece.style.backgroundPosition = `-${newX}px -${newY}px`; puzzlePiece.style.top = `${newY}px`; }

3. 移动端适配与优化

为了让滑动验证码在移动设备上也能良好工作,我们需要添加触摸事件的支持:

// 触摸开始事件 sliderButton.addEventListener('touchstart', (e) => { isDragging = true; startX = e.touches[0].clientX; sliderButton.style.transition = 'none'; e.preventDefault(); }); // 触摸移动事件 document.addEventListener('touchmove', (e) => { if (!isDragging) return; const containerRect = sliderContainer.getBoundingClientRect(); const buttonRect = sliderButton.getBoundingClientRect(); currentX = e.touches[0].clientX - startX; if (currentX < 0) currentX = 0; if (currentX > containerRect.width - buttonRect.width) { currentX = containerRect.width - buttonRect.width; } sliderButton.style.transform = `translateX(${currentX}px)`; const puzzleMaxX = puzzleArea.offsetWidth - puzzlePiece.offsetWidth - 10; const puzzleX = (currentX / (containerRect.width - buttonRect.width)) * puzzleMaxX; puzzlePiece.style.left = `${10 + puzzleX}px`; e.preventDefault(); }); // 触摸结束事件 document.addEventListener('touchend', () => { if (!isDragging) return; isDragging = false; // 验证逻辑与鼠标版本相同 // ... });

移动端优化还包括以下几点:

  • 增加触摸反馈:在触摸时改变滑块颜色或大小
  • 防止页面滚动:在滑动验证码时阻止页面滚动
  • 响应式设计:根据屏幕大小调整验证码尺寸
@media (max-width: 480px) { .captcha-container { width: 90%; margin: 30px auto; } .puzzle-area { height: 150px; } .slider-container { height: 40px; } .slider-button { width: 40px; height: 40px; } .slider-text { line-height: 40px; font-size: 12px; } }

4. 安全增强与防破解策略

虽然前端验证不能完全阻止恶意行为,但我们可以增加一些基本的安全措施:

  1. 随机性增强
    • 每次验证时随机生成拼图形状
    • 随机设置验证容差范围
    • 随机改变背景图片
// 增强的随机位置生成 function getRandomPosition() { const minX = 50; // 最小X位置 const minY = 30; // 最小Y位置 const maxX = puzzleArea.offsetWidth - puzzleHole.offsetWidth - 50; const maxY = puzzleArea.offsetHeight - puzzleHole.offsetHeight - 30; return { x: minX + Math.floor(Math.random() * (maxX - minX)), y: minY + Math.floor(Math.random() * (maxY - minY)) }; } // 随机拼图形状 function randomizePuzzleShape() { const shapes = ['square', 'circle', 'triangle']; const selectedShape = shapes[Math.floor(Math.random() * shapes.length)]; puzzleHole.classList.remove('square', 'circle', 'triangle'); puzzlePiece.classList.remove('square', 'circle', 'triangle'); puzzleHole.classList.add(selectedShape); puzzlePiece.classList.add(selectedShape); } // 随机背景图片 function randomizeBackground() { const images = [ 'background1.jpg', 'background2.jpg', 'background3.jpg' ]; const selectedImage = images[Math.floor(Math.random() * images.length)]; puzzleArea.style.backgroundImage = `url(${selectedImage})`; }
  1. 行为检测
    • 检测滑动速度是否异常
    • 检测是否直接设置滑块位置
    • 记录失败次数并限制尝试
let lastMoveTime = 0; let moveCount = 0; let failedAttempts = 0; document.addEventListener('mousemove', (e) => { if (!isDragging) return; const now = Date.now(); const elapsed = now - lastMoveTime; lastMoveTime = now; // 检测异常快速的滑动 if (elapsed < 10 && moveCount > 5) { console.log('异常滑动行为检测'); resetCaptcha(); isDragging = false; return; } moveCount++; // 其余滑动逻辑... }); // 在验证失败时增加计数 function handleFailedAttempt() { failedAttempts++; if (failedAttempts >= 3) { // 显示更复杂的验证方式或暂时禁用 alert('多次验证失败,请稍后再试'); sliderButton.style.pointerEvents = 'none'; setTimeout(() => { sliderButton.style.pointerEvents = 'auto'; failedAttempts = 0; }, 30000); } }
  1. 后端验证
    • 即使前端验证通过,仍需在后端进行二次验证
    • 可以发送滑动轨迹数据到后端分析
    • 使用一次性token防止重放攻击
// 模拟发送验证数据到后端 function sendVerificationToServer(success) { const verificationData = { timestamp: Date.now(), success: success, position: { x: randomX, y: randomY }, actualPosition: parseInt(puzzlePiece.style.left), slideDuration: Date.now() - startTime, failedAttempts: failedAttempts }; // 实际项目中这里应该是AJAX请求 console.log('发送验证数据:', verificationData); // 生成新的token generateNewToken(); } let currentToken = generateToken(); function generateToken() { return 'token_' + Math.random().toString(36).substr(2, 16); }

5. 高级功能扩展

基础功能实现后,我们可以考虑添加一些增强用户体验和功能性的特性:

  1. 多主题支持
    • 明亮/暗黑主题切换
    • 自定义颜色方案
/* 暗黑主题 */ .captcha-container.dark { background-color: #333; color: #fff; } .captcha-container.dark .puzzle-area { border: 1px solid #555; } .captcha-container.dark .slider-container { background: #444; } .captcha-container.dark .slider-text { color: #aaa; } /* 主题切换按钮 */ .theme-toggle { position: absolute; top: 10px; right: 10px; background: none; border: none; cursor: pointer; font-size: 20px; }
  1. 可访问性改进
    • 键盘导航支持
    • ARIA属性添加
    • 高对比度模式
// 键盘支持 sliderButton.addEventListener('keydown', (e) => { if (e.key === 'ArrowRight') { currentX += 5; if (currentX > sliderContainer.offsetWidth - sliderButton.offsetWidth) { currentX = sliderContainer.offsetWidth - sliderButton.offsetWidth; } sliderButton.style.transform = `translateX(${currentX}px)`; updatePuzzlePosition(); } else if (e.key === 'ArrowLeft') { currentX -= 5; if (currentX < 0) currentX = 0; sliderButton.style.transform = `translateX(${currentX}px)`; updatePuzzlePosition(); } }); function updatePuzzlePosition() { const puzzleMaxX = puzzleArea.offsetWidth - puzzlePiece.offsetWidth - 10; const puzzleX = (currentX / (sliderContainer.offsetWidth - sliderButton.offsetWidth)) * puzzleMaxX; puzzlePiece.style.left = `${10 + puzzleX}px`; }
  1. 性能优化
    • 减少重绘和回流
    • 使用transform代替left/top
    • 图片懒加载
// 使用requestAnimationFrame优化动画 function smoothSlide() { if (!isDragging) return; requestAnimationFrame(() => { sliderButton.style.transform = `translateX(${currentX}px)`; updatePuzzlePosition(); }); } // 图片预加载 function preloadImages() { const images = [ 'background1.jpg', 'background2.jpg', 'background3.jpg' ]; images.forEach(img => { new Image().src = img; }); } // 初始化时预加载 window.addEventListener('DOMContentLoaded', preloadImages);
  1. 国际化支持
    • 多语言提示
    • 右到左(RTL)布局支持
const i18n = { en: { instruction: "Slide to complete the puzzle", success: "Verification successful", fail: "Verification failed, please try again" }, zh: { instruction: "拖动滑块完成拼图验证", success: "验证成功", fail: "验证失败,请重试" }, ar: { instruction: "اسحب لإكمال اللغز", success: "تم التحقق بنجاح", fail: "فشل التحقق، يرجى المحاولة مرة أخرى" } }; function setLanguage(lang) { const strings = i18n[lang] || i18n.en; document.querySelector('.slider-text').textContent = strings.instruction; // 更新其他文本元素... } // 检测用户语言 const userLang = navigator.language.split('-')[0]; setLanguage(userLang);

6. 实际应用中的问题与解决方案

在实际项目中实现滑动验证码时,可能会遇到以下常见问题:

  1. 跨浏览器兼容性问题
问题解决方案
IE不支持transform使用left属性并添加IE条件注释
Safari触摸事件延迟添加touch-action: manipulation样式
Firefox鼠标事件坐标差异使用event.pageX而非clientX
  1. 性能瓶颈
  • 问题:频繁的DOM操作导致卡顿
  • 解决方案
    • 使用documentFragment批量操作
    • 节流鼠标移动事件
    • 使用CSS will-change属性提示浏览器优化
// 节流函数实现 function throttle(func, limit) { let lastFunc; let lastRan; return function() { const context = this; const args = arguments; if (!lastRan) { func.apply(context, args); lastRan = Date.now(); } else { clearTimeout(lastFunc); lastFunc = setTimeout(function() { if ((Date.now() - lastRan) >= limit) { func.apply(context, args); lastRan = Date.now(); } }, limit - (Date.now() - lastRan)); } }; } // 应用节流 document.addEventListener('mousemove', throttle(handleMouseMove, 16));
  1. 验证码被自动化工具破解
  • 问题:简单的滑动验证容易被自动化脚本模拟
  • 增强措施
    • 添加滑动轨迹分析
    • 要求先完成人机验证(如点击特定区域)
    • 结合其他验证因素(如IP信誉、行为分析)
// 记录滑动轨迹 const slidePath = []; document.addEventListener('mousemove', (e) => { if (!isDragging) return; slidePath.push({ x: e.clientX, y: e.clientY, t: Date.now() }); // 保留最近50个点 if (slidePath.length > 50) { slidePath.shift(); } }); function analyzeSlidePath() { if (slidePath.length < 10) return false; // 计算平均速度 const distance = Math.sqrt( Math.pow(slidePath[slidePath.length-1].x - slidePath[0].x, 2) + Math.pow(slidePath[slidePath.length-1].y - slidePath[0].y, 2) ); const duration = (slidePath[slidePath.length-1].t - slidePath[0].t) / 1000; const velocity = distance / duration; // 人类滑动速度通常在100-1000px/s之间 return velocity > 100 && velocity < 1000; }
  1. 移动端特殊问题

提示:在移动设备上,触摸事件和鼠标事件的触发顺序不同,可能导致意外行为。建议统一使用触摸事件处理移动端交互,并通过特征检测自动选择适当的事件类型。

// 检测是否触摸设备 const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; // 根据设备类型绑定事件 if (isTouchDevice) { sliderButton.addEventListener('touchstart', handleStart); document.addEventListener('touchmove', handleMove); document.addEventListener('touchend', handleEnd); } else { sliderButton.addEventListener('mousedown', handleStart); document.addEventListener('mousemove', handleMove); document.addEventListener('mouseup', handleEnd); }

7. 完整实现与源码结构

一个完整的滑动拼图验证码实现应该包含以下文件结构:

/captcha ├── index.html # 主HTML文件 ├── style.css # 样式文件 ├── script.js # 主逻辑文件 ├── images/ # 图片资源目录 │ ├── bg1.jpg │ ├── bg2.jpg │ └── bg3.jpg └── README.md # 使用说明

index.html 完整代码

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>滑动拼图验证码</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="captcha-container"> <button class="theme-toggle">🌓</button> <div class="puzzle-area"> <div class="puzzle-hole square"></div> <div class="puzzle-piece square"></div> </div> <div class="slider-container"> <div class="slider-button" tabindex="0"></div> <div class="slider-text">拖动滑块完成拼图验证</div> </div> </div> <script src="script.js"></script> </body> </html>

style.css 核心部分

/* 基础样式重置 */ * { box-sizing: border-box; margin: 0; padding: 0; } body { display: flex; justify-content: center; align-items: center; min-height: 100vh; background-color: #f5f5f5; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; } .captcha-container { width: 350px; padding: 20px; background: #fff; border-radius: 12px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); position: relative; } .theme-toggle { position: absolute; top: 15px; right: 15px; background: none; border: none; font-size: 1.2em; cursor: pointer; opacity: 0.7; transition: opacity 0.3s; } .theme-toggle:hover { opacity: 1; } /* 暗黑主题样式 */ .captcha-container.dark { background: #333; color: #fff; } .captcha-container.dark .puzzle-area { border-color: #555; } .captcha-container.dark .slider-container { background: #444; } .captcha-container.dark .slider-text { color: #aaa; } /* 拼图区域样式 */ .puzzle-area { position: relative; height: 200px; background-image: url('images/bg1.jpg'); background-size: cover; background-position: center; border-radius: 8px; border: 1px solid #ddd; overflow: hidden; } /* 拼图缺口样式 */ .puzzle-hole { position: absolute; width: 50px; height: 50px; background: rgba(0, 0, 0, 0.3); border: 2px dashed #fff; cursor: pointer; } /* 拼图形状变体 */ .puzzle-hole.square { border-radius: 4px; } .puzzle-hole.circle { border-radius: 50%; } .puzzle-hole.triangle { clip-path: polygon(50% 0%, 0% 100%, 100% 100%); background: rgba(0, 0, 0, 0.4); } /* 拼图块样式 */ .puzzle-piece { position: absolute; width: 50px; height: 50px; background-image: inherit; background-size: 350px 200px; border: 2px solid #fff; cursor: grab; transition: left 0.1s; } .puzzle-piece:active { cursor: grabbing; } .puzzle-piece.square { border-radius: 4px; } .puzzle-piece.circle { border-radius: 50%; } .puzzle-piece.triangle { clip-path: polygon(50% 0%, 0% 100%, 100% 100%); } /* 滑动条容器 */ .slider-container { height: 50px; background: #f0f0f0; margin-top: 15px; border-radius: 25px; position: relative; overflow: hidden; } /* 滑块按钮 */ .slider-button { width: 50px; height: 50px; background: #4CAF50; border-radius: 50%; position: absolute; left: 0; top: 0; cursor: pointer; z-index: 10; transition: background 0.3s, transform 0.1s; outline: none; } .slider-button:hover { background: #45a049; } .slider-button:active { background: #3e8e41; } /* 滑动提示文字 */ .slider-text { position: absolute; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: #666; font-size: 14px; user-select: none; pointer-events: none; } /* 响应式设计 */ @media (max-width: 480px) { .captcha-container { width: 90%; padding: 15px; } .puzzle-area { height: 150px; } .slider-container { height: 40px; } .slider-button { width: 40px; height: 40px; } .slider-text { font-size: 12px; } }

script.js 核心逻辑

// DOM元素引用 const captchaContainer = document.querySelector('.captcha-container'); const puzzleArea = document.querySelector('.puzzle-area'); const puzzleHole = document.querySelector('.puzzle-hole'); const puzzlePiece = document.querySelector('.puzzle-piece'); const sliderButton = document.querySelector('.slider-button'); const sliderContainer = document.querySelector('.slider-container'); const themeToggle = document.querySelector('.theme-toggle'); // 状态变量 let isDragging = false; let startX = 0; let currentX = 0; let randomX = 0; let randomY = 0; let failedAttempts = 0; let startTime = 0; const slidePath = []; // 初始化验证码 function initCaptcha() { // 随机生成拼图位置 const position = getRandomPosition(); randomX = position.x; randomY = position.y; // 设置拼图缺口位置 puzzleHole.style.left = `${randomX}px`; puzzleHole.style.top = `${randomY}px`; // 设置拼图块初始位置和背景 puzzlePiece.style.backgroundPosition = `-${randomX}px -${randomY}px`; puzzlePiece.style.left = '10px'; puzzlePiece.style.top = `${randomY}px`; // 随机选择拼图形状和背景 randomizePuzzleShape(); randomizeBackground(); // 重置状态 isDragging = false; currentX = 0; sliderButton.style.transform = 'translateX(0)'; sliderButton.style.backgroundColor = '#4CAF50'; slidePath.length = 0; } // 获取随机位置 function getRandomPosition() { const minX = 50; const minY = 30; const maxX = puzzleArea.offsetWidth - puzzleHole.offsetWidth - 50; const maxY = puzzleArea.offsetHeight - puzzleHole.offsetHeight - 30; return { x: minX + Math.floor(Math.random() * (maxX - minX)), y: minY + Math.floor(Math.random() * (maxY - minY)) }; } // 随机拼图形状 function randomizePuzzleShape() { const shapes = ['square', 'circle', 'triangle']; const selectedShape = shapes[Math.floor(Math.random() * shapes.length)]; puzzleHole.className = 'puzzle-hole ' + selectedShape; puzzlePiece.className = 'puzzle-piece ' + selectedShape; } // 随机背景图片 function randomizeBackground() { const images = ['bg1.jpg', 'bg2.jpg', 'bg3.jpg']; const selectedImage = images[Math.floor(Math.random() * images.length)]; puzzleArea.style.backgroundImage = `url(images/${selectedImage})`; } // 开始拖动 function handleStart(e) { isDragging = true; startX = e.clientX || e.touches[0].clientX; startTime = Date.now(); sliderButton.style.transition = 'none'; slidePath.length = 0; // 记录初始点 slidePath.push({ x: startX, y: (e.clientY || e.touches[0].clientY), t: startTime }); e.preventDefault(); } // 处理拖动 function handleMove(e) { if (!isDragging) return; const clientX = e.clientX || e.touches[0].clientX; const clientY = e.clientY || e.touches[0].clientY; // 记录滑动路径 slidePath.push({ x: clientX, y: clientY, t: Date.now() }); // 保留最近50个点 if (slidePath.length > 50) { slidePath.shift(); } // 计算移动距离 currentX = clientX - startX; // 限制移动范围 const maxX = sliderContainer.offsetWidth - sliderButton.offsetWidth; if (currentX < 0) currentX = 0; if (currentX > maxX) currentX = maxX; // 更新滑块位置 sliderButton.style.transform = `translateX(${currentX}px)`; // 更新拼图块位置 updatePuzzlePosition(); e.preventDefault(); } // 更新拼图块位置 function updatePuzzlePosition() { const puzzleMaxX = puzzleArea.offsetWidth - puzzlePiece.offsetWidth - 10; const puzzleX = (currentX / (sliderContainer.offsetWidth - sliderButton.offsetWidth)) * puzzleMaxX; puzzlePiece.style.left = `${10 + puzzleX}px`; } // 结束拖动 function handleEnd() { if (!isDragging) return; isDragging = false; // 分析滑动行为 const isHuman = analyzeSlidePath(); // 验证结果 const tolerance = 5 + Math.floor(Math.random() * 5); // 随机容差 const expectedX = randomX - 10; const actualX = parseInt(puzzlePiece.style.left); const isPositionValid = Math.abs(actualX - expectedX) <= tolerance; if (isPositionValid && isHuman) { // 验证成功 handleSuccess(); } else { // 验证失败 handleFailure(); } } // 分析滑动路径 function analyzeSlidePath() { if (slidePath.length < 10) return false; // 计算速度变化 let speedChanges = 0; let lastSpeed = 0; for (let i = 1; i < slidePath.length; i++) { const dx = slidePath[i].x - slidePath[i-1].x; const dy = slidePath[i].y - slidePath[i-1].y; const dt = (slidePath[i].t - slidePath[i-1].t) || 1; const speed = Math.sqrt(dx*dx + dy*dy) / dt; if (i > 1 && Math.abs(speed - lastSpeed) > 2) { speedChanges++; } lastSpeed = speed; } // 人类滑动通常有较多速度变化 return speedChanges > slidePath.length * 0.3; } // 处理验证成功 function handleSuccess() { sliderButton.style.backgroundColor = '#4CAF50'; sliderButton.style.transition = 'background 0.3s'; // 显示成功状态 puzzleHole.style.borderColor = '#4CAF50'; puzzlePiece.style.borderColor = '#4CAF50'; setTimeout(() => { alert('验证成功!'); sendVerificationToServer(true); initCaptcha(); }, 300); } // 处理验证失败 function handleFailure() { sliderButton.style.backgroundColor = '#f44336'; failedAttempts++; // 显示失败状态 puzzleHole.style.borderColor = '#f44336'; puzzlePiece.style.borderColor = '#f44336'; // 根据失败次数采取不同措施 if (failedAttempts >= 3) { setTimeout(() => { alert('多次验证失败,请稍后再试'); sliderButton.style.pointerEvents = 'none'; setTimeout(() => { sliderButton.style.pointerEvents = 'auto'; failedAttempts = 0; initCaptcha(); }, 30000); }, 500); } else { setTimeout(() => { sendVerificationToServer(false); initCaptcha(); }, 500); } } // 发送验证结果到服务器 function sendVerificationToServer(success) { // 实际项目中这里应该是AJAX请求 console.log('验证结果:', { success: success, position: { x: randomX, y: randomY }, actualPosition: parseInt(puzzlePiece.style.left), slideDuration: Date.now() - startTime, pathPoints: slidePath.length, failedAttempts: failedAttempts, timestamp: Date.now() }); } // 主题切换 function toggleTheme() { captchaContainer.classList.toggle('dark'); localStorage.setItem('captchaTheme', captchaContainer.classList.contains('dark') ? 'dark' : 'light'); } // 检测设备类型 const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; // 事件绑定 function bindEvents() { if (isTouchDevice) { sliderButton.addEventListener('touchstart', handleStart); document.addEventListener('touchmove', handleMove); document.addEventListener('touchend', handleEnd); } else { sliderButton.addEventListener('mousedown', handleStart); document.addEventListener('mousemove', handleMove); document.addEventListener('mouseup', handleEnd); } // 键盘支持 sliderButton.addEventListener('keydown', (e) => {
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/6 19:49:36

RAGFlow 系列教程 第二十九课:性能优化与生产最佳实践

系列: RAGFlow v0.25.0 源码深度解析 作者: 耿雨飞 前置知识: 已完成第二十八课"Agent 工作流开发实战"的学习 导读 在前面的课程中,我们已经深入了解了 RAGFlow 的文档解析、分块、检索、LLM 集成、Agent 工作流等核心功能。然而,将 RAG 系统从"能用"推…

作者头像 李华
网站建设 2026/5/6 19:49:23

效率提升秘籍:利用快马AI智能批量处理未来免费正版图片素材

效率提升秘籍&#xff1a;利用快马AI智能批量处理未来免费正版图片素材 最近在准备一个设计项目时&#xff0c;遇到了图片素材管理的难题。随着"49正版图库免费2026"这类资源的出现&#xff0c;我们获取素材的方式正在发生革命性变化&#xff0c;但随之而来的管理问…

作者头像 李华
网站建设 2026/5/6 19:46:10

VRoidStudio汉化插件架构深度解析:构建可扩展的界面本地化方案

VRoidStudio汉化插件架构深度解析&#xff1a;构建可扩展的界面本地化方案 【免费下载链接】VRoidChinese VRoidStudio汉化插件 项目地址: https://gitcode.com/gh_mirrors/vr/VRoidChinese 在3D角色创作领域&#xff0c;VRoidStudio以其强大的功能和直观的操作界面赢得…

作者头像 李华
网站建设 2026/5/6 19:41:32

NBTExplorer终极指南:快速掌握Minecraft数据编辑的完整教程

NBTExplorer终极指南&#xff1a;快速掌握Minecraft数据编辑的完整教程 【免费下载链接】NBTExplorer A graphical NBT editor for all Minecraft NBT data sources 项目地址: https://gitcode.com/gh_mirrors/nb/NBTExplorer NBTExplorer是一款强大的开源Minecraft NBT…

作者头像 李华
网站建设 2026/5/6 19:37:41

告别图像模糊!利用GE/飞利浦超声设备优化浅表器官(甲状腺、乳腺)扫查的5个实战技巧

告别图像模糊&#xff01;GE/飞利浦超声设备浅表器官扫查优化实战指南 在甲状腺结节和乳腺肿块的超声检查中&#xff0c;图像质量直接关系到诊断的准确性。许多超声医师都遇到过这样的困扰&#xff1a;明明按照标准流程操作&#xff0c;得到的图像却总是差强人意——边缘模糊、…

作者头像 李华