尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

playwright-拖拽验证码

playwright-拖拽验证码
📅 发布时间:2026/7/1 2:36:22

一、有如下一个拖拽验证码demo,实现自动拖拽

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> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #f0f2f5; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .captcha-container { background: #fff; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.1); padding: 24px; width: 360px; } .captcha-title { font-size: 14px; color: #333; margin-bottom: 16px; font-weight: 500; } .image-wrapper { position: relative; width: 100%; height: 200px; background: #e8e8e8; border-radius: 4px; overflow: hidden; margin-bottom: 16px; user-select: none; } .image-wrapper img { width: 100%; height: 100%; object-fit: cover; display: block; } /* 缺口 */ .gap { position: absolute; width: 50px; height: 50px; border-radius: 4px; pointer-events: none; box-shadow: 0 0 0 2px rgba(255,255,255,0.7), inset 0 0 0 2px rgba(255,255,255,0.7); } /* 滑块 */ .slider-piece { position: absolute; width: 50px; height: 50px; border-radius: 4px; top: 0; left: 0; cursor: grab; box-shadow: 0 0 0 2px #409eff, inset 0 0 0 2px #409eff; background: rgba(64,158,255,0.3); z-index: 10; transition: left .05s linear; } .slider-piece.dragging { cursor: grabbing; } /* 底部滑轨 */ .slider-track { position: relative; width: 100%; height: 40px; background: #e8e8e8; border-radius: 4px; margin-bottom: 12px; } .slider-track-bg { position: absolute; top: 0; left: 0; bottom: 0; width: 0; background: linear-gradient(90deg, #409eff, #79bbff); border-radius: 4px 0 0 4px; transition: width .05s linear; } .slider-track-bg.success { background: linear-gradient(90deg, #67c23a, #95d475); width: 100% !important; border-radius: 4px; transition: background .3s, border-radius .3s; } .slider-track-bg.fail { background: linear-gradient(90deg, #f56c6c, #f89898); width: 100% !important; border-radius: 4px; transition: background .3s, border-radius .3s; } .slider-btn { position: absolute; top: -4px; left: 0; width: 48px; height: 48px; background: #fff; border: 1px solid #d9d9d9; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); cursor: grab; display: flex; align-items: center; justify-content: center; z-index: 20; transition: box-shadow .2s; } .slider-btn:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.15); } .slider-btn:active { cursor: grabbing; } .slider-btn .arrow { display: inline-block; width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 8px solid #999; transition: border-top-color .2s; } .slider-btn.active .arrow { border-top-color: #409eff; } .slider-track.success .slider-btn { background: #67c23a; border-color: #67c23a; } .slider-track.success .slider-btn .arrow { border-top-color: #fff; } .slider-track.fail .slider-btn { background: #f56c6c; border-color: #f56c6c; } .slider-track.fail .slider-btn .arrow { border-top-color: #fff; } .hint { font-size: 12px; color: #999; text-align: center; min-height: 18px; line-height: 18px; transition: color .2s; } .hint.success { color: #67c23a; } .hint.fail { color: #f56c6c; } </style> </head> <body> <div class="captcha-container"> <div class="captcha-title">安全验证</div> <div class="image-wrapper" id="imageWrapper"> <div class="gap" id="gap"></div> <div class="slider-piece" id="sliderPiece"></div> </div> <div class="slider-track" id="sliderTrack"> <div class="slider-track-bg" id="sliderTrackBg"></div> <div class="slider-btn" id="sliderBtn"> <span class="arrow"></span> </div> </div> <div class="hint" id="hint">请按住滑块,拖拽到缺口处</div> </div> <script> (function () { const TRACK_WIDTH = 310; // 滑轨可拖动像素 const GAP_SIZE = 50; const TOLERANCE = 5; // 允许误差像素 const wrapper = document.getElementById('imageWrapper'); const gap = document.getElementById('gap'); const sliderPiece = document.getElementById('sliderPiece'); const track = document.getElementById('sliderTrack'); const bg = document.getElementById('sliderTrackBg'); const btn = document.getElementById('sliderBtn'); const hint = document.getElementById('hint'); let gapX = 0; // 缺口 left let isDragging = false; let startX = 0; let currentX = 0; let verified = false; /* ---- 工具 ---- */ function rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } /* ---- 重置缺口与滑块位置 ---- */ function resetChallenge() { const wrapperW = wrapper.clientWidth; gapX = rand(GAP_SIZE, wrapperW - GAP_SIZE * 2); // 留出左右余量 gap.style.left = gapX + 'px'; gap.style.top = rand(0, wrapper.clientHeight - GAP_SIZE) + 'px'; // 滑块 piece 与缺口同 Y sliderPiece.style.top = gap.style.top; sliderPiece.style.left = '0px'; // 复位 UI btn.style.left = '0px'; bg.style.width = '0px'; bg.className = 'slider-track-bg'; track.className = 'slider-track'; btn.className = 'slider-btn'; hint.textContent = '请按住滑块,拖拽到缺口处'; hint.className = 'hint'; verified = false; currentX = 0; } /* ---- 验证 ---- */ function verify(offsetX) { const diff = Math.abs(offsetX - gapX); const passed = diff <= TOLERANCE; if (passed) { bg.className = 'slider-track-bg success'; track.className = 'slider-track success'; btn.className = 'slider-btn'; hint.textContent = '验证通过'; hint.className = 'hint success'; // 滑块 piece 同步到最终位置 sliderPiece.style.left = gapX + 'px'; verified = true; } else { bg.className = 'slider-track-bg fail'; track.className = 'slider-track fail'; btn.className = 'slider-btn'; hint.textContent = '验证失败,请重试'; hint.className = 'hint fail'; // 弹回 setTimeout(resetChallenge, 800); } } /* ---- 拖动逻辑 ---- */ function onPointerDown(e) { if (verified) return; isDragging = true; startX = e.clientX - currentX; btn.classList.add('active'); btn.setPointerCapture(e.pointerId); } function onPointerMove(e) { if (!isDragging) return; let offset = e.clientX - startX; offset = Math.max(0, Math.min(offset, TRACK_WIDTH)); currentX = offset; btn.style.left = offset + 'px'; bg.style.width = offset + 'px'; sliderPiece.style.left = offset + 'px'; } function onPointerUp(e) { if (!isDragging) return; isDragging = false; btn.classList.remove('active'); btn.releasePointerCapture(e.pointerId); if (!verified) verify(currentX); } /* ---- 事件绑定 ---- */ btn.addEventListener('pointerdown', onPointerDown); btn.addEventListener('pointermove', onPointerMove); btn.addEventListener('pointerup', onPointerUp); btn.addEventListener('pointercancel', onPointerUp); /* ---- 阻止页面选中/拖拽图片等默认行为 ---- */ wrapper.addEventListener('dragstart', e => e.preventDefault()); /* ---- 初始化 ---- */ resetChallenge(); // 演示用:点击图片随机重置 wrapper.addEventListener('click', () => { if (verified) resetChallenge(); }); })(); </script> </body> </html>

