-
-
-
用户登录
-
+
+
+
+
+ document.querySelectorAll('.type-btn').forEach(btn => {
+ btn.addEventListener('click', function() {
+ document.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active'));
+ this.classList.add('active');
+ initCaptcha(this.dataset.type);
+ });
+ });
+
+ initCaptcha('SLIDER');
+
-
diff --git a/tianai-captcha-web-sdk/src/captcha/angle/angle.js b/tianai-captcha-web-sdk/src/captcha/angle/angle.js
new file mode 100644
index 0000000..20159a6
--- /dev/null
+++ b/tianai-captcha-web-sdk/src/captcha/angle/angle.js
@@ -0,0 +1,181 @@
+import "./angle.scss"
+import {Dom, CommonCaptcha, destroyEvent} from "../common/common.js"
+
+/**
+ * 角度验证验证码 - 用户需要旋转图片到正确角度
+ */
+
+const TYPE = "ANGLE"
+function getTemplate(styleConfig) {
+ return `
+
+
+ ${styleConfig.i18n?.angle_title || '旋转图片到正确角度'}
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+`;
+}
+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;
diff --git a/tianai-captcha-web-sdk/src/captcha/angle/angle.scss b/tianai-captcha-web-sdk/src/captcha/angle/angle.scss
new file mode 100644
index 0000000..b6d0b5e
--- /dev/null
+++ b/tianai-captcha-web-sdk/src/captcha/angle/angle.scss
@@ -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;
+ }
+ }
+ }
+ }
+}
diff --git a/tianai-captcha-web-sdk/src/captcha/captcha.js b/tianai-captcha-web-sdk/src/captcha/captcha.js
index 4237238..0d03394 100644
--- a/tianai-captcha-web-sdk/src/captcha/captcha.js
+++ b/tianai-captcha-web-sdk/src/captcha/captcha.js
@@ -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 =
-

-
-
+
+

