Phase 3: 管理后台前端 (Vue3+Vite+Element Plus) - 登录页面/JWT认证/Pinia状态管理 - 首页大盘/站点管理/验证码日志/IP黑名单/套餐管理/系统设置 - 7个页面完整路由+API对接 Phase 4: 统计监控模块 - CaptchaLogAspect AOP自动记录 - RealtimeStatsService Redis实时计数 - HistoryStatsService 历史查询 - AnomalyDetectionService 异常检测 Phase 5: 部署和测试 - docker-compose.yml 5服务编排 - application-prod.yml 外部化配置 - CaptchaApiIntegrationTest 集成测试 - 前端 Dockerfile + nginx.conf
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
import "./angle.scss"
|
||||
import {Dom, CommonCaptcha, destroyEvent} from "../common/common.js"
|
||||
|
||||
/**
|
||||
* 角度验证验证码 - 用户需要旋转图片到正确角度
|
||||
*/
|
||||
|
||||
const TYPE = "ANGLE"
|
||||
function getTemplate(styleConfig) {
|
||||
return `
|
||||
<div id="tianai-captcha" class="tianai-captcha-slider tianai-captcha-angle">
|
||||
<div class="click-tip">
|
||||
<span id="tianai-captcha-click-track-font" style="font-size: ${styleConfig.i18n?.angle_title_size || '14px'}">${styleConfig.i18n?.angle_title || '旋转图片到正确角度'}</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="rotate-container">
|
||||
<div class="rotate-img" id="rotate-img">
|
||||
<img id="tianai-captcha-rotate-bg-img" src="" alt/>
|
||||
</div>
|
||||
<div class="rotate-indicator" id="rotate-indicator">
|
||||
<div class="indicator-line" id="indicator-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slider-bar" id="tianai-captcha-slider-bar">
|
||||
<div class="slider-bar-bg"></div>
|
||||
<div class="slider-bar-track" id="tianai-captcha-slider-track"></div>
|
||||
<div class="slider-bar-btn" id="tianai-captcha-slider-btn">
|
||||
<div class="slider-btn-icon"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tianai-captcha-tips" id="tianai-captcha-tips"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
class Angle extends CommonCaptcha{
|
||||
constructor(boxEl, styleConfig) {
|
||||
super();
|
||||
this.boxEl = boxEl;
|
||||
this.styleConfig = styleConfig;
|
||||
this.type = TYPE;
|
||||
this.currentCaptchaData = {};
|
||||
this.isDragging = false;
|
||||
this.startX = 0;
|
||||
this.currentAngle = 0;
|
||||
}
|
||||
init(captchaData, endCallback, loadSuccessCallback) {
|
||||
// 重载样式
|
||||
this.destroy();
|
||||
this.boxEl.append(getTemplate(this.styleConfig));
|
||||
this.el = this.boxEl.find("#tianai-captcha");
|
||||
// 载入验证码
|
||||
this.loadCaptchaForData(this, captchaData);
|
||||
this.endCallback = endCallback;
|
||||
|
||||
if (loadSuccessCallback) {
|
||||
// 加载成功
|
||||
loadSuccessCallback(this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
bindDragEvents() {
|
||||
const sliderBtn = document.getElementById('tianai-captcha-slider-btn');
|
||||
const rotateImg = document.getElementById('rotate-img');
|
||||
if (!sliderBtn || !rotateImg) return;
|
||||
|
||||
// 鼠标按下
|
||||
sliderBtn.addEventListener('mousedown', (e) => {
|
||||
this.isDragging = true;
|
||||
this.startX = e.pageX;
|
||||
this.currentCaptchaData.startTime = new Date();
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// 鼠标移动
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!this.isDragging) return;
|
||||
const moveX = e.pageX - this.startX;
|
||||
const sliderBar = document.getElementById('tianai-captcha-slider-bar');
|
||||
const barWidth = sliderBar ? sliderBar.offsetWidth - 40 : 200;
|
||||
const left = Math.max(0, Math.min(moveX, barWidth));
|
||||
|
||||
sliderBtn.style.left = left + 'px';
|
||||
document.getElementById('tianai-captcha-slider-track').style.width = (left + 20) + 'px';
|
||||
|
||||
// 计算旋转角度
|
||||
this.currentAngle = (left / barWidth) * 360;
|
||||
rotateImg.style.transform = 'rotate(' + this.currentAngle + 'deg)';
|
||||
|
||||
// 更新指示器
|
||||
this.updateIndicator();
|
||||
|
||||
// 记录轨迹
|
||||
if (!this.currentCaptchaData.trackList) {
|
||||
this.currentCaptchaData.trackList = [];
|
||||
}
|
||||
this.currentCaptchaData.trackList.push({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
angle: this.currentAngle,
|
||||
t: new Date().getTime() - this.currentCaptchaData.startTime.getTime()
|
||||
});
|
||||
});
|
||||
|
||||
// 鼠标松开
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (!this.isDragging) return;
|
||||
this.isDragging = false;
|
||||
this.currentCaptchaData.stopTime = new Date();
|
||||
this.currentCaptchaData.currentAngle = this.currentAngle;
|
||||
this.endCallback(this.currentCaptchaData, this);
|
||||
});
|
||||
|
||||
// 触摸事件
|
||||
sliderBtn.addEventListener('touchstart', (e) => {
|
||||
this.isDragging = true;
|
||||
this.startX = e.touches[0].pageX;
|
||||
this.currentCaptchaData.startTime = new Date();
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener('touchmove', (e) => {
|
||||
if (!this.isDragging) return;
|
||||
const moveX = e.touches[0].pageX - this.startX;
|
||||
const sliderBar = document.getElementById('tianai-captcha-slider-bar');
|
||||
const barWidth = sliderBar ? sliderBar.offsetWidth - 40 : 200;
|
||||
const left = Math.max(0, Math.min(moveX, barWidth));
|
||||
|
||||
sliderBtn.style.left = left + 'px';
|
||||
document.getElementById('tianai-captcha-slider-track').style.width = (left + 20) + 'px';
|
||||
|
||||
this.currentAngle = (left / barWidth) * 360;
|
||||
rotateImg.style.transform = 'rotate(' + this.currentAngle + 'deg)';
|
||||
this.updateIndicator();
|
||||
});
|
||||
|
||||
document.addEventListener('touchend', () => {
|
||||
if (!this.isDragging) return;
|
||||
this.isDragging = false;
|
||||
this.currentCaptchaData.stopTime = new Date();
|
||||
this.currentCaptchaData.currentAngle = this.currentAngle;
|
||||
this.endCallback(this.currentCaptchaData, this);
|
||||
});
|
||||
}
|
||||
|
||||
updateIndicator() {
|
||||
const indicatorLine = document.getElementById('indicator-line');
|
||||
if (indicatorLine) {
|
||||
indicatorLine.style.transform = 'rotate(' + this.currentAngle + 'deg)';
|
||||
}
|
||||
}
|
||||
|
||||
destroy () {
|
||||
const existsCaptchaEl = this.boxEl.children("#tianai-captcha");
|
||||
if (existsCaptchaEl) {
|
||||
existsCaptchaEl.remove();
|
||||
}
|
||||
destroyEvent();
|
||||
}
|
||||
|
||||
loadCaptchaForData (that, data) {
|
||||
const bgImg = that.el.find("#tianai-captcha-rotate-bg-img");
|
||||
bgImg.on("load",() => {
|
||||
that.currentCaptchaData = {
|
||||
bgImageWidth: bgImg.width(),
|
||||
bgImageHeight: bgImg.height(),
|
||||
startTime: new Date(),
|
||||
trackList: [],
|
||||
targetAngle: data.data.targetAngle || 0,
|
||||
currentAngle: 0
|
||||
};
|
||||
that.currentCaptchaData.currentCaptchaId = data.data.id;
|
||||
// 绑定滑动事件
|
||||
setTimeout(() => that.bindDragEvents(), 100);
|
||||
})
|
||||
bgImg.attr("src", data.data.backgroundImage);
|
||||
}
|
||||
}
|
||||
|
||||
export default Angle;
|
||||
@@ -0,0 +1,107 @@
|
||||
#tianai-captcha.tianai-captcha-angle {
|
||||
box-sizing: border-box;
|
||||
|
||||
.click-tip {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
#tianai-captcha-click-track-font {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
.rotate-container {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin: 0 auto;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 3px solid #e8e8e8;
|
||||
|
||||
.rotate-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.1s;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.rotate-indicator {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 2px;
|
||||
height: 50%;
|
||||
background-color: transparent;
|
||||
transform-origin: bottom center;
|
||||
transform: translate(-50%, -100%);
|
||||
|
||||
.indicator-line {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #ff4444;
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slider-bar {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
margin-top: 3px;
|
||||
|
||||
.slider-bar-bg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 4px;
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
.slider-bar-track {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border-radius: 4px;
|
||||
background-image: linear-gradient(90deg, #409eff, #66b1ff);
|
||||
transition: width 0.1s;
|
||||
}
|
||||
|
||||
.slider-bar-btn {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 40px;
|
||||
border-radius: 4px;
|
||||
background-image: linear-gradient(173deg, hsl(38.09deg 91% 57.89%) 0%, hsl(38.09deg 89.38% 71.74%) 100%);
|
||||
cursor: move;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
|
||||
.slider-btn-icon {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 6px 0 6px 8px;
|
||||
border-color: transparent transparent transparent #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,14 @@ import Rotate from "./rotate/rotate";
|
||||
import Concat from "./concat/concat";
|
||||
import Disable from "./disable/disable";
|
||||
import WordImageClick from "./word_image_click/word_image_click";
|
||||
import IconClick from "./icon_click/icon_click";
|
||||
import WordOrderClick from "./word_order_click/word_order_click";
|
||||
import Scratch from "./scratch/scratch";
|
||||
import Jigsaw from "./jigsaw/jigsaw";
|
||||
import CurveSlider from "./curve_slider/curve_slider";
|
||||
import Angle from "./angle/angle";
|
||||
import CurveDraw from "./curve_draw/curve_draw";
|
||||
import ProofOfWork from "./proof_of_work/proof_of_work";
|
||||
import {CaptchaConfig, wrapConfig, wrapStyle} from "./config/config";
|
||||
import {clearAllPreventDefault} from "./common/common";
|
||||
const template =
|
||||
@@ -11,13 +19,30 @@ const template =
|
||||
<div id="tianai-captcha-parent">
|
||||
<div id="tianai-captcha-bg-img"></div>
|
||||
<div id="tianai-captcha-box">
|
||||
<div id="tianai-captcha-loading" class="loading"></div>
|
||||
<div id="tianai-captcha-loading" class="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部 -->
|
||||
<div class="slider-bottom">
|
||||
<img class="logo" id="tianai-captcha-logo" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAMAAAAM7l6QAAAAMFBMVEVHcEz3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkVmTmjZAAAAD3RSTlMASbTm8wh12hOGoCNiyTV98jvOAAABB0lEQVR42nVT0aIFEQiMorD0/397Lc5a7J0n1UylgIniLRKyDcbBDudZH2DYCAabn3PmTrjeUX+7rJGWx0SqVpzReAfTtKU5fgVCNfxWjB69USUDGwoOiauHpZEpSr0tCx8ILb3Dm3WgBbAlifAJk6+Ww6wqEUmpmIorQVZ1JtqKnDMjkb7AgIpO/wMCaQbuBuEtsBUxhuD9daUaZnApiQB8NAKotMwirGGr6mbXpPnHLHDmy6oy3FgP+1j8IBdVklFc01xUJwv3NR0rIeXV5zpzdlruiijzNq/ufOeKWzZLP3160u5P8RjT1M+HHFtx+PwGyOZqT/D8ROOfjOInTLBIHjy/hvwHxkwPu5cCE1QAAAAASUVORK5CYII=" id="tianai-captcha-logo"></img>
|
||||
<div class="close-btn" id="tianai-captcha-slider-close-btn"></div>
|
||||
<div class="refresh-btn" id="tianai-captcha-slider-refresh-btn"></div>
|
||||
<div class="bottom-left">
|
||||
<img class="logo" id="tianai-captcha-logo" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 28'%3E%3Ctext x='0' y='22' font-family='Arial,sans-serif' font-size='18' font-weight='bold' fill='%23667eea'%3ETianAI%3C/text%3E%3C/svg%3E" id="tianai-captcha-logo">
|
||||
</div>
|
||||
<div class="bottom-right">
|
||||
<div class="icon-btn refresh-btn" id="tianai-captcha-slider-refresh-btn" title="刷新">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M23 4v6h-6M1 20v-6h6"/>
|
||||
<path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="icon-btn close-btn" id="tianai-captcha-slider-close-btn" title="关闭">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -33,6 +58,22 @@ function createCaptchaByType(type, tac) {
|
||||
return new Concat(box, styleConfig);
|
||||
case "WORD_IMAGE_CLICK":
|
||||
return new WordImageClick(box, styleConfig);
|
||||
case "ICON_CLICK":
|
||||
return new IconClick(box, styleConfig);
|
||||
case "WORD_ORDER_CLICK":
|
||||
return new WordOrderClick(box, styleConfig);
|
||||
case "SCRATCH":
|
||||
return new Scratch(box, styleConfig);
|
||||
case "JIGSAW":
|
||||
return new Jigsaw(box, styleConfig);
|
||||
case "CURVE_SLIDER":
|
||||
return new CurveSlider(box, styleConfig);
|
||||
case "ANGLE":
|
||||
return new Angle(box, styleConfig);
|
||||
case "CURVE_DRAW":
|
||||
return new CurveDraw(box, styleConfig);
|
||||
case "PROOF_OF_WORK":
|
||||
return new ProofOfWork(box, styleConfig);
|
||||
case "DISABLED":
|
||||
return new Disable(box, styleConfig);
|
||||
default:
|
||||
|
||||
@@ -1,39 +1,58 @@
|
||||
#tianai-captcha-parent {
|
||||
box-shadow: 0 0 11px 0 #999999;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
||||
width: 318px;
|
||||
height: 318px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 997;
|
||||
box-sizing: border-box;
|
||||
border-radius: 5px;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
|
||||
#tianai-captcha-box {
|
||||
height: 260px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
|
||||
.loading {
|
||||
width: 120px;
|
||||
height: 20px;
|
||||
-webkit-mask: linear-gradient(90deg, #000 70%, #0000 0) 0/20%;
|
||||
background: linear-gradient(#f7b645 0 0) 0 / 0% no-repeat #dddddd6b;
|
||||
animation: cartoon 1s infinite steps(6);
|
||||
margin: 120px auto;
|
||||
@keyframes cartoon {
|
||||
100% {
|
||||
background-size: 120%;
|
||||
}
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #fafafa;
|
||||
|
||||
.loading-spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 3px solid #f0f0f0;
|
||||
border-top-color: #f7b645;
|
||||
border-radius: 50%;
|
||||
animation: tianai-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tianai-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
#tianai-captcha {
|
||||
transform-style: preserve-3d;
|
||||
will-change: transform;
|
||||
transition-duration: .45s;
|
||||
//transition-timing-function: cubic-bezier(0.36, 0.3, 0.42, 1.5);
|
||||
transition-duration: 0.45s;
|
||||
transform: translateX(-300px);
|
||||
}
|
||||
}
|
||||
|
||||
#tianai-captcha-bg-img {
|
||||
background-color: #fff;
|
||||
background-position: top;
|
||||
@@ -44,11 +63,54 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
border-radius: 6px;
|
||||
//background-image: url("");
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.slider-bottom {
|
||||
.slider-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 26px;
|
||||
width: 100%;
|
||||
padding: 0 2px;
|
||||
|
||||
.bottom-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.logo {
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.icon-btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@@ -59,6 +121,7 @@
|
||||
margin-right: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@@ -69,13 +132,16 @@
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 30px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
height: 19px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.slider-move-shadow {
|
||||
animation: myanimation 2s infinite;
|
||||
height: 100%;
|
||||
@@ -88,20 +154,18 @@
|
||||
box-shadow: 1px 1px 1px #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
#tianai-captcha-slider-move-track-mask {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #00f4ab;
|
||||
|
||||
width: 0;
|
||||
height: 32px;
|
||||
background-color: #a9ffe5;
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import "./curve_draw.scss"
|
||||
import {Dom, CommonCaptcha, destroyEvent} from "../common/common.js"
|
||||
|
||||
/**
|
||||
* 曲线绘制验证码 - 用户需要绘制指定曲线
|
||||
*/
|
||||
|
||||
const TYPE = "CURVE_DRAW"
|
||||
function getTemplate(styleConfig) {
|
||||
return `
|
||||
<div id="tianai-captcha" class="tianai-captcha-slider tianai-captcha-curve-draw">
|
||||
<div class="click-tip">
|
||||
<span id="tianai-captcha-click-track-font" style="font-size: ${styleConfig.i18n?.curve_draw_title_size || '14px'}">${styleConfig.i18n?.curve_draw_title || '请沿虚线绘制曲线'}</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="draw-container" id="draw-container">
|
||||
<canvas id="tianai-captcha-draw-canvas" class="draw-canvas"></canvas>
|
||||
</div>
|
||||
<div class="draw-actions">
|
||||
<button class="draw-btn reset-btn" id="draw-reset-btn">重绘</button>
|
||||
<button class="draw-btn confirm-btn" id="draw-confirm-btn">确定</button>
|
||||
</div>
|
||||
<div class="tianai-captcha-tips" id="tianai-captcha-tips"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
class CurveDraw extends CommonCaptcha{
|
||||
constructor(boxEl, styleConfig) {
|
||||
super();
|
||||
this.boxEl = boxEl;
|
||||
this.styleConfig = styleConfig;
|
||||
this.type = TYPE;
|
||||
this.currentCaptchaData = {};
|
||||
this.isDrawing = false;
|
||||
this.drawPoints = [];
|
||||
}
|
||||
init(captchaData, endCallback, loadSuccessCallback) {
|
||||
// 重载样式
|
||||
this.destroy();
|
||||
this.boxEl.append(getTemplate(this.styleConfig));
|
||||
this.el = this.boxEl.find("#tianai-captcha");
|
||||
// 载入验证码
|
||||
this.loadCaptchaForData(this, captchaData);
|
||||
this.endCallback = endCallback;
|
||||
|
||||
if (loadSuccessCallback) {
|
||||
// 加载成功
|
||||
loadSuccessCallback(this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
initCanvas() {
|
||||
const canvas = document.getElementById('tianai-captcha-draw-canvas');
|
||||
const container = document.getElementById('draw-container');
|
||||
if (!canvas || !container) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
const rect = container.getBoundingClientRect();
|
||||
canvas.width = rect.width;
|
||||
canvas.height = rect.height;
|
||||
|
||||
// 绘制提示曲线(虚线)
|
||||
this.drawGuideCurve(ctx, canvas.width, canvas.height);
|
||||
|
||||
// 绑定绘制事件
|
||||
this.bindDrawEvents(canvas, ctx);
|
||||
}
|
||||
|
||||
drawGuideCurve(ctx, width, height) {
|
||||
const curveData = this.currentCaptchaData.curveData;
|
||||
if (!curveData || curveData.length < 2) return;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = 'rgba(64, 158, 255, 0.5)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([5, 5]);
|
||||
|
||||
ctx.moveTo(curveData[0].x * width, curveData[0].y * height);
|
||||
for (let i = 1; i < curveData.length; i++) {
|
||||
ctx.lineTo(curveData[i].x * width, curveData[i].y * height);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
bindDrawEvents(canvas, ctx) {
|
||||
const draw = (e) => {
|
||||
if (!this.isDrawing) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
let x, y;
|
||||
if (e.touches) {
|
||||
x = e.touches[0].clientX - rect.left;
|
||||
y = e.touches[0].clientY - rect.top;
|
||||
} else {
|
||||
x = e.offsetX;
|
||||
y = e.offsetY;
|
||||
}
|
||||
|
||||
this.drawPoints.push({x, y});
|
||||
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
ctx.strokeStyle = '#409eff';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
|
||||
if (this.drawPoints.length > 1) {
|
||||
const prev = this.drawPoints[this.drawPoints.length - 2];
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(prev.x, prev.y);
|
||||
ctx.lineTo(x, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
};
|
||||
|
||||
// 鼠标事件
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
this.isDrawing = true;
|
||||
this.currentCaptchaData.startTime = new Date();
|
||||
this.drawPoints = [];
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
this.drawGuideCurve(ctx, canvas.width, canvas.height);
|
||||
draw(e);
|
||||
});
|
||||
canvas.addEventListener('mousemove', draw);
|
||||
canvas.addEventListener('mouseup', () => {
|
||||
this.isDrawing = false;
|
||||
});
|
||||
canvas.addEventListener('mouseleave', () => {
|
||||
this.isDrawing = false;
|
||||
});
|
||||
|
||||
// 触摸事件
|
||||
canvas.addEventListener('touchstart', (e) => {
|
||||
this.isDrawing = true;
|
||||
this.currentCaptchaData.startTime = new Date();
|
||||
this.drawPoints = [];
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
this.drawGuideCurve(ctx, canvas.width, canvas.height);
|
||||
e.preventDefault();
|
||||
draw(e);
|
||||
});
|
||||
canvas.addEventListener('touchmove', (e) => {
|
||||
e.preventDefault();
|
||||
draw(e);
|
||||
});
|
||||
canvas.addEventListener('touchend', () => {
|
||||
this.isDrawing = false;
|
||||
});
|
||||
|
||||
// 重绘按钮
|
||||
const resetBtn = document.getElementById('draw-reset-btn');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', () => {
|
||||
this.drawPoints = [];
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
this.drawGuideCurve(ctx, canvas.width, canvas.height);
|
||||
});
|
||||
}
|
||||
|
||||
// 确定按钮
|
||||
const confirmBtn = document.getElementById('draw-confirm-btn');
|
||||
if (confirmBtn) {
|
||||
confirmBtn.addEventListener('click', () => {
|
||||
if (this.drawPoints.length > 0) {
|
||||
this.currentCaptchaData.stopTime = new Date();
|
||||
this.currentCaptchaData.drawPoints = this.drawPoints;
|
||||
this.endCallback(this.currentCaptchaData, this);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
destroy () {
|
||||
const existsCaptchaEl = this.boxEl.children("#tianai-captcha");
|
||||
if (existsCaptchaEl) {
|
||||
existsCaptchaEl.remove();
|
||||
}
|
||||
destroyEvent();
|
||||
}
|
||||
|
||||
loadCaptchaForData (that, data) {
|
||||
that.currentCaptchaData = {
|
||||
startTime: new Date(),
|
||||
drawPoints: [],
|
||||
curveData: data.data.curveData || []
|
||||
};
|
||||
that.currentCaptchaData.currentCaptchaId = data.data.id;
|
||||
// 初始化Canvas
|
||||
setTimeout(() => that.initCanvas(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
export default CurveDraw;
|
||||
@@ -0,0 +1,64 @@
|
||||
#tianai-captcha.tianai-captcha-curve-draw {
|
||||
box-sizing: border-box;
|
||||
|
||||
.click-tip {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
#tianai-captcha-click-track-font {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
.draw-container {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e8e8e8;
|
||||
|
||||
.draw-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: crosshair;
|
||||
touch-action: none;
|
||||
}
|
||||
}
|
||||
|
||||
.draw-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
|
||||
.draw-btn {
|
||||
flex: 1;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&.reset-btn {
|
||||
background-color: #e8e8e8;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
&.confirm-btn {
|
||||
background-image: linear-gradient(173deg, hsl(38.09deg 91% 57.89%) 0%, hsl(38.09deg 89.38% 71.74%) 100%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import "./curve_slider.scss"
|
||||
import {Dom, CommonCaptcha, destroyEvent} from "../common/common.js"
|
||||
|
||||
/**
|
||||
* 曲线滑块验证码 - 用户需要沿曲线轨迹滑动
|
||||
*/
|
||||
|
||||
const TYPE = "CURVE_SLIDER"
|
||||
function getTemplate(styleConfig) {
|
||||
return `
|
||||
<div id="tianai-captcha" class="tianai-captcha-slider tianai-captcha-curve-slider">
|
||||
<div class="click-tip">
|
||||
<span id="tianai-captcha-click-track-font" style="font-size: ${styleConfig.i18n?.curve_slider_title_size || '14px'}">${styleConfig.i18n?.curve_slider_title || '沿曲线轨迹滑动到终点'}</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="bg-img-div">
|
||||
<img id="tianai-captcha-slider-bg-img" src="" alt/>
|
||||
<canvas id="tianai-captcha-curve-canvas" class="curve-canvas"></canvas>
|
||||
<div class="curve-point" id="curve-point"></div>
|
||||
<div class="curve-end-point" id="curve-end-point"></div>
|
||||
</div>
|
||||
<div class="slider-bar" id="tianai-captcha-slider-bar">
|
||||
<div class="slider-bar-bg"></div>
|
||||
<div class="slider-bar-track" id="tianai-captcha-slider-track"></div>
|
||||
<div class="slider-bar-btn" id="tianai-captcha-slider-btn">
|
||||
<div class="slider-btn-icon"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tianai-captcha-tips" id="tianai-captcha-tips"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
class CurveSlider extends CommonCaptcha{
|
||||
constructor(boxEl, styleConfig) {
|
||||
super();
|
||||
this.boxEl = boxEl;
|
||||
this.styleConfig = styleConfig;
|
||||
this.type = TYPE;
|
||||
this.currentCaptchaData = {};
|
||||
this.isDragging = false;
|
||||
this.startX = 0;
|
||||
}
|
||||
init(captchaData, endCallback, loadSuccessCallback) {
|
||||
// 重载样式
|
||||
this.destroy();
|
||||
this.boxEl.append(getTemplate(this.styleConfig));
|
||||
this.el = this.boxEl.find("#tianai-captcha");
|
||||
// 载入验证码
|
||||
this.loadCaptchaForData(this, captchaData);
|
||||
this.endCallback = endCallback;
|
||||
|
||||
if (loadSuccessCallback) {
|
||||
// 加载成功
|
||||
loadSuccessCallback(this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
initCanvas() {
|
||||
const canvas = document.getElementById('tianai-captcha-curve-canvas');
|
||||
const bgImg = document.getElementById('tianai-captcha-slider-bg-img');
|
||||
if (!canvas || !bgImg) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = bgImg.width;
|
||||
canvas.height = bgImg.height;
|
||||
|
||||
// 绘制曲线
|
||||
this.drawCurve(ctx, canvas.width, canvas.height);
|
||||
|
||||
// 绑定滑动事件
|
||||
this.bindDragEvents(canvas);
|
||||
}
|
||||
|
||||
drawCurve(ctx, width, height) {
|
||||
const curveData = this.currentCaptchaData.curveData;
|
||||
if (!curveData || curveData.length < 2) return;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = 'rgba(255, 165, 0, 0.8)';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.setLineDash([5, 5]);
|
||||
|
||||
ctx.moveTo(curveData[0].x, curveData[0].y);
|
||||
for (let i = 1; i < curveData.length; i++) {
|
||||
ctx.lineTo(curveData[i].x, curveData[i].y);
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
// 绘制起点
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = '#409eff';
|
||||
ctx.arc(curveData[0].x, curveData[0].y, 8, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// 绘制终点
|
||||
const endPoint = curveData[curveData.length - 1];
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = '#ff4444';
|
||||
ctx.arc(endPoint.x, endPoint.y, 8, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
bindDragEvents(canvas) {
|
||||
const point = document.getElementById('curve-point');
|
||||
const sliderBtn = document.getElementById('tianai-captcha-slider-btn');
|
||||
if (!point || !sliderBtn) return;
|
||||
|
||||
// 鼠标按下
|
||||
sliderBtn.addEventListener('mousedown', (e) => {
|
||||
this.isDragging = true;
|
||||
this.startX = e.pageX;
|
||||
this.currentCaptchaData.startTime = new Date();
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// 鼠标移动
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!this.isDragging) return;
|
||||
const moveX = e.pageX - this.startX;
|
||||
const sliderBar = document.getElementById('tianai-captcha-slider-bar');
|
||||
const barWidth = sliderBar ? sliderBar.offsetWidth - 40 : 200;
|
||||
const left = Math.max(0, Math.min(moveX, barWidth));
|
||||
|
||||
sliderBtn.style.left = left + 'px';
|
||||
document.getElementById('tianai-captcha-slider-track').style.width = (left + 20) + 'px';
|
||||
|
||||
// 记录轨迹
|
||||
if (!this.currentCaptchaData.trackList) {
|
||||
this.currentCaptchaData.trackList = [];
|
||||
}
|
||||
this.currentCaptchaData.trackList.push({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
t: new Date().getTime() - this.currentCaptchaData.startTime.getTime()
|
||||
});
|
||||
});
|
||||
|
||||
// 鼠标松开
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (!this.isDragging) return;
|
||||
this.isDragging = false;
|
||||
this.currentCaptchaData.stopTime = new Date();
|
||||
const sliderBtn = document.getElementById('tianai-captcha-slider-btn');
|
||||
this.currentCaptchaData.left = sliderBtn ? sliderBtn.offsetLeft : 0;
|
||||
this.endCallback(this.currentCaptchaData, this);
|
||||
});
|
||||
|
||||
// 触摸事件
|
||||
sliderBtn.addEventListener('touchstart', (e) => {
|
||||
this.isDragging = true;
|
||||
this.startX = e.touches[0].pageX;
|
||||
this.currentCaptchaData.startTime = new Date();
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener('touchmove', (e) => {
|
||||
if (!this.isDragging) return;
|
||||
const moveX = e.touches[0].pageX - this.startX;
|
||||
const sliderBar = document.getElementById('tianai-captcha-slider-bar');
|
||||
const barWidth = sliderBar ? sliderBar.offsetWidth - 40 : 200;
|
||||
const left = Math.max(0, Math.min(moveX, barWidth));
|
||||
|
||||
sliderBtn.style.left = left + 'px';
|
||||
document.getElementById('tianai-captcha-slider-track').style.width = (left + 20) + 'px';
|
||||
});
|
||||
|
||||
document.addEventListener('touchend', () => {
|
||||
if (!this.isDragging) return;
|
||||
this.isDragging = false;
|
||||
this.currentCaptchaData.stopTime = new Date();
|
||||
const sliderBtn = document.getElementById('tianai-captcha-slider-btn');
|
||||
this.currentCaptchaData.left = sliderBtn ? sliderBtn.offsetLeft : 0;
|
||||
this.endCallback(this.currentCaptchaData, this);
|
||||
});
|
||||
}
|
||||
|
||||
destroy () {
|
||||
const existsCaptchaEl = this.boxEl.children("#tianai-captcha");
|
||||
if (existsCaptchaEl) {
|
||||
existsCaptchaEl.remove();
|
||||
}
|
||||
destroyEvent();
|
||||
}
|
||||
|
||||
loadCaptchaForData (that, data) {
|
||||
const bgImg = that.el.find("#tianai-captcha-slider-bg-img");
|
||||
bgImg.on("load",() => {
|
||||
that.currentCaptchaData = {
|
||||
bgImageWidth: bgImg.width(),
|
||||
bgImageHeight: bgImg.height(),
|
||||
startTime: new Date(),
|
||||
trackList: [],
|
||||
curveData: data.data.curveData || []
|
||||
};
|
||||
that.currentCaptchaData.currentCaptchaId = data.data.id;
|
||||
// 初始化Canvas
|
||||
setTimeout(() => that.initCanvas(), 100);
|
||||
})
|
||||
bgImg.attr("src", data.data.backgroundImage);
|
||||
}
|
||||
}
|
||||
|
||||
export default CurveSlider;
|
||||
@@ -0,0 +1,114 @@
|
||||
#tianai-captcha.tianai-captcha-curve-slider {
|
||||
box-sizing: border-box;
|
||||
|
||||
.click-tip {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
#tianai-captcha-click-track-font {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
.bg-img-div {
|
||||
position: relative;
|
||||
height: 180px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.curve-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.curve-point {
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background-color: #409eff;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
transform: translate(-50%, -50%);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.curve-end-point {
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background-color: #ff4444;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
transform: translate(-50%, -50%);
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.slider-bar {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
margin-top: 3px;
|
||||
|
||||
.slider-bar-bg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 4px;
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
.slider-bar-track {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border-radius: 4px;
|
||||
background-image: linear-gradient(90deg, #409eff, #66b1ff);
|
||||
transition: width 0.1s;
|
||||
}
|
||||
|
||||
.slider-bar-btn {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 40px;
|
||||
border-radius: 4px;
|
||||
background-image: linear-gradient(173deg, hsl(38.09deg 91% 57.89%) 0%, hsl(38.09deg 89.38% 71.74%) 100%);
|
||||
cursor: move;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
transition: transform 0.1s;
|
||||
|
||||
.slider-btn-icon {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 6px 0 6px 8px;
|
||||
border-color: transparent transparent transparent #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import "./icon_click.scss"
|
||||
import {Dom, CommonCaptcha, move, initConfig, destroyEvent} from "../common/common.js"
|
||||
|
||||
/**
|
||||
* 图标点选验证码
|
||||
*/
|
||||
|
||||
const TYPE = "ICON_CLICK"
|
||||
function getTemplate(styleConfig) {
|
||||
return `
|
||||
<div id="tianai-captcha" class="tianai-captcha-slider tianai-captcha-icon-click">
|
||||
<div class="click-tip">
|
||||
<span id="tianai-captcha-click-track-font" style="font-size: ${styleConfig.i18n?.icon_click_title_size || '14px'}">${styleConfig.i18n?.icon_click_title || '请依次点击图中的图标'}</span>
|
||||
<img src="" id="tianai-captcha-tip-img" class="tip-img">
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="bg-img-div">
|
||||
<img id="tianai-captcha-slider-bg-img" src="" alt/>
|
||||
<canvas id="tianai-captcha-slider-bg-canvas"></canvas>
|
||||
<div id="bg-img-click-mask"></div>
|
||||
</div>
|
||||
<div class="tianai-captcha-tips" id="tianai-captcha-tips"></div>
|
||||
</div>
|
||||
<div class="click-confirm-btn">确定</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
class IconClick extends CommonCaptcha{
|
||||
constructor(boxEl, styleConfig) {
|
||||
super();
|
||||
this.boxEl = boxEl;
|
||||
this.styleConfig = styleConfig;
|
||||
this.type = TYPE;
|
||||
this.currentCaptchaData = {}
|
||||
}
|
||||
init(captchaData, endCallback, loadSuccessCallback) {
|
||||
// 重载样式
|
||||
this.destroy();
|
||||
this.boxEl.append(getTemplate(this.styleConfig));
|
||||
this.el = this.boxEl.find("#tianai-captcha");
|
||||
// 载入验证码
|
||||
this.loadCaptchaForData(this, captchaData);
|
||||
this.endCallback = endCallback;
|
||||
const moveFun = move.bind(null, this);
|
||||
// 绑定事件
|
||||
this.el.find("#bg-img-click-mask").click((event) => {
|
||||
if(event.target.className === "click-span") {
|
||||
return;
|
||||
}
|
||||
this.currentCaptchaData.clickCount++;
|
||||
const trackList = this.currentCaptchaData.trackList;
|
||||
if (this.currentCaptchaData.clickCount === 1) {
|
||||
this.currentCaptchaData.startTime = new Date();
|
||||
// move 轨迹
|
||||
window.addEventListener("mousemove", moveFun);
|
||||
this.currentCaptchaData.startX = event.offsetX;
|
||||
this.currentCaptchaData.startY = event.offsetY;
|
||||
}
|
||||
const startTime = this.currentCaptchaData.startTime;
|
||||
trackList.push({
|
||||
x: Math.round(event.offsetX),
|
||||
y: Math.round(event.offsetY),
|
||||
type: "click",
|
||||
t: (new Date().getTime() - startTime.getTime())
|
||||
});
|
||||
const left = event.offsetX - 12;
|
||||
const top = event.offsetY - 12;
|
||||
this.el.find("#bg-img-click-mask").append("<span class='click-span' style='left:" + left + "px;top: " + top + "px'>" + this.currentCaptchaData.clickCount + "</span>")
|
||||
});
|
||||
this.el.find(".click-confirm-btn").click(() => {
|
||||
if (this.currentCaptchaData.clickCount > 0) {
|
||||
// 校验
|
||||
this.currentCaptchaData.stopTime = new Date();
|
||||
window.removeEventListener("mousemove", moveFun);
|
||||
this.endCallback(this.currentCaptchaData,this);
|
||||
}
|
||||
});
|
||||
|
||||
if (loadSuccessCallback) {
|
||||
// 加载成功
|
||||
loadSuccessCallback(this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
destroy () {
|
||||
const existsCaptchaEl = this.boxEl.children("#tianai-captcha");
|
||||
if (existsCaptchaEl) {
|
||||
existsCaptchaEl.remove();
|
||||
}
|
||||
destroyEvent();
|
||||
}
|
||||
loadCaptchaForData (that, data) {
|
||||
const bgImg = that.el.find("#tianai-captcha-slider-bg-img");
|
||||
const tipImg = that.el.find("#tianai-captcha-tip-img");
|
||||
bgImg.on("load",() => {
|
||||
that.currentCaptchaData = initConfig(bgImg.width(), bgImg.height(), tipImg.width(), tipImg.height());
|
||||
that.currentCaptchaData.currentCaptchaId = data.data.id;
|
||||
})
|
||||
bgImg.attr("src", data.data.backgroundImage);
|
||||
tipImg.attr("src", data.data.templateImage);
|
||||
}
|
||||
}
|
||||
|
||||
export default IconClick;
|
||||
@@ -0,0 +1,75 @@
|
||||
#tianai-captcha.tianai-captcha-icon-click {
|
||||
box-sizing: border-box;
|
||||
|
||||
.click-tip {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
.tip-img {
|
||||
height: 35px;
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
}
|
||||
#tianai-captcha-click-track-font {
|
||||
font-size: 14px;
|
||||
display: inline-block;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
position: absolute;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.slider-bottom {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
.content {
|
||||
#bg-img-click-mask {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
|
||||
.click-span {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-radius: 50%;
|
||||
background-color: #409eff;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
text-align: center;
|
||||
line-height: 24px;
|
||||
color: #fff;
|
||||
border: 2px solid #fff;
|
||||
box-sizing: content-box;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.click-confirm-btn {
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
border-radius: 4px;
|
||||
background-image: linear-gradient(173deg, hsl(38.09deg 91% 57.89%) 0%, hsl(38.09deg 89.38% 71.74%) 100%);
|
||||
font-size: 15px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
line-height: 35px;
|
||||
color: #fff;
|
||||
margin-top: 3px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import "./jigsaw.scss"
|
||||
import {Dom, CommonCaptcha, move, initConfig, destroyEvent} from "../common/common.js"
|
||||
|
||||
/**
|
||||
* 乱序拼图验证码 - 用户需要拖动拼图块到正确位置
|
||||
*/
|
||||
|
||||
const TYPE = "JIGSAW"
|
||||
function getTemplate(styleConfig) {
|
||||
return `
|
||||
<div id="tianai-captcha" class="tianai-captcha-slider tianai-captcha-jigsaw">
|
||||
<div class="click-tip">
|
||||
<span id="tianai-captcha-click-track-font" style="font-size: ${styleConfig.i18n?.jigsaw_title_size || '14px'}">${styleConfig.i18n?.jigsaw_title || '拖动拼图块到正确位置'}</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="bg-img-div">
|
||||
<img id="tianai-captcha-slider-bg-img" src="" alt/>
|
||||
<div class="jigsaw-block" id="jigsaw-block"></div>
|
||||
</div>
|
||||
<div class="slider-bar" id="tianai-captcha-slider-bar">
|
||||
<div class="slider-bar-bg"></div>
|
||||
<div class="slider-bar-track" id="tianai-captcha-slider-track">
|
||||
</div>
|
||||
<div class="slider-bar-btn" id="tianai-captcha-slider-btn">
|
||||
<div class="slider-btn-icon">
|
||||
<div class="click-mask-btn" id="tianai-captcha-click-mask-btn"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tianai-captcha-tips" id="tianai-captcha-tips"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
class Jigsaw extends CommonCaptcha{
|
||||
constructor(boxEl, styleConfig) {
|
||||
super();
|
||||
this.boxEl = boxEl;
|
||||
this.styleConfig = styleConfig;
|
||||
this.type = TYPE;
|
||||
this.currentCaptchaData = {};
|
||||
}
|
||||
init(captchaData, endCallback, loadSuccessCallback) {
|
||||
// 重载样式
|
||||
this.destroy();
|
||||
this.boxEl.append(getTemplate(this.styleConfig));
|
||||
this.el = this.boxEl.find("#tianai-captcha");
|
||||
// 载入验证码
|
||||
this.loadCaptchaForData(this, captchaData);
|
||||
this.endCallback = endCallback;
|
||||
const moveFun = move.bind(null, this);
|
||||
|
||||
// 绑定事件
|
||||
this.el.find("#tianai-captcha-click-mask-btn").mousedown((event) => {
|
||||
this.currentCaptchaData.moveFlag = true;
|
||||
this.currentCaptchaData.startTime = new Date();
|
||||
// move 轨迹
|
||||
window.addEventListener("mousemove", moveFun);
|
||||
this.currentCaptchaData.startX = event.pageX;
|
||||
this.currentCaptchaData.startY = event.pageY;
|
||||
this.el.find("#tianai-captcha-click-mask-btn").css("transition", "none");
|
||||
});
|
||||
window.addEventListener("mouseup", (event) => {
|
||||
if (!this.currentCaptchaData.moveFlag) {
|
||||
return;
|
||||
}
|
||||
this.currentCaptchaData.moveFlag = false;
|
||||
this.currentCaptchaData.stopTime = new Date();
|
||||
window.removeEventListener("mousemove", moveFun);
|
||||
const left = this.el.find("#tianai-captcha-slider-btn").css("left") ? this.el.find("#tianai-captcha-slider-btn").css("left").split("px")[0] : 0;
|
||||
this.currentCaptchaData.left = left;
|
||||
this.endCallback(this.currentCaptchaData,this);
|
||||
});
|
||||
|
||||
if (loadSuccessCallback) {
|
||||
// 加载成功
|
||||
loadSuccessCallback(this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
destroy () {
|
||||
const existsCaptchaEl = this.boxEl.children("#tianai-captcha");
|
||||
if (existsCaptchaEl) {
|
||||
existsCaptchaEl.remove();
|
||||
}
|
||||
destroyEvent();
|
||||
}
|
||||
|
||||
loadCaptchaForData (that, data) {
|
||||
const bgImg = that.el.find("#tianai-captcha-slider-bg-img");
|
||||
bgImg.on("load",() => {
|
||||
that.currentCaptchaData = initConfig(bgImg.width(), bgImg.height(), data.data.blockSize, data.data.blockSize);
|
||||
that.currentCaptchaData.currentCaptchaId = data.data.id;
|
||||
// 设置拼图块位置
|
||||
that.el.find("#jigsaw-block").css({
|
||||
"width": data.data.blockSize + "px",
|
||||
"height": data.data.blockSize + "px",
|
||||
"left": data.data.blockPositionX + "px",
|
||||
"top": data.data.blockPositionY + "px",
|
||||
"background-image": "url(" + data.data.blockImage + ")",
|
||||
"background-size": bgImg.width() + "px " + bgImg.height() + "px",
|
||||
"background-position": "-" + data.data.blockPositionX + "px -" + data.data.blockPositionY + "px"
|
||||
});
|
||||
that.el.find("#tianai-captcha-slider-track").css("height", that.el.find("#tianai-captcha-slider-bar").outerHeight() + "px");
|
||||
})
|
||||
bgImg.attr("src", data.data.backgroundImage);
|
||||
}
|
||||
}
|
||||
|
||||
export default Jigsaw;
|
||||
@@ -0,0 +1,114 @@
|
||||
#tianai-captcha.tianai-captcha-jigsaw {
|
||||
box-sizing: border-box;
|
||||
|
||||
.click-tip {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
#tianai-captcha-click-track-font {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
|
||||
.bg-img-div {
|
||||
position: relative;
|
||||
height: 180px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.jigsaw-block {
|
||||
position: absolute;
|
||||
cursor: move;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
border-radius: 4px;
|
||||
transition: box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slider-bar {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
margin-top: 3px;
|
||||
|
||||
.slider-bar-bg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 4px;
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
.slider-bar-track {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border-radius: 4px;
|
||||
background-image: linear-gradient(90deg, #409eff, #66b1ff);
|
||||
transition: width 0.1s;
|
||||
}
|
||||
|
||||
.slider-bar-btn {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 40px;
|
||||
border-radius: 4px;
|
||||
background-image: linear-gradient(173deg, hsl(38.09deg 91% 57.89%) 0%, hsl(38.09deg 89.38% 71.74%) 100%);
|
||||
cursor: move;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
transition: transform 0.1s;
|
||||
|
||||
.slider-btn-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 6px 0 6px 8px;
|
||||
border-color: transparent transparent transparent #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.click-mask-btn {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import "./proof_of_work.scss"
|
||||
import {Dom, CommonCaptcha, destroyEvent} from "../common/common.js"
|
||||
|
||||
/**
|
||||
* 工作量证明验证码 - 用户需要完成计算工作
|
||||
*/
|
||||
|
||||
const TYPE = "PROOF_OF_WORK"
|
||||
function getTemplate(styleConfig) {
|
||||
return `
|
||||
<div id="tianai-captcha" class="tianai-captcha-slider tianai-captcha-proof-of-work">
|
||||
<div class="click-tip">
|
||||
<span id="tianai-captcha-click-track-font" style="font-size: ${styleConfig.i18n?.proof_of_work_title_size || '14px'}">${styleConfig.i18n?.proof_of_work_title || '请完成工作量证明'}</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="pow-container">
|
||||
<div class="pow-info">
|
||||
<div class="pow-challenge" id="pow-challenge">
|
||||
<span class="label">挑战:</span>
|
||||
<span class="value" id="pow-challenge-value">-</span>
|
||||
</div>
|
||||
<div class="pow-difficulty" id="pow-difficulty">
|
||||
<span class="label">难度:</span>
|
||||
<span class="value" id="pow-difficulty-value">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pow-progress">
|
||||
<div class="progress-bar" id="pow-progress-bar"></div>
|
||||
<span class="progress-text" id="pow-progress-text">0%</span>
|
||||
</div>
|
||||
<div class="pow-status" id="pow-status">等待开始...</div>
|
||||
</div>
|
||||
<div class="pow-actions">
|
||||
<button class="pow-btn start-btn" id="pow-start-btn">开始计算</button>
|
||||
</div>
|
||||
<div class="tianai-captcha-tips" id="tianai-captcha-tips"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
class ProofOfWork extends CommonCaptcha{
|
||||
constructor(boxEl, styleConfig) {
|
||||
super();
|
||||
this.boxEl = boxEl;
|
||||
this.styleConfig = styleConfig;
|
||||
this.type = TYPE;
|
||||
this.currentCaptchaData = {};
|
||||
this.isComputing = false;
|
||||
this.worker = null;
|
||||
}
|
||||
init(captchaData, endCallback, loadSuccessCallback) {
|
||||
// 重载样式
|
||||
this.destroy();
|
||||
this.boxEl.append(getTemplate(this.styleConfig));
|
||||
this.el = this.boxEl.find("#tianai-captcha");
|
||||
// 载入验证码
|
||||
this.loadCaptchaForData(this, captchaData);
|
||||
this.endCallback = endCallback;
|
||||
|
||||
if (loadSuccessCallback) {
|
||||
// 加载成功
|
||||
loadSuccessCallback(this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
const startBtn = document.getElementById('pow-start-btn');
|
||||
if (!startBtn) return;
|
||||
|
||||
startBtn.addEventListener('click', () => {
|
||||
if (this.isComputing) return;
|
||||
this.startComputation();
|
||||
});
|
||||
}
|
||||
|
||||
startComputation() {
|
||||
this.isComputing = true;
|
||||
this.currentCaptchaData.startTime = new Date();
|
||||
|
||||
const startBtn = document.getElementById('pow-start-btn');
|
||||
if (startBtn) {
|
||||
startBtn.disabled = true;
|
||||
startBtn.textContent = '计算中...';
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
this.updateStatus('正在计算工作量证明...');
|
||||
|
||||
// 模拟计算过程(实际应用中应使用Web Worker)
|
||||
this.simulateComputation();
|
||||
}
|
||||
|
||||
simulateComputation() {
|
||||
const challenge = this.currentCaptchaData.challenge || 'demo';
|
||||
const difficulty = this.currentCaptchaData.difficulty || 4;
|
||||
let nonce = 0;
|
||||
const target = '0'.repeat(difficulty);
|
||||
|
||||
const compute = () => {
|
||||
if (!this.isComputing) return;
|
||||
|
||||
// 简单的SHA-256模拟(实际应用中应使用真实的SHA-256)
|
||||
const hash = this.simpleHash(challenge + nonce);
|
||||
|
||||
// 更新进度
|
||||
const progress = Math.min(99, (nonce / 10000) * 100);
|
||||
this.updateProgress(progress);
|
||||
|
||||
if (hash.startsWith(target)) {
|
||||
// 找到解决方案
|
||||
this.currentCaptchaData.nonce = nonce;
|
||||
this.currentCaptchaData.hash = hash;
|
||||
this.currentCaptchaData.stopTime = new Date();
|
||||
this.updateProgress(100);
|
||||
this.updateStatus('计算完成!');
|
||||
this.endCallback(this.currentCaptchaData, this);
|
||||
} else {
|
||||
nonce++;
|
||||
if (nonce % 100 === 0) {
|
||||
// 每100次更新一次UI
|
||||
setTimeout(compute, 0);
|
||||
} else {
|
||||
compute();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
compute();
|
||||
}
|
||||
|
||||
simpleHash(str) {
|
||||
// 简单的哈希函数(仅用于演示)
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
// 转换为16进制字符串
|
||||
return Math.abs(hash).toString(16).padStart(8, '0');
|
||||
}
|
||||
|
||||
updateProgress(percent) {
|
||||
const progressBar = document.getElementById('pow-progress-bar');
|
||||
const progressText = document.getElementById('pow-progress-text');
|
||||
if (progressBar) {
|
||||
progressBar.style.width = percent + '%';
|
||||
}
|
||||
if (progressText) {
|
||||
progressText.textContent = Math.round(percent) + '%';
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus(text) {
|
||||
const statusEl = document.getElementById('pow-status');
|
||||
if (statusEl) {
|
||||
statusEl.textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.isComputing = false;
|
||||
const existsCaptchaEl = this.boxEl.children("#tianai-captcha");
|
||||
if (existsCaptchaEl) {
|
||||
existsCaptchaEl.remove();
|
||||
}
|
||||
destroyEvent();
|
||||
}
|
||||
|
||||
loadCaptchaForData (that, data) {
|
||||
that.currentCaptchaData = {
|
||||
startTime: new Date(),
|
||||
challenge: data.data.challenge || 'demo_challenge',
|
||||
difficulty: data.data.difficulty || 4,
|
||||
nonce: 0,
|
||||
hash: ''
|
||||
};
|
||||
that.currentCaptchaData.currentCaptchaId = data.data.id;
|
||||
|
||||
// 更新UI
|
||||
const challengeValue = document.getElementById('pow-challenge-value');
|
||||
const difficultyValue = document.getElementById('pow-difficulty-value');
|
||||
if (challengeValue) {
|
||||
challengeValue.textContent = that.currentCaptchaData.challenge.substring(0, 20) + '...';
|
||||
}
|
||||
if (difficultyValue) {
|
||||
difficultyValue.textContent = that.currentCaptchaData.difficulty;
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
setTimeout(() => that.bindEvents(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
export default ProofOfWork;
|
||||
@@ -0,0 +1,104 @@
|
||||
#tianai-captcha.tianai-captcha-proof-of-work {
|
||||
box-sizing: border-box;
|
||||
|
||||
.click-tip {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
#tianai-captcha-click-track-font {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
.pow-container {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.pow-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.pow-challenge,
|
||||
.pow-difficulty {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
|
||||
.label {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pow-progress {
|
||||
position: relative;
|
||||
height: 20px;
|
||||
background-color: #e8e8e8;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background-image: linear-gradient(90deg, #409eff, #66b1ff);
|
||||
transition: width 0.3s;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 11px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.pow-status {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.pow-actions {
|
||||
.pow-btn {
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&.start-btn {
|
||||
background-image: linear-gradient(173deg, hsl(38.09deg 91% 57.89%) 0%, hsl(38.09deg 89.38% 71.74%) 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import "./scratch.scss"
|
||||
import {Dom, CommonCaptcha, destroyEvent} from "../common/common.js"
|
||||
|
||||
/**
|
||||
* 刮刮乐验证码 - 用户需要刮开涂层
|
||||
*/
|
||||
|
||||
const TYPE = "SCRATCH"
|
||||
function getTemplate(styleConfig) {
|
||||
return `
|
||||
<div id="tianai-captcha" class="tianai-captcha-slider tianai-captcha-scratch">
|
||||
<div class="scratch-tip">
|
||||
<span id="tianai-captcha-scratch-track-font" style="font-size: ${styleConfig.i18n?.scratch_title_size || '14px'}">${styleConfig.i18n?.scratch_title || '请刮开涂层完成验证'}</span>
|
||||
<span class="scratch-progress" id="scratch-progress">0%</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="scratch-container" id="scratch-container">
|
||||
<img id="tianai-captcha-scratch-bg-img" src="" alt class="scratch-bg-img"/>
|
||||
<canvas id="tianai-captcha-scratch-canvas" class="scratch-canvas"></canvas>
|
||||
</div>
|
||||
<div class="tianai-captcha-tips" id="tianai-captcha-tips"></div>
|
||||
</div>
|
||||
<div class="click-confirm-btn">确定</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
class Scratch extends CommonCaptcha{
|
||||
constructor(boxEl, styleConfig) {
|
||||
super();
|
||||
this.boxEl = boxEl;
|
||||
this.styleConfig = styleConfig;
|
||||
this.type = TYPE;
|
||||
this.currentCaptchaData = {};
|
||||
this.isDrawing = false;
|
||||
this.scratchPercentage = 0;
|
||||
}
|
||||
init(captchaData, endCallback, loadSuccessCallback) {
|
||||
// 重载样式
|
||||
this.destroy();
|
||||
this.boxEl.append(getTemplate(this.styleConfig));
|
||||
this.el = this.boxEl.find("#tianai-captcha");
|
||||
// 载入验证码
|
||||
this.loadCaptchaForData(this, captchaData);
|
||||
this.endCallback = endCallback;
|
||||
|
||||
if (loadSuccessCallback) {
|
||||
// 加载成功
|
||||
loadSuccessCallback(this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
initCanvas() {
|
||||
const canvas = document.getElementById('tianai-captcha-scratch-canvas');
|
||||
const container = document.getElementById('scratch-container');
|
||||
if (!canvas || !container) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
const rect = container.getBoundingClientRect();
|
||||
canvas.width = rect.width;
|
||||
canvas.height = rect.height;
|
||||
|
||||
// 绘制灰色遮罩层
|
||||
ctx.fillStyle = '#c0c0c0';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 绘制提示文字
|
||||
ctx.fillStyle = '#999';
|
||||
ctx.font = '14px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('请用鼠标刮开此区域', canvas.width / 2, canvas.height / 2);
|
||||
|
||||
// 绑定刮擦事件
|
||||
this.bindScratchEvents(canvas, ctx);
|
||||
}
|
||||
|
||||
bindScratchEvents(canvas, ctx) {
|
||||
const scratch = (e) => {
|
||||
if (!this.isDrawing) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
let x, y;
|
||||
if (e.touches) {
|
||||
x = e.touches[0].clientX - rect.left;
|
||||
y = e.touches[0].clientY - rect.top;
|
||||
} else {
|
||||
x = e.offsetX;
|
||||
y = e.offsetY;
|
||||
}
|
||||
|
||||
ctx.globalCompositeOperation = 'destination-out';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 15, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
this.calculateScratchPercentage(canvas, ctx);
|
||||
};
|
||||
|
||||
// 鼠标事件
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
this.isDrawing = true;
|
||||
this.currentCaptchaData.startTime = new Date();
|
||||
scratch(e);
|
||||
});
|
||||
canvas.addEventListener('mousemove', scratch);
|
||||
canvas.addEventListener('mouseup', () => {
|
||||
this.isDrawing = false;
|
||||
});
|
||||
canvas.addEventListener('mouseleave', () => {
|
||||
this.isDrawing = false;
|
||||
});
|
||||
|
||||
// 触摸事件
|
||||
canvas.addEventListener('touchstart', (e) => {
|
||||
this.isDrawing = true;
|
||||
this.currentCaptchaData.startTime = new Date();
|
||||
e.preventDefault();
|
||||
scratch(e);
|
||||
});
|
||||
canvas.addEventListener('touchmove', (e) => {
|
||||
e.preventDefault();
|
||||
scratch(e);
|
||||
});
|
||||
canvas.addEventListener('touchend', () => {
|
||||
this.isDrawing = false;
|
||||
});
|
||||
}
|
||||
|
||||
calculateScratchPercentage(canvas, ctx) {
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const pixels = imageData.data;
|
||||
let transparent = 0;
|
||||
const total = pixels.length / 4;
|
||||
|
||||
for (let i = 3; i < pixels.length; i += 4) {
|
||||
if (pixels[i] === 0) {
|
||||
transparent++;
|
||||
}
|
||||
}
|
||||
|
||||
this.scratchPercentage = Math.round((transparent / total) * 100);
|
||||
const progressEl = this.el.find("#scratch-progress");
|
||||
progressEl.text(this.scratchPercentage + '%');
|
||||
|
||||
// 超过60%自动完成
|
||||
if (this.scratchPercentage >= 60 && !this.currentCaptchaData.verified) {
|
||||
this.currentCaptchaData.verified = true;
|
||||
this.currentCaptchaData.stopTime = new Date();
|
||||
this.currentCaptchaData.scratchPercentage = this.scratchPercentage;
|
||||
this.endCallback(this.currentCaptchaData, this);
|
||||
}
|
||||
}
|
||||
|
||||
destroy () {
|
||||
const existsCaptchaEl = this.boxEl.children("#tianai-captcha");
|
||||
if (existsCaptchaEl) {
|
||||
existsCaptchaEl.remove();
|
||||
}
|
||||
destroyEvent();
|
||||
}
|
||||
|
||||
loadCaptchaForData (that, data) {
|
||||
const bgImg = that.el.find("#tianai-captcha-scratch-bg-img");
|
||||
bgImg.on("load",() => {
|
||||
that.currentCaptchaData = {
|
||||
bgImageWidth: bgImg.width(),
|
||||
bgImageHeight: bgImg.height(),
|
||||
startTime: new Date(),
|
||||
trackList: [],
|
||||
verified: false,
|
||||
scratchPercentage: 0
|
||||
};
|
||||
that.currentCaptchaData.currentCaptchaId = data.data.id;
|
||||
// 初始化Canvas
|
||||
setTimeout(() => that.initCanvas(), 100);
|
||||
})
|
||||
bgImg.attr("src", data.data.backgroundImage);
|
||||
}
|
||||
}
|
||||
|
||||
export default Scratch;
|
||||
@@ -0,0 +1,73 @@
|
||||
#tianai-captcha.tianai-captcha-scratch {
|
||||
box-sizing: border-box;
|
||||
|
||||
.scratch-tip {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
#tianai-captcha-scratch-track-font {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.scratch-progress {
|
||||
font-size: 13px;
|
||||
color: #409eff;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.slider-bottom {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
.content {
|
||||
.scratch-container {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
|
||||
.scratch-bg-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.scratch-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
touch-action: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.click-confirm-btn {
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
border-radius: 4px;
|
||||
background-image: linear-gradient(173deg, hsl(38.09deg 91% 57.89%) 0%, hsl(38.09deg 89.38% 71.74%) 100%);
|
||||
font-size: 15px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
line-height: 35px;
|
||||
color: #fff;
|
||||
margin-top: 3px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import "./word_order_click.scss"
|
||||
import {Dom, CommonCaptcha, move, initConfig, destroyEvent} from "../common/common.js"
|
||||
|
||||
/**
|
||||
* 语序点选验证码 - 用户需要按顺序点击文字
|
||||
*/
|
||||
|
||||
const TYPE = "WORD_ORDER_CLICK"
|
||||
function getTemplate(styleConfig) {
|
||||
return `
|
||||
<div id="tianai-captcha" class="tianai-captcha-slider tianai-captcha-word-order-click">
|
||||
<div class="click-tip">
|
||||
<span id="tianai-captcha-click-track-font" style="font-size: ${styleConfig.i18n?.word_order_click_title_size || '14px'}">${styleConfig.i18n?.word_order_click_title || '请按顺序点击文字'}</span>
|
||||
<div class="word-order-hint" id="word-order-hint"></div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="bg-img-div">
|
||||
<img id="tianai-captcha-slider-bg-img" src="" alt/>
|
||||
<canvas id="tianai-captcha-slider-bg-canvas"></canvas>
|
||||
<div id="bg-img-click-mask"></div>
|
||||
</div>
|
||||
<div class="tianai-captcha-tips" id="tianai-captcha-tips"></div>
|
||||
</div>
|
||||
<div class="click-confirm-btn">确定</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
class WordOrderClick extends CommonCaptcha{
|
||||
constructor(boxEl, styleConfig) {
|
||||
super();
|
||||
this.boxEl = boxEl;
|
||||
this.styleConfig = styleConfig;
|
||||
this.type = TYPE;
|
||||
this.currentCaptchaData = {}
|
||||
}
|
||||
init(captchaData, endCallback, loadSuccessCallback) {
|
||||
// 重载样式
|
||||
this.destroy();
|
||||
this.boxEl.append(getTemplate(this.styleConfig));
|
||||
this.el = this.boxEl.find("#tianai-captcha");
|
||||
// 载入验证码
|
||||
this.loadCaptchaForData(this, captchaData);
|
||||
this.endCallback = endCallback;
|
||||
const moveFun = move.bind(null, this);
|
||||
|
||||
// 初始化点击顺序提示
|
||||
this.updateOrderHint();
|
||||
|
||||
// 绑定事件
|
||||
this.el.find("#bg-img-click-mask").click((event) => {
|
||||
if(event.target.className === "click-span") {
|
||||
return;
|
||||
}
|
||||
this.currentCaptchaData.clickCount++;
|
||||
const trackList = this.currentCaptchaData.trackList;
|
||||
if (this.currentCaptchaData.clickCount === 1) {
|
||||
this.currentCaptchaData.startTime = new Date();
|
||||
// move 轨迹
|
||||
window.addEventListener("mousemove", moveFun);
|
||||
this.currentCaptchaData.startX = event.offsetX;
|
||||
this.currentCaptchaData.startY = event.offsetY;
|
||||
}
|
||||
const startTime = this.currentCaptchaData.startTime;
|
||||
trackList.push({
|
||||
x: Math.round(event.offsetX),
|
||||
y: Math.round(event.offsetY),
|
||||
type: "click",
|
||||
t: (new Date().getTime() - startTime.getTime())
|
||||
});
|
||||
const left = event.offsetX - 12;
|
||||
const top = event.offsetY - 12;
|
||||
this.el.find("#bg-img-click-mask").append("<span class='click-span' style='left:" + left + "px;top: " + top + "px'>" + this.currentCaptchaData.clickCount + "</span>")
|
||||
this.updateOrderHint();
|
||||
});
|
||||
|
||||
// 重新开始按钮
|
||||
this.el.find(".click-reset-btn").click(() => {
|
||||
this.currentCaptchaData.clickCount = 0;
|
||||
this.currentCaptchaData.trackList = [];
|
||||
this.el.find("#bg-img-click-mask").find(".click-span").remove();
|
||||
this.updateOrderHint();
|
||||
});
|
||||
|
||||
this.el.find(".click-confirm-btn").click(() => {
|
||||
if (this.currentCaptchaData.clickCount > 0) {
|
||||
// 校验
|
||||
this.currentCaptchaData.stopTime = new Date();
|
||||
window.removeEventListener("mousemove", moveFun);
|
||||
this.endCallback(this.currentCaptchaData,this);
|
||||
}
|
||||
});
|
||||
|
||||
if (loadSuccessCallback) {
|
||||
// 加载成功
|
||||
loadSuccessCallback(this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
updateOrderHint() {
|
||||
const hintEl = this.el.find("#word-order-hint");
|
||||
const count = this.currentCaptchaData.clickCount || 0;
|
||||
const maxCount = this.currentCaptchaData.maxClickCount || 4;
|
||||
let html = '';
|
||||
for (let i = 1; i <= maxCount; i++) {
|
||||
if (i <= count) {
|
||||
html += '<span class="hint-dot active">' + i + '</span>';
|
||||
} else {
|
||||
html += '<span class="hint-dot">' + i + '</span>';
|
||||
}
|
||||
}
|
||||
hintEl.html(html);
|
||||
}
|
||||
|
||||
destroy () {
|
||||
const existsCaptchaEl = this.boxEl.children("#tianai-captcha");
|
||||
if (existsCaptchaEl) {
|
||||
existsCaptchaEl.remove();
|
||||
}
|
||||
destroyEvent();
|
||||
}
|
||||
loadCaptchaForData (that, data) {
|
||||
const bgImg = that.el.find("#tianai-captcha-slider-bg-img");
|
||||
bgImg.on("load",() => {
|
||||
that.currentCaptchaData = initConfig(bgImg.width(), bgImg.height(), 0, 0);
|
||||
that.currentCaptchaData.currentCaptchaId = data.data.id;
|
||||
that.currentCaptchaData.maxClickCount = data.data.maxClickCount || 4;
|
||||
})
|
||||
bgImg.attr("src", data.data.backgroundImage);
|
||||
}
|
||||
}
|
||||
|
||||
export default WordOrderClick;
|
||||
@@ -0,0 +1,105 @@
|
||||
#tianai-captcha.tianai-captcha-word-order-click {
|
||||
box-sizing: border-box;
|
||||
|
||||
.click-tip {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
#tianai-captcha-click-track-font {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.word-order-hint {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
.hint-dot {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: #e0e0e0;
|
||||
color: #999;
|
||||
font-size: 11px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.active {
|
||||
background-color: #409eff;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slider-bottom {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
.content {
|
||||
#bg-img-click-mask {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
|
||||
.click-span {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-radius: 50%;
|
||||
background-color: #409eff;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
text-align: center;
|
||||
line-height: 24px;
|
||||
color: #fff;
|
||||
border: 2px solid #fff;
|
||||
box-sizing: content-box;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.click-confirm-btn {
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
border-radius: 4px;
|
||||
background-image: linear-gradient(173deg, hsl(38.09deg 91% 57.89%) 0%, hsl(38.09deg 89.38% 71.74%) 100%);
|
||||
font-size: 15px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
line-height: 35px;
|
||||
color: #fff;
|
||||
margin-top: 3px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.click-reset-btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
font-size: 12px;
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user