二、playwright 拖拽测试类

import com.microsoft.playwright.*; import com.microsoft.playwright.options.BoundingBox; import util.CaptchaSolver; import util.MouseTracker; public class TestCaptcha { public static void main(String[] args) { try (Playwright playwright = Playwright.create()) { Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false)); Page page = browser.newPage(); //打开测试页面 page.navigate("file:///E:/OPENCODE/captcha.html"); CaptchaSolver.solveSlider(page); //停2秒 page.waitForTimeout(10000); browser.close(); } } }

工具类:

CaptchaSolver
package util; import com.microsoft.playwright.Page; import com.microsoft.playwright.options.BoundingBox; public class CaptchaSolver { public static void solveSlider(Page page) { String leftStr = page.evaluate("document.getElementById('gap').style.left").toString(); double targetX = Double.parseDouble(leftStr.replace("px", "")); BoundingBox btnBox = page.locator("#sliderBtn").boundingBox(); if (btnBox == null) throw new RuntimeException("Slider button not found"); double fromX = btnBox.x + btnBox.width / 2; double toX = btnBox.x + btnBox.width / 2 + targetX; double y = btnBox.y + btnBox.height / 2; int steps = (int) Math.max(Math.abs(toX - fromX) / 2, 10); steps = Math.min(steps, 30); MouseTracker.drag(page, fromX, y, toX, y, steps); } }

工具类

MouseTracker
package util; import com.microsoft.playwright.Locator; import com.microsoft.playwright.Page; import com.microsoft.playwright.options.BoundingBox; public class MouseTracker { public static void inject(Page page) { page.evaluate("() => {" + "const dot = document.createElement('div');" + "dot.id = '__pw_mouse_tracker__';" + "dot.style.cssText = 'position:fixed;width:10px;height:10px;" + "background:red;border-radius:50%;z-index:99999;" + "pointer-events:none;transform:translate(-50%,-50%)';" + "document.body.appendChild(dot);" + "}"); } public static void moveTo(Page page, double x, double y) { page.evaluate("(x, y) => {" + "const d = document.getElementById('__pw_mouse_tracker__');" + "if (d) { d.style.left = x + 'px'; d.style.top = y + 'px'; }" + "}"); page.mouse().move(x, y); } public static void drag(Page page, double fromX, double fromY, double toX, double toY, int steps) { moveTo(page, fromX, fromY); page.mouse().down(); for (int i = 1; i <= steps; i++) { double x = fromX + (toX - fromX) * i / steps; double y = fromY + (toY - fromY) * i / steps; moveTo(page, x, y); } page.mouse().up(); } public static void dragSlider(Page page, String selector, double targetPercent) { Locator slider = page.locator(selector); BoundingBox box = slider.boundingBox(); if (box == null) throw new RuntimeException("Element not found or not visible: " + selector); double fromX = box.x + box.width * 0.5; double toX = box.x + box.width * targetPercent; double y = box.y + box.height / 2; drag(page, fromX, y, toX, y, 10); } public static void remove(Page page) { page.evaluate("() => {" + "document.getElementById('__pw_mouse_tracker__')?.remove();" + "}"); } }

三、执行效果如下:

相关新闻

  • 修改很简单,但网上讲这点的文档不多,因此多记一笔。另外基于out_ptr会临时转移所有权这点来看,共享所有权模型的std::shared_ptr其实并不适合使用out_ptr,虽然标准没有禁止甚至还要
  • 【课程设计/毕业设计】基于 SpringBoot 的农业设备销售订单管理系统的设计与实现 基于 SpringBoot 的智慧农机综合服务管理系统【附源码、数据库、万字文档】
  • 【Springboot毕设全套源码+文档】基于Java+springboot电缆行业生产管理系统的设计与实现(丰富项目+远程调试+讲解+定制)

最新新闻

  • 从亚麻布到汽车音响:为什么喇叭音盆材料会影响声音?
  • 本地 AI 自动化工具 OpenClaw 部署全流程,附常见故障修复(含安装包)
  • 圆满收官|VeryCloud亮相2026亚马逊云科技中国峰会,AI实践获行业积极反馈
  • 卡尔曼滤波在桥区船舶航行轨迹预判中的工程落地实践
  • 接纳孩子的平凡,是父母最高级的通透
  • AI Agent 实战部署指南:从核心能力到接口测试的完整流程

日新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

周新闻

  • Windows字体自定义终极方案:No!! MeiryoUI完全指南
  • Deepin Boot Maker:告别命令行,3分钟制作Linux启动盘的智能解决方案
  • Plain Craft Launcher 2:重新定义你的Minecraft游戏体验

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号