+
+
`;
@@ -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:
diff --git a/tianai-captcha-web-sdk/src/captcha/captcha.scss b/tianai-captcha-web-sdk/src/captcha/captcha.scss
index 6252b93..fa7734a 100644
--- a/tianai-captcha-web-sdk/src/captcha/captcha.scss
+++ b/tianai-captcha-web-sdk/src/captcha/captcha.scss
@@ -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;
}
-
}
-
diff --git a/tianai-captcha-web-sdk/src/captcha/curve_draw/curve_draw.js b/tianai-captcha-web-sdk/src/captcha/curve_draw/curve_draw.js
new file mode 100644
index 0000000..85c9028
--- /dev/null
+++ b/tianai-captcha-web-sdk/src/captcha/curve_draw/curve_draw.js
@@ -0,0 +1,195 @@
+import "./curve_draw.scss"
+import {Dom, CommonCaptcha, destroyEvent} from "../common/common.js"
+
+/**
+ * 曲线绘制验证码 - 用户需要绘制指定曲线
+ */
+
+const TYPE = "CURVE_DRAW"
+function getTemplate(styleConfig) {
+ return `
+
+
+ ${styleConfig.i18n?.curve_draw_title || '请沿虚线绘制曲线'}
+
+
+
+`;
+}
+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;
diff --git a/tianai-captcha-web-sdk/src/captcha/curve_draw/curve_draw.scss b/tianai-captcha-web-sdk/src/captcha/curve_draw/curve_draw.scss
new file mode 100644
index 0000000..1df7cfa
--- /dev/null
+++ b/tianai-captcha-web-sdk/src/captcha/curve_draw/curve_draw.scss
@@ -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;
+ }
+ }
+ }
+ }
+}
diff --git a/tianai-captcha-web-sdk/src/captcha/curve_slider/curve_slider.js b/tianai-captcha-web-sdk/src/captcha/curve_slider/curve_slider.js
new file mode 100644
index 0000000..5259d51
--- /dev/null
+++ b/tianai-captcha-web-sdk/src/captcha/curve_slider/curve_slider.js
@@ -0,0 +1,205 @@
+import "./curve_slider.scss"
+import {Dom, CommonCaptcha, destroyEvent} from "../common/common.js"
+
+/**
+ * 曲线滑块验证码 - 用户需要沿曲线轨迹滑动
+ */
+
+const TYPE = "CURVE_SLIDER"
+function getTemplate(styleConfig) {
+ return `
+
+
+ ${styleConfig.i18n?.curve_slider_title || '沿曲线轨迹滑动到终点'}
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+`;
+}
+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;
diff --git a/tianai-captcha-web-sdk/src/captcha/curve_slider/curve_slider.scss b/tianai-captcha-web-sdk/src/captcha/curve_slider/curve_slider.scss
new file mode 100644
index 0000000..1bd54bd
--- /dev/null
+++ b/tianai-captcha-web-sdk/src/captcha/curve_slider/curve_slider.scss
@@ -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;
+ }
+ }
+ }
+ }
+}
diff --git a/tianai-captcha-web-sdk/src/captcha/icon_click/icon_click.js b/tianai-captcha-web-sdk/src/captcha/icon_click/icon_click.js
new file mode 100644
index 0000000..810962c
--- /dev/null
+++ b/tianai-captcha-web-sdk/src/captcha/icon_click/icon_click.js
@@ -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 `
+
+
+
${styleConfig.i18n?.icon_click_title || '请依次点击图中的图标'}
+
![]()
+
+
+
+
![]()
+
+
+
+
+
+
确定
+
+`;
+}
+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("
" + this.currentCaptchaData.clickCount + "")
+ });
+ 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;
diff --git a/tianai-captcha-web-sdk/src/captcha/icon_click/icon_click.scss b/tianai-captcha-web-sdk/src/captcha/icon_click/icon_click.scss
new file mode 100644
index 0000000..8934b13
--- /dev/null
+++ b/tianai-captcha-web-sdk/src/captcha/icon_click/icon_click.scss
@@ -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;
+ }
+ }
+}
diff --git a/tianai-captcha-web-sdk/src/captcha/jigsaw/jigsaw.js b/tianai-captcha-web-sdk/src/captcha/jigsaw/jigsaw.js
new file mode 100644
index 0000000..5217205
--- /dev/null
+++ b/tianai-captcha-web-sdk/src/captcha/jigsaw/jigsaw.js
@@ -0,0 +1,111 @@
+import "./jigsaw.scss"
+import {Dom, CommonCaptcha, move, initConfig, destroyEvent} from "../common/common.js"
+
+/**
+ * 乱序拼图验证码 - 用户需要拖动拼图块到正确位置
+ */
+
+const TYPE = "JIGSAW"
+function getTemplate(styleConfig) {
+ return `
+
+
+ ${styleConfig.i18n?.jigsaw_title || '拖动拼图块到正确位置'}
+
+
+
+
![]()
+
+
+
+
+
+
+`;
+}
+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;
diff --git a/tianai-captcha-web-sdk/src/captcha/jigsaw/jigsaw.scss b/tianai-captcha-web-sdk/src/captcha/jigsaw/jigsaw.scss
new file mode 100644
index 0000000..f20af30
--- /dev/null
+++ b/tianai-captcha-web-sdk/src/captcha/jigsaw/jigsaw.scss
@@ -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;
+ }
+ }
+ }
+ }
+}
diff --git a/tianai-captcha-web-sdk/src/captcha/proof_of_work/proof_of_work.js b/tianai-captcha-web-sdk/src/captcha/proof_of_work/proof_of_work.js
new file mode 100644
index 0000000..c50daa1
--- /dev/null
+++ b/tianai-captcha-web-sdk/src/captcha/proof_of_work/proof_of_work.js
@@ -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 `
+
+
+ ${styleConfig.i18n?.proof_of_work_title || '请完成工作量证明'}
+
+
+
+
+
+ 挑战:
+ -
+
+
+ 难度:
+ -
+
+
+
+
等待开始...
+
+
+
+
+
+
+
+`;
+}
+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;
diff --git a/tianai-captcha-web-sdk/src/captcha/proof_of_work/proof_of_work.scss b/tianai-captcha-web-sdk/src/captcha/proof_of_work/proof_of_work.scss
new file mode 100644
index 0000000..030c4fe
--- /dev/null
+++ b/tianai-captcha-web-sdk/src/captcha/proof_of_work/proof_of_work.scss
@@ -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;
+ }
+ }
+ }
+ }
+}
diff --git a/tianai-captcha-web-sdk/src/captcha/scratch/scratch.js b/tianai-captcha-web-sdk/src/captcha/scratch/scratch.js
new file mode 100644
index 0000000..e4788bf
--- /dev/null
+++ b/tianai-captcha-web-sdk/src/captcha/scratch/scratch.js
@@ -0,0 +1,181 @@
+import "./scratch.scss"
+import {Dom, CommonCaptcha, destroyEvent} from "../common/common.js"
+
+/**
+ * 刮刮乐验证码 - 用户需要刮开涂层
+ */
+
+const TYPE = "SCRATCH"
+function getTemplate(styleConfig) {
+ return `
+
+
+ ${styleConfig.i18n?.scratch_title || '请刮开涂层完成验证'}
+ 0%
+
+
+
+
![]()
+
+
+
+
+
确定
+
+`;
+}
+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;
diff --git a/tianai-captcha-web-sdk/src/captcha/scratch/scratch.scss b/tianai-captcha-web-sdk/src/captcha/scratch/scratch.scss
new file mode 100644
index 0000000..4189743
--- /dev/null
+++ b/tianai-captcha-web-sdk/src/captcha/scratch/scratch.scss
@@ -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;
+ }
+ }
+}
diff --git a/tianai-captcha-web-sdk/src/captcha/word_order_click/word_order_click.js b/tianai-captcha-web-sdk/src/captcha/word_order_click/word_order_click.js
new file mode 100644
index 0000000..5d35174
--- /dev/null
+++ b/tianai-captcha-web-sdk/src/captcha/word_order_click/word_order_click.js
@@ -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 `
+
+
+
${styleConfig.i18n?.word_order_click_title || '请按顺序点击文字'}
+
+
+
+
+
![]()
+
+
+
+
+
+
确定
+
+`;
+}
+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("
" + this.currentCaptchaData.clickCount + "")
+ 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 += '
' + i + '';
+ } else {
+ html += '
' + i + '';
+ }
+ }
+ 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;
diff --git a/tianai-captcha-web-sdk/src/captcha/word_order_click/word_order_click.scss b/tianai-captcha-web-sdk/src/captcha/word_order_click/word_order_click.scss
new file mode 100644
index 0000000..9f32cc9
--- /dev/null
+++ b/tianai-captcha-web-sdk/src/captcha/word_order_click/word_order_click.scss
@@ -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;
+ }
+ }
+}