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

html代码如下:
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 拖拽测试类
java
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
java
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
java
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();" +
"}");
}
}
三、执行效果如下:
