xianyu-backend-java/captcha_control.html
2025-11-24 16:24:09 +08:00

572 lines
18 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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 {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 30px;
max-width: 900px;
width: 100%;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #333;
font-size: 28px;
margin-bottom: 10px;
}
.status {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 10px;
border-radius: 8px;
margin-bottom: 20px;
}
.status.connected {
background: #d4edda;
color: #155724;
}
.status.disconnected {
background: #f8d7da;
color: #721c24;
}
.status.completed {
background: #d1ecf1;
color: #0c5460;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.status.connected .status-indicator {
background: #28a745;
}
.status.disconnected .status-indicator {
background: #dc3545;
}
.status.completed .status-indicator {
background: #17a2b8;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.canvas-container {
position: relative;
background: #f8f9fa;
border: 2px solid #dee2e6;
border-radius: 8px;
overflow: hidden;
margin-bottom: 20px;
max-height: 600px;
display: flex;
justify-content: center;
align-items: center;
}
#captchaCanvas {
max-width: 100%;
display: block;
cursor: crosshair;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.info-panel {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.info-card {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.info-card h3 {
color: #666;
font-size: 12px;
text-transform: uppercase;
margin-bottom: 5px;
}
.info-card p {
color: #333;
font-size: 18px;
font-weight: bold;
}
.instructions {
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
}
.instructions h3 {
color: #856404;
margin-bottom: 10px;
font-size: 16px;
}
.instructions ol {
margin-left: 20px;
color: #856404;
}
.instructions li {
margin-bottom: 5px;
}
.session-input {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.session-input input {
flex: 1;
padding: 12px;
border: 2px solid #dee2e6;
border-radius: 8px;
font-size: 14px;
}
.session-input button {
padding: 12px 24px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: background 0.3s;
}
.session-input button:hover {
background: #5568d3;
}
.session-input button:disabled {
background: #ccc;
cursor: not-allowed;
}
.log {
background: #2d3748;
color: #a0aec0;
border-radius: 8px;
padding: 15px;
max-height: 200px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 12px;
}
.log-entry {
margin-bottom: 5px;
}
.log-entry.success {
color: #48bb78;
}
.log-entry.error {
color: #f56565;
}
.log-entry.info {
color: #4299e1;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎨 刮刮乐验证控制面板</h1>
</div>
<div id="status" class="status disconnected">
<div class="status-indicator"></div>
<span id="statusText">未连接</span>
</div>
<div class="session-input">
<input type="text" id="sessionIdInput" placeholder="输入会话ID通常是用户ID" />
<button id="connectBtn" onclick="connectSession()">连接</button>
</div>
<div class="instructions">
<h3>📋 使用说明</h3>
<ol>
<li>输入会话ID并点击"连接"按钮</li>
<li>等待页面截图加载</li>
<li>使用鼠标在滑块按钮上按下并拖动</li>
<li>释放鼠标完成验证</li>
</ol>
</div>
<div class="info-panel">
<div class="info-card">
<h3>会话ID</h3>
<p id="sessionIdDisplay">-</p>
</div>
<div class="info-card">
<h3>连接状态</h3>
<p id="connectionStatus">未连接</p>
</div>
<div class="info-card">
<h3>验证状态</h3>
<p id="verificationStatus">待处理</p>
</div>
</div>
<div class="canvas-container" id="canvasContainer">
<div class="loading" id="loading">
<div class="spinner"></div>
<p>加载中...</p>
</div>
<canvas id="captchaCanvas"></canvas>
</div>
<div class="log" id="log">
<div class="log-entry info">等待连接...</div>
</div>
</div>
<script>
let ws = null;
let currentSessionId = null;
let canvas = null;
let ctx = null;
let isMouseDown = false;
let lastMoveTime = 0;
let moveThrottle = 5; // 节流每10ms最多发送一次极致流畅
let captchaOffset = {x: 0, y: 0}; // 滑块区域在页面中的偏移量
// 初始化
document.addEventListener('DOMContentLoaded', () => {
canvas = document.getElementById('captchaCanvas');
ctx = canvas.getContext('2d');
// 检查是否为嵌入模式从URL参数或iframe检测
const urlParams = new URLSearchParams(window.location.search);
const isEmbed = urlParams.get('embed') === '1' || window.self !== window.top;
if (isEmbed) {
// 隐藏不必要的元素
document.querySelector('.header').style.display = 'none';
document.querySelector('.session-input').style.display = 'none';
document.querySelector('.instructions').style.display = 'none';
document.querySelector('.info-panel').style.display = 'none';
document.querySelector('.log').style.display = 'none';
document.getElementById('status').style.display = 'none';
// 调整容器样式
document.querySelector('.container').style.padding = '0';
document.querySelector('.container').style.boxShadow = 'none';
document.querySelector('.container').style.background = 'transparent';
document.body.style.padding = '0';
}
// 检查是否有预设的会话ID
if (window.INITIAL_SESSION_ID) {
document.getElementById('sessionIdInput').value = window.INITIAL_SESSION_ID;
connectSession();
}
// 鼠标事件
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
canvas.addEventListener('mouseleave', handleMouseUp);
});
function log(message, type = 'info') {
const logEl = document.getElementById('log');
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
const timestamp = new Date().toLocaleTimeString();
entry.textContent = `[${timestamp}] ${message}`;
logEl.appendChild(entry);
logEl.scrollTop = logEl.scrollHeight;
}
function updateStatus(status, text) {
const statusEl = document.getElementById('status');
const statusTextEl = document.getElementById('statusText');
statusEl.className = `status ${status}`;
statusTextEl.textContent = text;
document.getElementById('connectionStatus').textContent = text;
}
async function connectSession() {
const sessionId = document.getElementById('sessionIdInput').value.trim();
if (!sessionId) {
alert('请输入会话ID');
return;
}
currentSessionId = sessionId;
document.getElementById('sessionIdDisplay').textContent = sessionId;
document.getElementById('connectBtn').disabled = true;
log(`正在连接会话: ${sessionId}`, 'info');
try {
// 建立 WebSocket 连接
const wsUrl = `ws://${window.location.host}/api/captcha/ws/${sessionId}`;
log(`WebSocket URL: ${wsUrl}`, 'info');
ws = new WebSocket(wsUrl);
ws.onopen = () => {
log('WebSocket 连接成功', 'success');
updateStatus('connected', '已连接');
};
ws.onmessage = async (event) => {
const data = JSON.parse(event.data);
handleWebSocketMessage(data);
};
ws.onerror = (error) => {
log(`WebSocket 错误: ${error}`, 'error');
updateStatus('disconnected', '连接错误');
document.getElementById('connectBtn').disabled = false;
};
ws.onclose = () => {
log('WebSocket 连接关闭', 'info');
updateStatus('disconnected', '连接已关闭');
document.getElementById('connectBtn').disabled = false;
};
} catch (error) {
log(`连接失败: ${error}`, 'error');
updateStatus('disconnected', '连接失败');
document.getElementById('connectBtn').disabled = false;
}
}
function handleWebSocketMessage(data) {
const type = data.type;
if (type === 'session_info') {
log('收到会话信息', 'success');
// 保存验证码容器偏移量
if (data.captcha_info && data.captcha_info.x !== undefined) {
captchaOffset.x = Math.max(0, data.captcha_info.x - 10);
captchaOffset.y = Math.max(0, data.captcha_info.y - 10);
log(`验证码容器偏移: (${captchaOffset.x}, ${captchaOffset.y})`, 'info');
log(`容器大小: ${data.captcha_info.width}x${data.captcha_info.height}`, 'info');
}
displayScreenshot(data.screenshot);
}
else if (type === 'screenshot_update') {
displayScreenshot(data.screenshot);
// 如果是在等待验证结果时收到截图更新,说明验证失败
const currentStatus = document.getElementById('verificationStatus').textContent;
if (currentStatus === '验证中...') {
log('⚠️ 验证未通过,请重试', 'error');
document.getElementById('verificationStatus').textContent = '待处理';
}
}
else if (type === 'completed') {
log('✅ 验证成功!', 'success');
updateStatus('completed', '验证成功');
document.getElementById('verificationStatus').textContent = '已完成';
// 显示成功提示
setTimeout(() => {
alert('✅ 验证成功!页面即将关闭...');
if (ws) {
ws.close();
}
}, 500);
}
else if (type === 'error') {
log(`错误: ${data.message}`, 'error');
}
}
function displayScreenshot(base64Image) {
const img = new Image();
img.onload = () => {
// 调整 canvas 大小(只在必要时调整)
if (canvas.width !== img.width || canvas.height !== img.height) {
canvas.width = img.width;
canvas.height = img.height;
}
// 使用 imageSmoothingEnabled 优化渲染
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'low'; // 低质量平滑,提升性能
// 绘制图片
ctx.drawImage(img, 0, 0);
// 隐藏加载提示
document.getElementById('loading').style.display = 'none';
canvas.style.display = 'block';
};
// 使用 JPEG 格式(后端已改为 JPEG
img.src = `data:image/jpeg;base64,${base64Image}`;
}
function handleMouseDown(event) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
return;
}
isMouseDown = true;
const coords = getMouseCoords(event);
log(`鼠标按下: (${coords.x}, ${coords.y})`, 'info');
ws.send(JSON.stringify({
type: 'mouse_event',
event_type: 'down',
x: coords.x,
y: coords.y
}));
}
function handleMouseMove(event) {
if (!ws || ws.readyState !== WebSocket.OPEN || !isMouseDown) {
return;
}
// 节流:限制发送频率,避免卡顿
const now = Date.now();
if (now - lastMoveTime < moveThrottle) {
return; // 跳过这次事件
}
lastMoveTime = now;
const coords = getMouseCoords(event);
ws.send(JSON.stringify({
type: 'mouse_event',
event_type: 'move',
x: coords.x,
y: coords.y
}));
}
function handleMouseUp(event) {
if (!ws || ws.readyState !== WebSocket.OPEN || !isMouseDown) {
return;
}
isMouseDown = false;
const coords = getMouseCoords(event);
log(`鼠标释放: (${coords.x}, ${coords.y})`, 'info');
log(`等待验证结果...`, 'info');
// 更新状态显示
document.getElementById('verificationStatus').textContent = '验证中...';
ws.send(JSON.stringify({
type: 'mouse_event',
event_type: 'up',
x: coords.x,
y: coords.y
}));
}
function getMouseCoords(event) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
// 计算canvas上的坐标
const canvasX = Math.round((event.clientX - rect.left) * scaleX);
const canvasY = Math.round((event.clientY - rect.top) * scaleY);
// 转换为页面上的实际坐标(加上偏移量)
return {
x: canvasX + captchaOffset.x,
y: canvasY + captchaOffset.y
};
}
</script>
</body>
</html>