class SliderCaptcha { constructor(container, options = {}) { this.container = typeof container === 'string' ? document.querySelector(container) : container; if (!this.container) { throw new Error('SliderCaptcha: Container element not found'); } this.options = { apiBaseUrl: '/user', onSuccess: null, onFail: null, onRefresh: null, width: 300, autoLoad: true, ...options }; this.captchaKey = null; this.bgImage = null; this.sliderImage = null; this.yPosition = 0; this.isDragging = false; this.startX = 0; this.currentX = 0; this.trajectory = []; this.maxX = 0; this.verified = false; this._init(); } _init() { this._createDOM(); this._bindEvents(); if (this.options.autoLoad) { this.loadCaptcha(); } } _createDOM() { const captchaId = 'slider-captcha-' + Date.now(); this.container.innerHTML = `
`; this.element = this.container.querySelector('.slider-captcha'); this.canvas = this.container.querySelector('.slider-captcha-canvas'); this.ctx = this.canvas.getContext('2d'); this.sliderPiece = this.container.querySelector('.slider-captcha-slider-piece'); this.loading = this.container.querySelector('.slider-captcha-loading'); this.track = this.container.querySelector('.slider-captcha-track'); this.trackFill = this.container.querySelector('.slider-captcha-track-fill'); this.trackText = this.container.querySelector('.slider-captcha-track-text'); this.sliderBtn = this.container.querySelector('.slider-captcha-slider-btn'); this.refreshBtn = this.container.querySelector('.slider-captcha-refresh'); this.status = this.container.querySelector('.slider-captcha-status'); this.maxX = this.track.offsetWidth - this.sliderBtn.offsetWidth; } _bindEvents() { this.refreshBtn.addEventListener('click', () => this.loadCaptcha()); this.sliderBtn.addEventListener('mousedown', (e) => this._onDragStart(e)); this.sliderBtn.addEventListener('touchstart', (e) => this._onDragStart(e), { passive: false }); document.addEventListener('mousemove', (e) => this._onDragMove(e)); document.addEventListener('touchmove', (e) => this._onDragMove(e), { passive: false }); document.addEventListener('mouseup', (e) => this._onDragEnd(e)); document.addEventListener('touchend', (e) => this._onDragEnd(e)); } async loadCaptcha() { this._showLoading(true); this._resetState(); try { const response = await fetch(`${this.options.apiBaseUrl}/slider-captcha/generate/`); const data = await response.json(); if (data.code === 0 || data.code === 20000) { const captchaData = data.data; this.captchaKey = captchaData.captcha_key; this.yPosition = captchaData.y_position; await this._loadImages(captchaData.bg_image, captchaData.slider_image); this._drawCaptcha(); this._showLoading(false); } else { throw new Error(data.message || '加载验证码失败'); } } catch (error) { console.error('Failed to load captcha:', error); this._showLoading(false); this._showStatus('加载失败,点击刷新重试', 'fail'); } } _loadImages(bgSrc, sliderSrc) { return new Promise((resolve, reject) => { let loaded = 0; const onLoad = () => { loaded++; if (loaded === 2) resolve(); }; this.bgImage = new Image(); this.bgImage.onload = onLoad; this.bgImage.onerror = reject; this.bgImage.src = bgSrc; this.sliderImage = new Image(); this.sliderImage.onload = onLoad; this.sliderImage.onerror = reject; this.sliderImage.src = sliderSrc; }); } _drawCaptcha() { if (!this.bgImage || !this.ctx) return; this.canvas.width = this.options.width; this.canvas.height = 150; this.ctx.drawImage(this.bgImage, 0, 0, this.canvas.width, this.canvas.height); if (this.sliderImage) { this.sliderPiece.style.backgroundImage = `url(${this.sliderImage.src})`; this.sliderPiece.style.top = `${this.yPosition}px`; this.sliderPiece.style.left = '0px'; this.sliderPiece.style.width = '60px'; this.sliderPiece.style.height = '60px'; } } _onDragStart(e) { if (this.verified) return; e.preventDefault(); this.isDragging = true; this.trajectory = []; const clientX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX; this.startX = clientX; this.currentX = 0; this.sliderBtn.classList.add('dragging'); this.element.classList.add('dragging'); this._recordTrajectory(0); } _onDragMove(e) { if (!this.isDragging) return; e.preventDefault(); const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX; let moveX = clientX - this.startX; moveX = Math.max(0, Math.min(moveX, this.maxX)); this.currentX = moveX; this._updateSliderPosition(moveX); this._recordTrajectory(moveX); } _onDragEnd(e) { if (!this.isDragging) return; this.isDragging = false; this.sliderBtn.classList.remove('dragging'); this.element.classList.remove('dragging'); if (this.currentX > 10) { this._verify(); } else { this._resetSlider(); } } _updateSliderPosition(x) { const percent = (x / this.maxX) * 100; this.sliderBtn.style.left = `${x}px`; this.sliderBtn.setAttribute('aria-valuenow', Math.round(percent)); this.trackFill.style.width = `${x + this.sliderBtn.offsetWidth / 2}px`; this.sliderPiece.style.left = `${x}px`; } _recordTrajectory(x) { this.trajectory.push({ x: Math.round(x), t: Date.now() }); } async _verify() { const xPosition = Math.round(this.currentX); try { const response = await fetch(`${this.options.apiBaseUrl}/slider-captcha/verify/`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ captcha_key: this.captchaKey, x_position: xPosition, trajectory: this.trajectory }) }); const data = await response.json(); if (data.code === 0 || data.code === 20000) { if (data.data && data.data.verified) { this._onSuccess(); } else { this._onFail(); } } else { this._onFail(data.message); } } catch (error) { console.error('Verification failed:', error); this._onFail('验证失败,请重试'); } } _onSuccess() { this.verified = true; this.sliderBtn.classList.add('success'); this.element.classList.add('verified'); this.trackFill.style.background = 'linear-gradient(90deg, #52c41a, #73d13d)'; this._showStatus('验证成功', 'success'); if (typeof this.options.onSuccess === 'function') { this.options.onSuccess({ captchaKey: this.captchaKey, xPosition: Math.round(this.currentX) }); } } _onFail(message = '验证失败,请重试') { this.sliderBtn.classList.add('fail'); this._showStatus(message, 'fail'); setTimeout(() => { this._resetSlider(); this.sliderBtn.classList.remove('fail'); this.loadCaptcha(); }, 1500); if (typeof this.options.onFail === 'function') { this.options.onFail(message); } } _resetState() { this.verified = false; this.isDragging = false; this.currentX = 0; this.trajectory = []; this.element.classList.remove('verified', 'dragging'); this.sliderBtn.classList.remove('success', 'fail', 'dragging'); this.sliderBtn.style.left = '0px'; this.sliderBtn.setAttribute('aria-valuenow', 0); this.trackFill.style.width = '0px'; this.trackFill.style.background = 'linear-gradient(90deg, #52c41a, #73d13d)'; this.trackText.style.display = 'block'; this._showStatus('', ''); } _resetSlider() { this.currentX = 0; this.trajectory = []; this.sliderBtn.style.left = '0px'; this.sliderBtn.setAttribute('aria-valuenow', 0); this.trackFill.style.width = '0px'; this.sliderPiece.style.left = '0px'; this._showStatus('', ''); } _showLoading(show) { if (this.loading) { this.loading.classList.toggle('hidden', !show); } } _showStatus(message, type) { if (this.status) { this.status.textContent = message; this.status.className = 'slider-captcha-status'; if (type) { this.status.classList.add(type); } if (!message) { this.status.classList.add('hidden'); } } } refresh() { this.loadCaptcha(); } destroy() { document.removeEventListener('mousemove', this._onDragMove); document.removeEventListener('touchmove', this._onDragMove); document.removeEventListener('mouseup', this._onDragEnd); document.removeEventListener('touchend', this._onDragEnd); this.container.innerHTML = ''; } isVerified() { return this.verified; } } if (typeof module !== 'undefined' && module.exports) { module.exports = SliderCaptcha; }