/** * tianai-captcha-enhanced SDK * 兼容 TPCaptcha API (TPCaptcha.init / onSuccess / verifyToken) * * Usage: * TACaptcha.init({ * elId: 'captcha-box', * siteKey: 'your-site-key', * type: 'SLIDER', // optional * scene: 'login', // optional * serverUrl: 'https://your-server/api', // optional * onSuccess: (result) => { console.log(result.data.verifyToken); }, * onFail: () => { console.log('failed'); } * }); */ (function (global) { 'use strict'; const INSTANCES = new Map(); class TACaptchaInstance { constructor(options) { this.options = Object.assign({ elId: null, siteKey: '', mode: 'click', type: null, scene: 'default', serverUrl: '/api', onSuccess: null, onFail: null, onOpen: null, onClose: null, onRefresh: null, }, options); this.container = null; this.modal = null; this.captchaData = null; this.verifyToken = null; this.isDragging = false; this.startX = 0; this.currentX = 0; this.trackList = []; this.startTime = 0; this.isDark = this._detectTheme(); this._themeObserver = null; this._init(); } _detectTheme() { if (this.options.theme === 'dark') return true; if (this.options.theme === 'light') return false; if (document.body.classList.contains('dark') || document.documentElement.classList.contains('dark')) return true; if (document.documentElement.getAttribute('data-theme') === 'dark') return true; if (document.documentElement.getAttribute('color-scheme') === 'dark') return true; if (document.documentElement.style.colorScheme === 'dark') return true; return false; } _watchTheme() { if (this._themeObserver) this._themeObserver.disconnect(); const targets = [document.body, document.documentElement]; this._themeObserver = new MutationObserver(() => { const newDark = this._detectTheme(); if (newDark !== this.isDark) { this.isDark = newDark; this._createTriggerButton(); } }); targets.forEach(t => { if (t) this._themeObserver.observe(t, { attributes: true, attributeFilter: ['class', 'data-theme', 'color-scheme'] }); }); if (window.matchMedia) { const mq = window.matchMedia('(prefers-color-scheme: dark)'); this._mqHandler = () => { const newDark = this._detectTheme(); if (newDark !== this.isDark) { this.isDark = newDark; this._createTriggerButton(); } }; mq.addEventListener('change', this._mqHandler); } } _init() { if (this.options.mode === 'event') { this._createModal(); return; } this._createTriggerButton(); this._createModal(); this._watchTheme(); } _createTriggerButton() { const el = document.getElementById(this.options.elId); if (!el) return; this.container = el; const typeLabel = this.options.type || '智能验证'; // 注入呼吸灯 + 盾牌动画样式 if (!document.getElementById('tacaptcha-breathing-style')) { const style = document.createElement('style'); style.id = 'tacaptcha-breathing-style'; style.textContent = '@keyframes tacaptcha-breathe{0%,100%{box-shadow:0 0 0 0 rgba(82,196,26,0.4)}50%{box-shadow:0 0 0 6px rgba(82,196,26,0)}}' + '.tacaptcha-dot{width:8px;height:8px;border-radius:50%;background:#52c41a;animation:tacaptcha-breathe 2s ease-in-out infinite}' + '.tacaptcha-shield .shield-shard{transform-origin:50px 50px;transition:transform 2s cubic-bezier(0.22,1,0.36,1),fill 2s ease,opacity 2s ease}' + '.tacaptcha-shield .ward-group{transition:opacity 2s ease}' + '.tacaptcha-shield .ward-pulse{transform-origin:50px 50px;animation:pulse-ward 2s cubic-bezier(0.1,0.7,0.3,1) infinite}' + '.tacaptcha-shield .ward-pulse.delay{animation-delay:1s}' + '@keyframes pulse-ward{0%{transform:scale(0.6);opacity:1;stroke-width:6px}100%{transform:scale(1.8);opacity:0;stroke-width:1px}}' + '.tacaptcha-shield .sword-slide{transform:translateY(-85px);opacity:0;transition:transform 2s cubic-bezier(0.175,0.885,0.32,1.4),opacity 2s ease}' + '.tacaptcha-shield .sword-anchor-a{transform-origin:50px 50px;transform:rotate(-45deg)}' + '.tacaptcha-shield .sword-anchor-b{transform-origin:50px 50px;transform:rotate(45deg)}' + '.tacaptcha-shield.state-wait .ward-group{opacity:1}' + '.tacaptcha-shield.state-success .ward-group,.tacaptcha-shield.state-fail .ward-group{opacity:0}' + '.tacaptcha-shield.state-success .ward-pulse,.tacaptcha-shield.state-fail .ward-pulse{animation:none}' + '.tacaptcha-shield.state-wait .shield-shard{fill:url(#tacaptcha-mb);transform:translate(0,0) scale(1) rotate(0deg);opacity:1;filter:drop-shadow(0 6px 8px rgba(0,0,0,0.6))}' + '.tacaptcha-shield.state-success .shield-shard{fill:url(#tacaptcha-mg);transform:translate(0,0) scale(1) rotate(0deg);opacity:1;filter:drop-shadow(0 6px 8px rgba(0,0,0,0.6))}' + '.tacaptcha-shield.state-success .sword-slide{transform:translateY(-15px);opacity:1}' + '.tacaptcha-shield.state-fail .shield-shard{fill:url(#tacaptcha-mr);opacity:0.9;filter:none}' + '.tacaptcha-shield.state-fail .shard-tl{transform:translate(-25px,-30px) scale(0.9) rotateX(30deg) rotateY(-20deg) rotateZ(-15deg);opacity:0.7}' + '.tacaptcha-shield.state-fail .shard-tr{transform:translate(25px,-30px) scale(0.9) rotateX(30deg) rotateY(20deg) rotateZ(15deg);opacity:0.7}' + '.tacaptcha-shield.state-fail .shard-b{transform:translate(0,35px) scale(0.8) rotateX(-20deg) rotateZ(5deg);opacity:0.5}'; document.head.appendChild(style); } const bgColor = this.isDark ? '#1a1a2e' : '#fff'; const borderColor = this.isDark ? '#2a2a4a' : '#e8e8e8'; const textColor = this.isDark ? '#e0e0e0' : '#333'; const subColor = this.isDark ? '#888' : '#999'; const btnBg = this.isDark ? '#16213e' : '#fafafa'; const btnBorder = this.isDark ? '#0f3460' : '#d9d9d9'; const btnColor = this.isDark ? '#aaa' : '#666'; const shadow = this.isDark ? '0 2px 8px rgba(0,0,0,0.3)' : '0 1px 4px rgba(0,0,0,0.06)'; const shieldSvg = this._createShieldSvg('state-wait'); const btn = document.createElement('div'); btn.style.cssText = 'display:flex;align-items:center;gap:10px;padding:12px 16px;border:1px solid ' + borderColor + ';border-radius:8px;cursor:pointer;user-select:none;transition:all 0.2s;font-size:14px;color:' + textColor + ';background:' + bgColor + ';box-shadow:' + shadow + ';'; btn.innerHTML = shieldSvg + '点击进行验证' + '' + typeLabel + '' + '' + '点击验证'; btn.addEventListener('mouseenter', () => { btn.style.borderColor = '#1890ff'; }); btn.addEventListener('mouseleave', () => { btn.style.borderColor = borderColor; }); btn.addEventListener('click', () => this._openCaptcha()); el.appendChild(btn); this.triggerBtn = btn; } _createShieldSvg(stateClass) { return '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; } _setShieldState(state) { if (!this.triggerBtn) return; const svg = this.triggerBtn.querySelector('.tacaptcha-shield'); if (svg) { svg.classList.remove('state-wait', 'state-success', 'state-fail'); const pulses = svg.querySelectorAll('.ward-pulse'); pulses.forEach(p => { p.style.animation = 'none'; }); void svg.offsetWidth; svg.classList.add('state-' + state); } } _createModal() { const panelBg = this.isDark ? '#1a1a2e' : '#fff'; const textColor = this.isDark ? '#e0e0e0' : '#333'; const subColor = this.isDark ? '#666' : '#999'; const overlayBg = this.isDark ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.3)'; const shadow = this.isDark ? '0 4px 20px rgba(0,0,0,0.4)' : '0 4px 20px rgba(0,0,0,0.15)'; this.modal = document.createElement('div'); this.modal.style.cssText = 'display:none;position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000;'; const overlay = document.createElement('div'); overlay.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;background:' + overlayBg + ';'; overlay.addEventListener('click', () => this._closeCaptcha()); const panel = document.createElement('div'); panel.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:' + panelBg + ';border-radius:8px;padding:20px;box-shadow:' + shadow + ';min-width:340px;'; this.captchaPanel = panel; const header = document.createElement('div'); header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;'; header.innerHTML = '请完成安全验证'; const refreshBtn = document.createElement('span'); refreshBtn.textContent = '↻'; refreshBtn.style.cssText = 'cursor:pointer;font-size:18px;color:' + subColor + ';'; refreshBtn.addEventListener('click', () => this._loadCaptcha()); header.appendChild(refreshBtn); const closeBtn = document.createElement('span'); closeBtn.textContent = '✕'; closeBtn.style.cssText = 'cursor:pointer;font-size:16px;color:' + subColor + ';margin-left:12px;'; closeBtn.addEventListener('click', () => this._closeCaptcha()); header.appendChild(closeBtn); this.captchaContent = document.createElement('div'); this.captchaContent.style.cssText = 'position:relative;'; this.statusBar = document.createElement('div'); this.statusBar.style.cssText = 'margin-top:12px;text-align:center;font-size:13px;color:' + subColor + ';'; panel.appendChild(header); panel.appendChild(this.captchaContent); panel.appendChild(this.statusBar); this.modal.appendChild(overlay); this.modal.appendChild(panel); document.body.appendChild(this.modal); } async _openCaptcha() { this.modal.style.display = 'block'; if (this.options.onOpen) this.options.onOpen(); await this._loadCaptcha(); } _closeCaptcha() { this.modal.style.display = 'none'; if (this.options.onClose) this.options.onClose(); } async _loadCaptcha() { this.statusBar.textContent = '加载中...'; this.captchaContent.innerHTML = ''; this.verifyToken = null; this._setShieldState('wait'); try { const url = `${this.options.serverUrl}/challenge/generate`; const params = new URLSearchParams(); if (this.options.type) params.set('type', this.options.type); params.set('scene', this.options.scene); const resp = await fetch(url + '?' + params.toString(), { method: 'POST', headers: { 'X-Site-Key': this.options.siteKey } }); const result = await resp.json(); if (result.code !== 200) { this.statusBar.textContent = result.msg || '加载失败'; if (this.options.onFail) this.options.onFail(result); return; } this.captchaData = result.data; console.log('[ROTATE-INIT] degree=' + result.data.degree + ' type=' + result.data.type + ' tplW=' + result.data.templateImageWidth + ' tplH=' + result.data.templateImageHeight); this._renderCaptcha(); this.statusBar.textContent = '请完成验证'; } catch (e) { this.statusBar.textContent = '网络错误'; if (this.options.onFail) this.options.onFail({ code: 10000, msg: e.message }); } } _isClickType(type) { return type === 'WORD_IMAGE_CLICK' || type === 'ICON_CLICK' || type === 'WORD_ORDER_CLICK'; } _renderCaptcha() { this.captchaContent.innerHTML = ''; const data = this.captchaData; if (!data) return; const isRotate = data.type === 'ROTATE'; const isConcat = data.type === 'CONCAT'; const isCurveDraw = data.type === 'CURVE_DRAW'; const isClick = this._isClickType(data.type); if (isClick) { this._renderClickCaptcha(); return; } if (isCurveDraw) { this._renderDrawCaptcha(); return; } const bgImg = document.createElement('img'); bgImg.src = data.backgroundImage; bgImg.style.cssText = 'width:100%;display:block;border-radius:4px;'; const wrapper = document.createElement('div'); wrapper.style.cssText = 'position:relative;border-radius:4px;'; wrapper.appendChild(bgImg); let tplImg = null; let concatTopLayer = null; let concatRandomY = 0; if (isConcat) { concatRandomY = (data.data && data.data.randomY) ? data.data.randomY : Math.round(data.backgroundImageHeight / 2); wrapper.innerHTML = ''; wrapper.style.cssText = 'position:relative;border-radius:4px;overflow:hidden;width:100%;height:180px;'; const bgLayer = document.createElement('div'); bgLayer.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;z-index:1;background-image:url(' + data.backgroundImage + ');background-size:100% 180px;background-position:0 0;background-repeat:no-repeat;'; wrapper.appendChild(bgLayer); const overlayHeight = ((data.backgroundImageHeight - concatRandomY) / data.backgroundImageHeight) * 180; concatTopLayer = document.createElement('div'); concatTopLayer.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:' + overlayHeight + 'px;z-index:2;overflow:hidden;background-image:url(' + data.backgroundImage + ');background-size:100% 180px;background-position:0 0;background-repeat:no-repeat;'; wrapper.appendChild(concatTopLayer); } else if (data.templateImage) { tplImg = document.createElement('img'); tplImg.src = data.templateImage; if (isRotate) { const holeLeft = ((data.backgroundImageWidth - data.templateImageWidth) / 2 / data.backgroundImageWidth * 100); const holeTop = ((data.backgroundImageHeight - data.templateImageHeight) / 2 / data.backgroundImageHeight * 100); tplImg.style.cssText = 'position:absolute;left:' + holeLeft + '%;top:' + holeTop + '%;width:' + (data.templateImageWidth / data.backgroundImageWidth * 100) + '%;pointer-events:none;transform:rotate(0deg);transform-origin:center center;z-index:1;'; } else { tplImg.style.cssText = 'position:absolute;top:0;left:0;height:100%;pointer-events:none;transition:none;'; } wrapper.appendChild(tplImg); } this.captchaContent.appendChild(wrapper); const sliderBar = document.createElement('div'); const barBg = this.isDark ? '#2a2a4a' : '#f0f0f0'; sliderBar.style.cssText = 'position:relative;height:40px;width:100%;background:' + barBg + ';border-radius:4px;margin-top:8px;overflow:hidden;'; const sliderTrack = document.createElement('div'); sliderTrack.style.cssText = 'position:absolute;top:0;left:0;height:100%;width:0;background:linear-gradient(90deg,#52c41a,#73d13d);border-radius:4px;transition:none;'; const sliderBtnBg = this.isDark ? '#1a1a2e' : '#fff'; const sliderBtn = document.createElement('div'); sliderBtn.style.cssText = 'position:absolute;top:2px;left:0;width:36px;height:36px;background:' + sliderBtnBg + ';border-radius:4px;box-shadow:0 1px 4px rgba(0,0,0,0.15);cursor:grab;display:flex;align-items:center;justify-content:center;font-size:16px;color:#999;transition:none;'; sliderBtn.textContent = isRotate ? '↻' : '→'; sliderBar.appendChild(sliderTrack); sliderBar.appendChild(sliderBtn); this.captchaContent.appendChild(sliderBar); this.startX = 0; this.currentX = 0; this.trackList = []; this.startTime = Date.now(); const getBarWidth = () => sliderBar.offsetWidth; const onMove = (e) => { if (!this.isDragging) return; const barWidth = getBarWidth(); const clientX = e.touches ? e.touches[0].clientX : e.clientX; const dx = clientX - this.startX; const maxX = barWidth - 36; this.currentX = Math.max(0, Math.min(dx, maxX)); sliderBtn.style.left = this.currentX + 'px'; sliderTrack.style.width = (this.currentX + 18) + 'px'; const ratio = this.currentX / barWidth; let scaledX; if (isRotate && tplImg) { const bgW = this.captchaData.backgroundImageWidth; const cssDegree = (this.currentX / barWidth) * 360; tplImg.style.transform = 'rotate(' + cssDegree + 'deg)'; scaledX = Math.round(ratio * bgW); } else if (isConcat && concatTopLayer) { const moveX = ratio * wrapper.offsetWidth; concatTopLayer.style.backgroundPositionX = moveX + 'px'; scaledX = Math.round(ratio * this.captchaData.backgroundImageWidth); } else if (tplImg) { tplImg.style.left = (ratio * 100) + '%'; scaledX = Math.round(ratio * this.captchaData.backgroundImageWidth); } else { scaledX = Math.round(ratio * this.captchaData.backgroundImageWidth); } this.trackList.push({ x: scaledX, y: 0, t: Date.now() - this.startTime, type: 'MOVE' }); }; const onUp = async () => { if (!this.isDragging) return; this.isDragging = false; sliderBtn.style.cursor = 'grab'; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.removeEventListener('touchmove', onMove); document.removeEventListener('touchend', onUp); const barWidth = getBarWidth(); const ratio = this.currentX / barWidth; const scaledX = Math.round(ratio * this.captchaData.backgroundImageWidth); this.trackList.push({ x: scaledX, y: 0, t: Date.now() - this.startTime, type: 'UP' }); this.statusBar.textContent = '验证中...'; this.statusBar.style.color = '#999'; await this._verify(); }; sliderBtn.addEventListener('mousedown', (e) => { this.isDragging = true; this.startX = e.clientX - this.currentX; this.trackList = [{ x: 0, y: 0, t: 0, type: 'DOWN' }]; this.startTime = Date.now(); sliderBtn.style.cursor = 'grabbing'; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }); sliderBtn.addEventListener('touchstart', (e) => { this.isDragging = true; this.startX = e.touches[0].clientX - this.currentX; this.trackList = [{ x: 0, y: 0, t: 0, type: 'DOWN' }]; this.startTime = Date.now(); document.addEventListener('touchmove', onMove, { passive: false }); document.addEventListener('touchend', onUp); }); } _renderClickCaptcha() { const data = this.captchaData; const isWordOrder = data.type === 'WORD_ORDER_CLICK'; const tipText = isWordOrder ? '请依次点击' : '请依次点击图中文字'; const tipBar = document.createElement('div'); tipBar.style.cssText = 'display:flex;align-items:center;justify-content:space-between;height:40px;margin-bottom:8px;'; const tipLabel = document.createElement('span'); tipLabel.style.cssText = 'font-size:14px;color:' + (this.isDark ? '#e0e0e0' : '#333') + ';'; tipLabel.textContent = tipText; tipBar.appendChild(tipLabel); if (data.templateImage) { const tipImg = document.createElement('img'); tipImg.src = data.templateImage; tipImg.style.cssText = 'height:35px;max-width:200px;'; tipBar.appendChild(tipImg); } this.captchaContent.appendChild(tipBar); const wrapper = document.createElement('div'); wrapper.style.cssText = 'position:relative;border-radius:4px;overflow:hidden;cursor:crosshair;'; const bgImg = document.createElement('img'); bgImg.src = data.backgroundImage; bgImg.style.cssText = 'width:100%;display:block;border-radius:4px;'; wrapper.appendChild(bgImg); const clickMask = document.createElement('div'); clickMask.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;'; wrapper.appendChild(clickMask); this.captchaContent.appendChild(wrapper); const confirmBtn = document.createElement('div'); confirmBtn.style.cssText = 'width:100%;height:35px;border-radius:4px;background:linear-gradient(173deg,#e8a838 0%,#f0c060 100%);font-size:15px;text-align:center;line-height:35px;color:#fff;margin-top:8px;cursor:pointer;'; confirmBtn.textContent = '确认'; this.captchaContent.appendChild(confirmBtn); this.trackList = []; this.startTime = Date.now(); this.clickCount = 0; this.clickMarkers = []; clickMask.addEventListener('click', (e) => { if (e.target.className === 'tacaptcha-click-marker') return; const rect = bgImg.getBoundingClientRect(); const clickX = Math.round(e.clientX - rect.left); const clickY = Math.round(e.clientY - rect.top); const displayW = rect.width; const displayH = rect.height; const scaledX = Math.round((clickX / displayW) * data.backgroundImageWidth); const scaledY = Math.round((clickY / displayH) * data.backgroundImageHeight); this.clickCount++; if (this.clickCount === 1) { this.startTime = Date.now(); } this.trackList.push({ x: scaledX, y: scaledY, t: Date.now() - this.startTime, type: 'CLICK' }); const marker = document.createElement('span'); marker.className = 'tacaptcha-click-marker'; marker.textContent = this.clickCount; const left = clickX - 11; const top = clickY - 11; marker.style.cssText = 'position:absolute;left:' + left + 'px;top:' + top + 'px;border-radius:50%;background:#409eff;width:22px;height:22px;text-align:center;line-height:22px;color:#fff;border:2px solid #fff;box-sizing:content-box;font-size:12px;pointer-events:none;z-index:2;'; clickMask.appendChild(marker); this.clickMarkers.push(marker); }); confirmBtn.addEventListener('click', async () => { if (this.clickCount === 0) return; this.statusBar.textContent = '验证中...'; this.statusBar.style.color = '#999'; await this._verify(); }); } _renderDrawCaptcha() { const data = this.captchaData; const wrapper = document.createElement('div'); wrapper.style.cssText = 'position:relative;border-radius:4px;overflow:hidden;'; const bgImg = document.createElement('img'); bgImg.src = data.backgroundImage; bgImg.style.cssText = 'width:100%;display:block;border-radius:4px;'; wrapper.appendChild(bgImg); const canvas = document.createElement('canvas'); canvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;touch-action:none;cursor:crosshair;'; wrapper.appendChild(canvas); const tip = document.createElement('div'); tip.textContent = '请在图上绘制曲线'; tip.style.cssText = 'position:absolute;top:8px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.6);color:#fff;padding:4px 12px;border-radius:4px;font-size:13px;pointer-events:none;z-index:2;'; wrapper.appendChild(tip); this.captchaContent.appendChild(wrapper); const confirmBtn = document.createElement('div'); confirmBtn.style.cssText = 'width:100%;height:35px;border-radius:4px;background:linear-gradient(173deg,#e8a838 0%,#f0c060 100%);font-size:15px;text-align:center;line-height:35px;color:#fff;margin-top:8px;cursor:pointer;'; confirmBtn.textContent = '确认'; this.captchaContent.appendChild(confirmBtn); this.trackList = []; this.startTime = Date.now(); this.isDrawing = false; let ctx = null; const getPoint = (e) => { const rect = bgImg.getBoundingClientRect(); const clientX = e.touches ? e.touches[0].clientX : e.clientX; const clientY = e.touches ? e.touches[0].clientY : e.clientY; const x = clientX - rect.left; const y = clientY - rect.top; const scaledX = Math.round((x / rect.width) * data.backgroundImageWidth); const scaledY = Math.round((y / rect.height) * data.backgroundImageHeight); return { x, y, scaledX, scaledY }; }; const startDraw = (e) => { e.preventDefault(); this.isDrawing = true; const p = getPoint(e); this.trackList.push({ x: p.scaledX, y: p.scaledY, t: 0, type: 'DOWN' }); const rect = wrapper.getBoundingClientRect(); canvas.width = rect.width; canvas.height = rect.height; ctx = canvas.getContext('2d'); ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineWidth = 3; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.strokeStyle = 'rgba(64, 158, 255, 0.9)'; tip.style.display = 'none'; }; const moveDraw = (e) => { if (!this.isDrawing) return; e.preventDefault(); const p = getPoint(e); this.trackList.push({ x: p.scaledX, y: p.scaledY, t: Date.now() - this.startTime, type: 'MOVE' }); if (ctx) { ctx.lineTo(p.x, p.y); ctx.stroke(); } }; const endDraw = (e) => { if (!this.isDrawing) return; this.isDrawing = false; let clientX, clientY; if (e.changedTouches && e.changedTouches.length > 0) { clientX = e.changedTouches[0].clientX; clientY = e.changedTouches[0].clientY; } else { clientX = e.clientX; clientY = e.clientY; } const rect = bgImg.getBoundingClientRect(); const x = clientX - rect.left; const y = clientY - rect.top; const scaledX = Math.round((x / rect.width) * data.backgroundImageWidth); const scaledY = Math.round((y / rect.height) * data.backgroundImageHeight); this.trackList.push({ x: scaledX, y: scaledY, t: Date.now() - this.startTime, type: 'UP' }); }; canvas.addEventListener('mousedown', startDraw); canvas.addEventListener('mousemove', moveDraw); canvas.addEventListener('mouseup', endDraw); canvas.addEventListener('touchstart', startDraw, { passive: false }); canvas.addEventListener('touchmove', moveDraw, { passive: false }); canvas.addEventListener('touchend', endDraw); confirmBtn.addEventListener('click', async () => { if (this.trackList.length === 0) return; this.statusBar.textContent = '验证中...'; this.statusBar.style.color = '#999'; await this._verify(); }); } async _verify() { if (!this.captchaData) return; this.statusBar.textContent = '验证中...'; try { const trackData = { bgImageWidth: this.captchaData.backgroundImageWidth, bgImageHeight: this.captchaData.backgroundImageHeight, templateImageWidth: this.captchaData.templateImageWidth, templateImageHeight: this.captchaData.templateImageHeight, startTime: this.startTime, stopTime: Date.now(), trackList: this.trackList }; const resp = await fetch(`${this.options.serverUrl}/challenge/verify`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Site-Key': this.options.siteKey }, body: JSON.stringify({ id: this.captchaData.id, data: trackData }) }); const result = await resp.json(); if (result.code === 200 && result.data && result.data.verifyToken) { this.verifyToken = result.data.verifyToken; this.statusBar.textContent = '验证成功'; this.statusBar.style.color = '#52c41a'; if (this.options.onSuccess) { this.options.onSuccess({ code: 200, msg: '操作成功', data: { verifyToken: this.verifyToken } }); } setTimeout(() => { this._closeCaptcha(); this._setShieldState('success'); }, 400); } else { this._setShieldState('fail'); this.statusBar.textContent = '验证失败,请重试'; this.statusBar.style.color = '#ff4d4f'; if (this.options.onFail) this.options.onFail(result); setTimeout(() => this._loadCaptcha(), 1500); } } catch (e) { this.statusBar.textContent = '网络错误'; if (this.options.onFail) this.options.onFail({ code: 10000, msg: e.message }); } } async show() { await this._openCaptcha(); } destroy() { if (this.modal && this.modal.parentNode) { this.modal.parentNode.removeChild(this.modal); } if (this.options.elId) { INSTANCES.delete(this.options.elId); } } } const TACaptcha = { async init(options) { const elId = options.elId || ('_tac_' + Date.now()); if (INSTANCES.has(elId)) { INSTANCES.get(elId).destroy(); } const instance = new TACaptchaInstance(options); INSTANCES.set(elId, instance); return instance; }, refresh(elId) { const inst = INSTANCES.get(elId); if (inst) inst._loadCaptcha(); }, show(elId) { const inst = INSTANCES.get(elId); if (inst) return inst.show(); return Promise.reject(new Error('Instance not found')); } }; if (typeof module !== 'undefined' && module.exports) { module.exports = TACaptcha; } else { global.TACaptcha = TACaptcha; } })(typeof window !== 'undefined' ? window : this);