补充主题静态资源与归档模板
This commit is contained in:
1 parent
1268a55dd0
commit
3247db7ed1
265 files changed
+59174
No files matched your search
@@ -0,0 +1,406 @@
|
||||
/**
|
||||
* AKSRT Slider CAPTCHA
|
||||
*
|
||||
* Canvas-based slider puzzle CAPTCHA verification.
|
||||
* No jQuery dependency — pure vanilla JS.
|
||||
*
|
||||
* Usage:
|
||||
* var captcha = new AksrtSliderCaptcha(container, {
|
||||
* onSuccess: function(token) { ... },
|
||||
* onFail: function() { ... },
|
||||
* setSrc: function(callback) { callback(imageUrl); }
|
||||
* });
|
||||
* captcha.reset();
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var DEFAULTS = {
|
||||
width: 280,
|
||||
height: 170,
|
||||
sliderL: 42,
|
||||
sliderR: 9,
|
||||
offset: 8,
|
||||
barText: '向右拖动滑块完成验证',
|
||||
successText: '验证成功',
|
||||
failText: '验证失败,请重试',
|
||||
loadingText: '加载中...',
|
||||
maxLoadCount: 3,
|
||||
onSuccess: function () {},
|
||||
onFail: function () {},
|
||||
setSrc: function (callback) { callback(''); },
|
||||
};
|
||||
|
||||
function AksrtSliderCaptcha(element, options) {
|
||||
this.el = typeof element === 'string' ? document.querySelector(element) : element;
|
||||
if (!this.el) return;
|
||||
this.options = {};
|
||||
for (var k in DEFAULTS) this.options[k] = DEFAULTS[k];
|
||||
if (options) { for (var k in options) this.options[k] = options[k]; }
|
||||
this.loadCount = 0;
|
||||
this.init();
|
||||
}
|
||||
|
||||
AksrtSliderCaptcha.prototype.init = function () {
|
||||
this.el.style.position = 'relative';
|
||||
this.el.style.width = this.options.width + 'px';
|
||||
this.el.style.margin = '0 auto';
|
||||
this.el.innerHTML = '';
|
||||
this.initDOM();
|
||||
this.initImg();
|
||||
};
|
||||
|
||||
AksrtSliderCaptcha.prototype.initDOM = function () {
|
||||
var o = this.options;
|
||||
|
||||
// Canvas container
|
||||
this.canvasWrap = document.createElement('div');
|
||||
this.canvasWrap.className = 'aksrt-captcha__canvas-wrap';
|
||||
this.canvasWrap.style.cssText = 'position:relative;overflow:hidden;border-radius:8px;background:#f0f0f0;';
|
||||
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas.width = o.width;
|
||||
this.canvas.height = o.height;
|
||||
this.canvasCtx = this.canvas.getContext('2d');
|
||||
|
||||
this.block = document.createElement('canvas');
|
||||
this.block.width = o.width;
|
||||
this.block.height = o.height;
|
||||
this.blockCtx = this.block.getContext('2d');
|
||||
|
||||
this.canvasWrap.appendChild(this.canvas);
|
||||
this.canvasWrap.appendChild(this.block);
|
||||
|
||||
// Refresh button
|
||||
this.refreshBtn = document.createElement('div');
|
||||
this.refreshBtn.className = 'aksrt-captcha__refresh';
|
||||
this.refreshBtn.innerHTML = '↻';
|
||||
this.refreshBtn.style.cssText = 'position:absolute;top:6px;right:6px;cursor:pointer;font-size:18px;color:#fff;background:rgba(0,0,0,0.3);border-radius:50%;width:28px;height:28px;display:flex;align-items:center;justify-content:center;z-index:5;';
|
||||
this.canvasWrap.appendChild(this.refreshBtn);
|
||||
|
||||
// Slider bar
|
||||
this.sliderBar = document.createElement('div');
|
||||
this.sliderBar.className = 'aksrt-captcha__bar';
|
||||
this.sliderBar.style.cssText = 'position:relative;height:40px;border-radius:20px;background:#e8e8e8;margin-top:10px;user-select:none;';
|
||||
|
||||
this.sliderMask = document.createElement('div');
|
||||
this.sliderMask.className = 'aksrt-captcha__mask';
|
||||
this.sliderMask.style.cssText = 'position:absolute;top:0;left:0;height:100%;border-radius:20px;background:linear-gradient(90deg,#1890ff,#36cfc9);width:0;transition:background 0.3s;';
|
||||
|
||||
this.slider = document.createElement('div');
|
||||
this.slider.className = 'aksrt-captcha__slider';
|
||||
this.slider.style.cssText = 'position:absolute;top:0;left:0;width:44px;height:40px;border-radius:20px;background:#fff;box-shadow:0 2px 8px rgba(0,0,0,0.15);cursor:grab;display:flex;align-items:center;justify-content:center;font-size:18px;z-index:2;transition:box-shadow 0.2s;';
|
||||
this.slider.innerHTML = '❯';
|
||||
|
||||
this.text = document.createElement('div');
|
||||
this.text.className = 'aksrt-captcha__text';
|
||||
this.text.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#999;font-size:14px;pointer-events:none;';
|
||||
this.text.textContent = o.barText;
|
||||
|
||||
this.sliderBar.appendChild(this.sliderMask);
|
||||
this.sliderBar.appendChild(this.slider);
|
||||
this.sliderBar.appendChild(this.text);
|
||||
|
||||
this.el.appendChild(this.canvasWrap);
|
||||
this.el.appendChild(this.sliderBar);
|
||||
|
||||
this.bindEvents();
|
||||
};
|
||||
|
||||
AksrtSliderCaptcha.prototype.initImg = function () {
|
||||
var self = this;
|
||||
this.loadCount++;
|
||||
if (this.loadCount > this.options.maxLoadCount) {
|
||||
this.text.textContent = '加载失败';
|
||||
return;
|
||||
}
|
||||
|
||||
this.text.textContent = this.options.loadingText;
|
||||
this.slider.style.display = 'none';
|
||||
|
||||
this.options.setSrc(function (src) {
|
||||
if (!src) {
|
||||
self.text.textContent = '验证服务不可用';
|
||||
return;
|
||||
}
|
||||
var img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.onload = function () {
|
||||
self.bgImg = img;
|
||||
self.drawBg();
|
||||
self.drawBlock();
|
||||
self.slider.style.display = '';
|
||||
self.text.textContent = self.options.barText;
|
||||
};
|
||||
img.onerror = function () {
|
||||
self.initImg();
|
||||
};
|
||||
img.src = src;
|
||||
});
|
||||
};
|
||||
|
||||
AksrtSliderCaptcha.prototype.drawBg = function () {
|
||||
var o = this.options;
|
||||
this.canvasCtx.drawImage(this.bgImg, 0, 0, o.width, o.height);
|
||||
this.canvasCtx.globalAlpha = 0.6;
|
||||
this.canvasCtx.fillStyle = '#000';
|
||||
this.canvasCtx.fillRect(0, 0, o.width, o.height);
|
||||
this.canvasCtx.globalAlpha = 1;
|
||||
};
|
||||
|
||||
AksrtSliderCaptcha.prototype.drawBlock = function () {
|
||||
var o = this.options;
|
||||
var x = o.sliderL + Math.random() * (o.width - o.sliderL * 2 - 20);
|
||||
var y = 10 + Math.random() * (o.height - o.sliderL - 20);
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
||||
var ctx = this.blockCtx;
|
||||
ctx.clearRect(0, 0, o.width, o.height);
|
||||
|
||||
// Draw the puzzle piece shape
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(x + o.sliderL / 2, y);
|
||||
ctx.arc(x + o.sliderL / 2, y - o.sliderR, o.sliderR, 0.5 * Math.PI, 2.5 * Math.PI, false);
|
||||
ctx.lineTo(x + o.sliderL, y);
|
||||
ctx.lineTo(x + o.sliderL, y + o.sliderL / 2);
|
||||
ctx.arc(x + o.sliderL + o.sliderR, y + o.sliderL / 2, o.sliderR, Math.PI, 0, false);
|
||||
ctx.lineTo(x + o.sliderL, y + o.sliderL);
|
||||
ctx.lineTo(x, y + o.sliderL);
|
||||
ctx.closePath();
|
||||
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.8)';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.5)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// Clip and draw image portion
|
||||
ctx.save();
|
||||
ctx.clip();
|
||||
ctx.drawImage(this.bgImg, 0, 0, o.width, o.height);
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
AksrtSliderCaptcha.prototype.bindEvents = function () {
|
||||
var self = this;
|
||||
var o = this.options;
|
||||
var dragging = false;
|
||||
var startX = 0;
|
||||
var currentX = 0;
|
||||
var maxMove = o.width - o.sliderL - 10;
|
||||
var trail = []; // y-axis trail for behavior verification
|
||||
|
||||
function onStart(e) {
|
||||
e.preventDefault();
|
||||
dragging = true;
|
||||
trail = [];
|
||||
startX = (e.touches ? e.touches[0].clientX : e.clientX) || 0;
|
||||
self.slider.style.cursor = 'grabbing';
|
||||
self.slider.style.boxShadow = '0 4px 12px rgba(0,0,0,0.25)';
|
||||
}
|
||||
|
||||
function onMove(e) {
|
||||
if (!dragging) return;
|
||||
e.preventDefault();
|
||||
var clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||
var clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
||||
currentX = Math.max(0, Math.min(maxMove, clientX - startX));
|
||||
trail.push(Math.round(clientY));
|
||||
self.slider.style.left = currentX + 'px';
|
||||
self.sliderMask.style.width = (currentX + 22) + 'px';
|
||||
self.block.style.left = currentX + 'px';
|
||||
}
|
||||
|
||||
function onEnd() {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
self.slider.style.cursor = 'grab';
|
||||
self.slider.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
|
||||
|
||||
var positionOk = Math.abs(currentX - self.x) < o.offset;
|
||||
var behaviorOk = verifyTrail(trail);
|
||||
|
||||
if (positionOk && behaviorOk) {
|
||||
self.onSuccess(trail, currentX);
|
||||
} else {
|
||||
self.onFail();
|
||||
}
|
||||
}
|
||||
|
||||
this.slider.addEventListener('mousedown', onStart);
|
||||
this.slider.addEventListener('touchstart', onStart, { passive: false });
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('touchmove', onMove, { passive: false });
|
||||
document.addEventListener('mouseup', onEnd);
|
||||
document.addEventListener('touchend', onEnd);
|
||||
|
||||
this.refreshBtn.addEventListener('click', function () {
|
||||
self.reset();
|
||||
});
|
||||
};
|
||||
|
||||
// Trail behavior verification: stddev of y-axis must be > 0 (not a robot straight line)
|
||||
function verifyTrail(trail) {
|
||||
if (!trail || trail.length < 5) return false;
|
||||
var sum = 0;
|
||||
for (var i = 0; i < trail.length; i++) sum += trail[i];
|
||||
var avg = sum / trail.length;
|
||||
var variance = 0;
|
||||
for (var j = 0; j < trail.length; j++) variance += (trail[j] - avg) * (trail[j] - avg);
|
||||
var stddev = Math.sqrt(variance / trail.length);
|
||||
return stddev > 0;
|
||||
}
|
||||
|
||||
// Encode trail + position for server verification
|
||||
function encodePayload(trail, spliced) {
|
||||
var str = JSON.stringify({ t: trail, s: Math.round(spliced), ts: Date.now() });
|
||||
try { return btoa(str); } catch (e) { return str; }
|
||||
}
|
||||
|
||||
AksrtSliderCaptcha.prototype.onSuccess = function (trail, spliced) {
|
||||
var self = this;
|
||||
var o = this.options;
|
||||
this.sliderBar.classList.add('sliderContainer_success');
|
||||
this.sliderMask.style.background = '#52c41a';
|
||||
this.text.textContent = o.successText;
|
||||
this.text.style.color = '#52c41a';
|
||||
this.slider.innerHTML = '✓';
|
||||
this.slider.style.color = '#52c41a';
|
||||
this.slider.style.cursor = 'default';
|
||||
|
||||
var payload = encodePayload(trail || [], spliced || 0);
|
||||
|
||||
// Server token verification if endpoint configured
|
||||
if (o.serverVerify) {
|
||||
var ajaxUrl = (window.aksrtData && aksrtData.ajaxUrl) || '/wp-admin/admin-ajax.php';
|
||||
var params = new URLSearchParams();
|
||||
params.set('action', 'aksrt_captcha_verify');
|
||||
if (window.aksrtData && aksrtData.nonce) params.set('nonce', aksrtData.nonce);
|
||||
params.set('payload', payload);
|
||||
if (o.scene) params.set('scene', o.scene);
|
||||
|
||||
fetch(ajaxUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||
credentials: 'same-origin',
|
||||
body: params.toString()
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (json) {
|
||||
if (json && json.success && json.data && json.data.token) {
|
||||
if (typeof o.onSuccess === 'function') o.onSuccess(json.data.token);
|
||||
} else {
|
||||
self.onFail();
|
||||
}
|
||||
})
|
||||
.catch(function () { self.onFail(); });
|
||||
} else {
|
||||
if (typeof o.onSuccess === 'function') {
|
||||
setTimeout(function () { o.onSuccess(payload); }, 300);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AksrtSliderCaptcha.prototype.onFail = function () {
|
||||
var self = this;
|
||||
var o = this.options;
|
||||
this.sliderBar.classList.add('sliderContainer_fail');
|
||||
this.sliderMask.style.background = '#ff4d4f';
|
||||
this.text.textContent = o.failText;
|
||||
this.text.style.color = '#ff4d4f';
|
||||
this.slider.style.color = '#ff4d4f';
|
||||
|
||||
if (typeof o.onFail === 'function') o.onFail();
|
||||
|
||||
setTimeout(function () {
|
||||
self.sliderBar.classList.remove('sliderContainer_fail');
|
||||
self.reset();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
AksrtSliderCaptcha.prototype.reset = function () {
|
||||
this.slider.style.left = '0px';
|
||||
this.sliderMask.style.width = '0px';
|
||||
this.block.style.left = '0px';
|
||||
this.sliderMask.style.background = 'linear-gradient(90deg,#1890ff,#36cfc9)';
|
||||
this.text.style.color = '#999';
|
||||
this.text.textContent = this.options.loadingText;
|
||||
this.slider.innerHTML = '❯';
|
||||
this.slider.style.color = '';
|
||||
this.slider.style.cursor = 'grab';
|
||||
this.loadCount = 0;
|
||||
this.initImg();
|
||||
};
|
||||
|
||||
// ── Modal wrapper ──
|
||||
|
||||
function SliderCaptchaModal(options) {
|
||||
var overlay = document.createElement('div');
|
||||
overlay.className = 'aksrt-captcha-modal-overlay';
|
||||
var dialog = document.createElement('div');
|
||||
dialog.className = 'aksrt-captcha-modal-dialog';
|
||||
overlay.appendChild(dialog);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
var origSuccess = options.onSuccess;
|
||||
options.onSuccess = function (token) {
|
||||
overlay.classList.remove('is-open');
|
||||
setTimeout(function () { overlay.remove(); }, 300);
|
||||
if (typeof origSuccess === 'function') origSuccess(token);
|
||||
};
|
||||
|
||||
if (!options.setSrc) {
|
||||
options.setSrc = function (cb) {
|
||||
cb('https://picsum.photos/' + (options.width || 280) + '/' + (options.height || 170) + '?random=' + Date.now());
|
||||
};
|
||||
}
|
||||
|
||||
var captcha = new AksrtSliderCaptcha(dialog, options);
|
||||
|
||||
requestAnimationFrame(function () { overlay.classList.add('is-open'); });
|
||||
|
||||
overlay.addEventListener('click', function (e) {
|
||||
if (e.target === overlay) {
|
||||
overlay.classList.remove('is-open');
|
||||
setTimeout(function () { overlay.remove(); }, 300);
|
||||
}
|
||||
});
|
||||
|
||||
return { el: overlay, captcha: captcha };
|
||||
}
|
||||
|
||||
// ── Public API ──
|
||||
|
||||
window.AksrtSliderCaptcha = AksrtSliderCaptcha;
|
||||
window.SliderCaptchaModal = SliderCaptchaModal;
|
||||
|
||||
// ── CSS ──
|
||||
|
||||
var STYLE_ID = 'aksrt-slider-captcha-style';
|
||||
|
||||
function injectStyle() {
|
||||
if (document.getElementById(STYLE_ID)) return;
|
||||
var style = document.createElement('style');
|
||||
style.id = STYLE_ID;
|
||||
style.textContent =
|
||||
'.aksrt-captcha__canvas-wrap{position:relative;overflow:hidden;border-radius:8px;background:#f0f0f0;}' +
|
||||
'.aksrt-captcha__canvas-wrap canvas{display:block;}' +
|
||||
'.aksrt-captcha__canvas-wrap canvas:last-child{position:absolute;top:0;left:0;pointer-events:none;}' +
|
||||
'.aksrt-captcha__bar{position:relative;height:40px;border-radius:20px;background:#e8e8e8;margin-top:10px;user-select:none;}' +
|
||||
'.aksrt-captcha__mask{position:absolute;top:0;left:0;height:100%;border-radius:20px;background:linear-gradient(90deg,#1890ff,#36cfc9);width:0;transition:background 0.3s;}' +
|
||||
'.aksrt-captcha__slider{position:absolute;top:0;left:0;width:44px;height:40px;border-radius:20px;background:#fff;box-shadow:0 2px 8px rgba(0,0,0,0.15);cursor:grab;display:flex;align-items:center;justify-content:center;font-size:18px;z-index:2;transition:box-shadow 0.2s;}' +
|
||||
'.aksrt-captcha__text{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#999;font-size:14px;pointer-events:none;}';
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', injectStyle);
|
||||
} else {
|
||||
injectStyle();
|
||||
}
|
||||
})();
|
||||
Reference in new issue
Block a user