You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Open-IM-Server/scripts/test/captcha_demo.html

317 lines
11 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Captcha 验证码测试</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
background:#f0f2f5;padding:20px;min-height:100vh}
/* ── 卡片 ── */
.card{background:#fff;border-radius:12px;box-shadow:0 2px 12px rgba(0,0,0,.08);
padding:20px;max-width:500px;margin:0 auto 16px}
.card-title{font-size:15px;font-weight:600;color:#1a1a2e;margin-bottom:14px;
display:flex;align-items:center;gap:6px}
/* ── 表单 ── */
.field{margin-bottom:10px}
.field label{display:block;font-size:11px;color:#888;margin-bottom:3px}
input[type=text]{width:100%;padding:8px 10px;border:1px solid #e0e0e0;border-radius:8px;
font-size:12px;outline:none;transition:border .15s}
input[type=text]:focus{border-color:#4f46e5}
/* ── 按钮 ── */
.btn{display:inline-flex;align-items:center;gap:5px;padding:8px 16px;border:none;
border-radius:8px;font-size:12px;font-weight:500;cursor:pointer;transition:.15s}
.btn:hover{filter:brightness(.92)}
.btn:disabled{opacity:.45;cursor:not-allowed}
.btn-indigo{background:#4f46e5;color:#fff}
.btn-gray {background:#6b7280;color:#fff}
.btn-green {background:#10b981;color:#fff}
.btn-row {display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
/* ── 状态标签 ── */
.tag{display:inline-block;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:600}
.tag-ok {background:#d1fae5;color:#065f46}
.tag-err {background:#fee2e2;color:#991b1b}
.tag-info{background:#e0e7ff;color:#3730a3}
#status,#verify-status{margin-top:10px;min-height:24px}
/* ── 验证码区域 ── */
#captcha-section{display:none}
/* 背景图 + 滑块叠层 */
.slide-wrap{
position:relative;width:300px;height:220px;
border:2px solid #e5e7eb;border-radius:10px;
overflow:hidden;background:#f3f4f6;
cursor:default;user-select:none;
}
#master-img{position:absolute;top:0;left:0;width:300px;height:220px;display:block}
#tile-img {position:absolute;cursor:grab;transition:none}
#tile-img:active{cursor:grabbing}
/* 滑轨 */
.slider-row{display:flex;align-items:center;gap:6px;margin-top:8px}
.slider-row span{font-size:11px;color:#6b7280;min-width:14px}
.slider-row input[type=range]{flex:1;accent-color:#4f46e5;height:4px}
.slider-row .val{font-size:11px;color:#4f46e5;min-width:30px;text-align:right}
/* 提示箭头 */
.hint{font-size:11px;color:#9ca3af;margin-top:6px}
/* ── 调试区 ── */
#debug-area{display:none;margin-top:14px}
.debug-label{font-size:10px;color:#9ca3af;margin-bottom:4px}
.image-preview{display:flex;gap:12px;flex-wrap:wrap;align-items:flex-start}
.image-preview figure{text-align:center}
.image-preview figcaption{font-size:10px;color:#888;margin-top:4px}
pre{background:#1e1e2e;color:#cdd6f4;border-radius:8px;padding:12px;
font-size:10px;overflow:auto;max-height:160px;word-break:break-all}
</style>
</head>
<body>
<!-- ① 配置 -->
<div class="card">
<div class="card-title">⚙️ 配置</div>
<div class="field">
<label>API Host</label>
<input type="text" id="host" value="http://127.0.0.1:10002">
</div>
<div class="field">
<label>Token留空则自动获取 Admin Token</label>
<input type="text" id="token" placeholder="eyJhbGci...">
</div>
<div class="field">
<label>Admin Secret</label>
<input type="text" id="secret" value="openIM123">
</div>
<div class="btn-row">
<button class="btn btn-gray" onclick="getToken()">🔑 获取 Token</button>
<button class="btn btn-indigo" id="btn-gen" onclick="generate()">🖼 生成验证码</button>
</div>
<div id="status"></div>
</div>
<!-- ② 验证码交互 -->
<div class="card" id="captcha-section">
<div class="card-title">🧩 滑块验证码</div>
<p class="hint">拖动滑块或直接拖拽图中的小方块,使其嵌入背景缺口后点击「验证」。</p>
<div style="margin-top:12px">
<!-- 背景图 + 浮动滑块 -->
<div class="slide-wrap" id="slide-wrap">
<img id="master-img" alt="">
<img id="tile-img" alt="">
</div>
<!-- X 轴滑轨 -->
<div class="slider-row">
<span>←→</span>
<input type="range" id="sx" min="0" max="240" value="0" oninput="syncTile()">
<span class="val" id="sx-val">0 px</span>
</div>
<!-- Y 轴调试用 -->
<div class="slider-row">
<span></span>
<input type="range" id="sy" min="0" max="160" value="0" oninput="syncTile()">
<span class="val" id="sy-val">0 px</span>
</div>
</div>
<div class="btn-row">
<button class="btn btn-green" onclick="verify()">✅ 验证</button>
<button class="btn btn-indigo" onclick="generate()">🔄 刷新</button>
</div>
<div id="verify-status"></div>
<!-- 调试:直接渲染两张图 + 原始数据 -->
<div id="debug-area">
<hr style="border:none;border-top:1px solid #f0f0f0;margin:14px 0">
<div class="debug-label">图片预览(直接从 Base64 渲染)</div>
<div class="image-preview">
<figure>
<img id="dbg-master" style="border-radius:6px;border:1px solid #e5e7eb" alt="">
<figcaption>masterImageJPEG 背景)</figcaption>
</figure>
<figure>
<img id="dbg-tile" style="border-radius:4px;border:1px solid #e5e7eb;background:#ddd" alt="">
<figcaption>tileImagePNG 滑块)</figcaption>
</figure>
</div>
<div class="debug-label" style="margin-top:10px">原始响应(去除图片数据)</div>
<pre id="raw-json"></pre>
</div>
</div>
<script>
/* ── 工具 ── */
const $ = id => document.getElementById(id);
let captchaState = { id:'', tileW:68, tileH:68, tileY:0 };
let opSeq = 0;
function setStatus(elId, html, type='info'){
$(elId).innerHTML = `<span class="tag tag-${type}">${html}</span>`;
}
async function post(path, body){
const host = $('host').value.replace(/\/$/,'');
const tok = $('token').value.trim();
const headers = {
'Content-Type':'application/json',
'operationID':`demo-${Date.now()}-${++opSeq}`,
};
if(tok) headers['token'] = tok;
const r = await fetch(host+path, {method:'POST', headers, body:JSON.stringify(body)});
return r.json();
}
/* ── 1. 获取 Token ── */
async function getToken(){
setStatus('status','获取 Token 中…','info');
try{
const d = await post('/auth/get_admin_token',{
secret:$('secret').value, platformID:1, userID:'imAdmin'
});
if(d.errCode!==0){ setStatus('status',`获取失败: ${d.errMsg}`,'err'); return; }
$('token').value = d.data.token;
setStatus('status','Token 获取成功 ✓','ok');
}catch(e){ setStatus('status',`请求异常: ${e.message}`,'err'); }
}
/* ── 2. 生成验证码 ── */
async function generate(){
if(!$('token').value.trim()){ await getToken(); if(!$('token').value.trim()) return; }
setStatus('status','生成验证码中…','info');
$('btn-gen').disabled = true;
try{
const d = await post('/captcha/generate',{});
/* 展示原始响应(隐去图片数据保持可读) */
const preview = JSON.parse(JSON.stringify(d));
if(preview.data){
if(preview.data.masterImage) preview.data.masterImage = `<base64 JPEG, ${preview.data.masterImage.length} chars>`;
if(preview.data.tileImage) preview.data.tileImage = `<base64 PNG, ${preview.data.tileImage.length} chars>`;
}
$('raw-json').textContent = JSON.stringify(preview, null, 2);
if(d.errCode!==0){
setStatus('status',`生成失败 errCode=${d.errCode}: ${d.errMsg}${d.errDlt}`,'err');
return;
}
const {captchaID, masterImage, tileImage, tileY, expireAt} = d.data;
captchaState.id = captchaID;
captchaState.tileY = tileY || 0;
/* ── 渲染 masterImageJPEG── */
const masterSrc = `data:image/jpeg;base64,${masterImage}`;
$('master-img').src = masterSrc;
$('dbg-master').src = masterSrc;
$('dbg-master').style.width = '300px';
/* ── 渲染 tileImagePNG── */
const tileSrc = `data:image/png;base64,${tileImage}`;
$('dbg-tile').src = tileSrc;
/* 等 tile 图片加载完毕后设置滑块 */
await loadImage($('tile-img'), tileSrc);
captchaState.tileW = $('tile-img').naturalWidth || 68;
captchaState.tileH = $('tile-img').naturalHeight || 68;
/* 更新滑轨范围,初始化 Y = tileY */
$('sx').max = 300 - captchaState.tileW;
$('sy').max = 220 - captchaState.tileH;
$('sx').value = 0;
$('sy').value = captchaState.tileY;
syncTile();
const expire = new Date(expireAt * 1000).toLocaleTimeString();
setStatus('status',
`验证码已生成 | id: ${captchaID.slice(0,8)}… | tileY: ${tileY} | 过期: ${expire}`,'ok');
$('captcha-section').style.display = 'block';
$('debug-area').style.display = 'block';
$('verify-status').innerHTML = '';
}catch(e){
setStatus('status',`请求异常: ${e.message}`,'err');
}finally{
$('btn-gen').disabled = false;
}
}
/* 等待 img.onloadPromise 封装) */
function loadImage(imgEl, src){
return new Promise((resolve, reject)=>{
imgEl.onload = resolve;
imgEl.onerror = reject;
imgEl.src = src;
});
}
/* ── 3. 同步滑块位置 ── */
function syncTile(){
const x = parseInt($('sx').value);
const y = parseInt($('sy').value);
$('sx-val').textContent = x + ' px';
$('sy-val').textContent = y + ' px';
$('tile-img').style.left = x + 'px';
$('tile-img').style.top = y + 'px';
}
/* ── 4. 鼠标 / 触屏拖拽 ── */
(()=>{
let drag=false, sx0=0, sliderX0=0;
const wrap = $('slide-wrap');
const start = (cx)=>{ drag=true; sx0=cx; sliderX0=parseInt($('sx').value); };
const move = (cx)=>{
if(!drag) return;
const nx = Math.max(0, Math.min(parseInt($('sx').max), sliderX0+(cx-sx0)));
$('sx').value = nx;
syncTile();
};
const end = ()=>{ drag=false; };
wrap.addEventListener('mousedown', e=>{ if(e.target.id==='tile-img') start(e.clientX); });
document.addEventListener('mousemove', e=>move(e.clientX));
document.addEventListener('mouseup', end);
wrap.addEventListener('touchstart', e=>{
if(e.target.id==='tile-img'){ start(e.touches[0].clientX); e.preventDefault(); }
},{passive:false});
document.addEventListener('touchmove', e=>{
if(drag){ move(e.touches[0].clientX); e.preventDefault(); }
},{passive:false});
document.addEventListener('touchend', end);
})();
/* ── 5. 验证 ── */
async function verify(){
if(!captchaState.id){ alert('请先生成验证码'); return; }
const x = parseInt($('sx').value);
const y = parseInt($('sy').value);
setStatus('verify-status',`提交验证 x=${x} y=${y}`,'info');
try{
const d = await post('/captcha/verify',{captchaID:captchaState.id, x, y});
if(d.errCode!==0){
setStatus('verify-status',`请求失败 errCode=${d.errCode}: ${d.errMsg}`,'err');
return;
}
if(d.data.success){
setStatus('verify-status',`🎉 验证通过x=${x} y=${y}`,'ok');
}else{
setStatus('verify-status',`❌ 验证失败,偏差过大,请重新拖动后再试`,'err');
}
}catch(e){
setStatus('verify-status',`请求异常: ${e.message}`,'err');
}
}
</script>
</body>
</html>