slider-captcha-sdk 1.0.24 → 1.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -1 +1,1417 @@
1
- function t(t){return new PasswordValidator(t)}function i(t,i,e={},s={}){return new PasswordValidator(e).validatePassword(t,i,s)}var e,s,r;import JSEncrypt from"jsencrypt";class PopupSliderCaptcha{static DEFAULTS={width:320,height:155,sliderSize:38,maxRetries:3,timeout:3e4,apiUrl:"/externalapi/commonservice/captcha/get",verifyUrl:"/externalapi/commonservice/captcha/check",baseUrl:"",throttleDelay:16,clickMaskClose:!1};static CSS_CLASSES={overlay:"slider-captcha-overlay",modal:"slider-captcha-modal",header:"slider-captcha-header",container:"slider-captcha-container",track:"slider-captcha-track",btn:"slider-captcha-btn",hint:"slider-captcha-hint",loading:"slider-captcha-loading",error:"slider-captcha-error"};static getStyles(){return":root{--sc-primary:#409eff;--sc-success:#67c23a;--sc-danger:#f56c6c;--sc-border:#e4e7eb;--sc-bg:linear-gradient(90deg, #f7f9fa 0%, #e8f4fd 100%);--sc-text:#333;--sc-text-light:#999;--sc-shadow:0 4px 20px rgba(0,0,0,.3);--sc-radius:8px;--sc-transition:.3s ease}.slider-captcha-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);z-index:9999;display:none;justify-content:center;align-items:center;opacity:0;transition:opacity var(--sc-transition)}.slider-captcha-overlay.show{opacity:1}.slider-captcha-modal{background:#fff;border-radius:var(--sc-radius);padding:20px;box-shadow:var(--sc-shadow);position:relative;max-width:90vw;max-height:90vh;transform:scale(.8) translateY(-20px);opacity:0;transition:all var(--sc-transition)}.slider-captcha-modal.show{transform:scale(1) translateY(0);opacity:1}.slider-captcha-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;padding-bottom:10px;border-bottom:1px solid var(--sc-border)}.slider-captcha-container{display:flex;align-items:center;position:relative;border-radius:4px;overflow:hidden;margin-bottom:15px;background:#837a7a;justify-content:center}.slider-captcha-track{width:100%;height:40px;line-height:40px;background:var(--sc-bg);border:1px solid var(--sc-border);border-radius:20px;position:relative;margin-bottom:15px;overflow:hidden}.slider-captcha-btn{width:38px;height:38px;background:#fff;border:1px solid #ccc;border-radius:50%;position:absolute;top:0;left:0;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 4px rgba(0,0,0,.1);transition:all var(--sc-transition);user-select:none;z-index:1}.slider-captcha-loading{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(255,255,255,.6);display:flex;align-items:center;justify-content:center;flex-direction:column;color:#666;font-size:14px;z-index:10;border-radius:4px}.slider-captcha-error{color:var(--sc-danger);font-size:12px;text-align:center;margin-top:10px;display:none}.slider-captcha-title{margin:0;font-size:16px;color:var(--sc-text)}.slider-captcha-close,.slider-captcha-refresh{background:none;border:none;cursor:pointer;color:var(--sc-text-light);padding:0;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:50%;transition:all var(--sc-transition);position:relative;font-size:0}.slider-captcha-close::before,.slider-captcha-close::after{content:'';position:absolute;width:16px;height:2px;background-color:var(--sc-text-light);border-radius:1px;transition:all var(--sc-transition)}.slider-captcha-close::before{transform:rotate(45deg)}.slider-captcha-close::after{transform:rotate(-45deg)}.slider-captcha-close:hover{background:#f5f5f5;transform:scale(1.1)}.slider-captcha-close:hover::before,.slider-captcha-close:hover::after{background-color:var(--sc-danger)}.slider-captcha-refresh{margin-left:10px}.slider-captcha-refresh svg{width:20px;height:20px;fill:var(--sc-text-light);transition:all var(--sc-transition)}.slider-captcha-refresh:hover{background:#f5f5f5;transform:scale(1.1)}.slider-captcha-refresh:hover svg{fill:var(--sc-primary);transform:rotate(180deg)}.slider-captcha-floating-time{position:absolute;bottom:-40px;left:50%;transform:translateX(-50%);color:#fff;font-size:12px;line-height:12px;white-space:nowrap;opacity:0;pointer-events:none;z-index:10;transition:all var(--sc-transition);background:#fff;padding:4px 15px;border-radius:10px}.slider-captcha-floating-time.show{opacity:1;transform:translateX(-50%) translateY(-45px)}.slider-captcha-floating-time.success{color:var(--sc-success)}.slider-captcha-floating-time.fail{color:var(--sc-danger)}.slider-captcha-bg{width:100%;height:100%;object-fit:cover;display:block}.slider-captcha-piece{position:absolute;top:0;left:0;cursor:pointer;transition:none;z-index:2}.slider-captcha-finger{position:absolute;top:50%;left:10px;transform:translateY(-50%);font-size:20px;animation:fingerSlide 2s ease-in-out infinite;pointer-events:none;z-index:1;opacity:.6}.slider-captcha-hint{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:var(--sc-text-light);font-size:14px;pointer-events:none;z-index:1;transition:all var(--sc-transition)}.slider-captcha-header-buttons{display:flex;align-items:center}@keyframes fingerSlide{0%{left:10px;opacity:.6}50%{opacity:1}100%{left:calc(50% - 10px);opacity:.6}}"}static ERROR_TYPES={NETWORK_ERROR:"NETWORK_ERROR",TIMEOUT_ERROR:"TIMEOUT_ERROR",VALIDATION_ERROR:"VALIDATION_ERROR",IMAGE_LOAD_ERROR:"IMAGE_LOAD_ERROR",CAPTCHA_DATA_ERROR:"CAPTCHA_DATA_ERROR"};constructor(t={}){this.options={...PopupSliderCaptcha.DEFAULTS,...t},this.options.apiUrl=this.normalizeUrl(this.options.apiUrl,this.options.baseUrl),this.options.verifyUrl=this.normalizeUrl(this.options.verifyUrl,this.options.baseUrl);const{elements:i={},state:e=this.createInitialState(),captchaData:s=null,times:r=[],startTime:a=null,eventListeners:o=[],timers:n=new Set,rafId:h=null,cachedDimensions:c=null,imageCache:l=new Map,abortController:d=null}=this;Object.assign(this,{elements:i,state:e,captchaData:s,times:r,startTime:a,eventListeners:o,timers:n,rafId:h,cachedDimensions:c,imageCache:l,abortController:d}),this.throttledHandleMove=this.throttle(t=>this.handleMove(t),this.options.throttleDelay);try{this.init()}catch(t){this.handleError(PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR,t.message)}}createInitialState(){return{isVisible:!1,isDragging:!1,currentX:0,startX:0,retryCount:0,isLoading:!1}}normalizeUrl(t,i){return t?/^https?:\/\//.test(t)||!i?t:(i.endsWith("/")?i:i+"/")+(t.startsWith("/")?t.substring(1):t):t}init(){this.injectStyles(),this.createElements(),this.bindEvents()}injectStyles(){if(document.querySelector("#slider-captcha-styles"))return;const t=Object.assign(document.createElement("style"),{id:"slider-captcha-styles",textContent:PopupSliderCaptcha.getStyles()});document.head.appendChild(t)}createElements(){const{elements:t,options:i}=this,e={overlay:["div",PopupSliderCaptcha.CSS_CLASSES.overlay],modal:["div",PopupSliderCaptcha.CSS_CLASSES.modal],header:["div",PopupSliderCaptcha.CSS_CLASSES.header],title:["h3","slider-captcha-title","安全验证"],closeBtn:["button","slider-captcha-close"],refreshBtn:["button","slider-captcha-refresh"],container:["div",PopupSliderCaptcha.CSS_CLASSES.container],backgroundImg:["img","slider-captcha-bg"],sliderImg:["img","slider-captcha-piece"],loadingText:["div",PopupSliderCaptcha.CSS_CLASSES.loading,"加载中..."],floatingTime:["div","slider-captcha-floating-time"],track:["div",PopupSliderCaptcha.CSS_CLASSES.track],fingerAnimation:["div","slider-captcha-finger","👉"],btn:["div",PopupSliderCaptcha.CSS_CLASSES.btn],icon:["div","","→"],hint:["div",PopupSliderCaptcha.CSS_CLASSES.hint,"向右滑动完成验证"],error:["div",PopupSliderCaptcha.CSS_CLASSES.error]};Object.entries(e).forEach(([i,[e,s,r]])=>{t[i]=this.createElement(e,s,r)}),t.container.style.cssText=`width:${i.width}px;height:${i.height}px`,t.refreshBtn.innerHTML='\n <svg viewBox="0 0 24 24">\n <path d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z"/>\n </svg>\n ',this.assembleDOM(),this.setInitialState()}createElement(t,i="",e=""){return Object.assign(document.createElement(t),{className:i,textContent:e})}assembleDOM(){const{elements:t}=this,i=this.createElement("div","slider-captcha-header-buttons");i.append(t.refreshBtn,t.closeBtn),t.header.append(t.title,i),t.container.append(t.backgroundImg,t.sliderImg,t.loadingText,t.floatingTime),t.btn.appendChild(t.icon),t.track.append(t.fingerAnimation,t.btn,t.hint),t.modal.append(t.header,t.container,t.track,t.error),t.overlay.appendChild(t.modal),document.body.appendChild(t.overlay)}setInitialState(){Object.assign(this.elements.container.style,{display:"none"}),Object.assign(this.elements.track.style,{display:"none"})}bindEvents(){const{elements:t}=this;[[t.closeBtn,"click",()=>this.hide()],[t.refreshBtn,"click",()=>this.refresh()],[t.overlay,"click",i=>{i.target===t.overlay&&this.options.clickMaskClose&&this.hide()}],[document,"keydown",t=>{"Escape"===t.key&&this.state.isVisible&&this.hide()}],[document,"visibilitychange",()=>this.handleVisibilityChange()]].forEach(([t,i,e])=>{this.addEventListener(t,i,e)}),this.bindSliderEvents()}bindSliderEvents(){const{elements:t}=this,i={start:t=>this.handleStart(t),move:this.throttledHandleMove,end:()=>this.handleEnd()};[[t.btn,"mousedown",i.start],[t.btn,"touchstart",i.start],[t.sliderImg,"mousedown",i.start],[t.sliderImg,"touchstart",i.start],[document,"mousemove",i.move,{passive:!1}],[document,"touchmove",i.move,{passive:!1}],[document,"mouseup",i.end],[document,"touchend",i.end]].forEach(([t,i,e,s])=>{this.addEventListener(t,i,e,s)})}addEventListener(t,i,e,s={}){t&&"function"==typeof e&&(t.addEventListener(i,e,s),this.eventListeners.push({element:t,event:i,handler:e,options:s}))}removeAllEventListeners(){this.eventListeners.forEach(({element:t,event:i,handler:e,options:s})=>{try{t?.removeEventListener?.(i,e,s)}catch(t){}}),this.eventListeners.length=0}getDimensions(){if(!this.cachedDimensions){const{track:t,btn:i}=this.elements;this.cachedDimensions={trackWidth:t.offsetWidth,btnWidth:i.offsetWidth,get maxX(){return this.trackWidth-this.btnWidth}}}return this.cachedDimensions}getPosition(){const{maxX:t}=this.getDimensions(),i=this.state.currentX/t;return Math.round(i*(this.options.width-this.options.sliderSize))}handleStart(t){!this.captchaData||this.state.isDragging||this.state.isLoading||(t.preventDefault(),Object.assign(this.state,{isDragging:!0,startX:this.getClientX(t)-this.state.currentX}),this.dragStartTime=Date.now(),this.times=[{time:Date.now(),position:this.getPosition()}],this.setTransition(!1),this.updateUIState("dragging"),this.cachedDimensions=null)}handleMove(t){if(!this.state.isDragging)return;t.preventDefault();const i=this.getClientX(t)-this.state.startX,{maxX:e}=this.getDimensions();this.state.currentX=Math.max(0,Math.min(i,e)),this.times.push({time:Date.now(),position:this.getPosition()}),this.rafId&&cancelAnimationFrame(this.rafId),this.rafId=requestAnimationFrame(()=>this.updateSliderPosition())}handleEnd(){this.state.isDragging&&(this.times.push({time:Date.now(),position:this.getPosition()}),this.state.isDragging=!1,this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.verify())}handleVisibilityChange(){const t=document.hidden?"paused":"running";this.elements.fingerAnimation?.style&&(this.elements.fingerAnimation.style.animationPlayState=t)}getClientX=t=>t.type.includes("touch")?t.touches[0].clientX:t.clientX;setTransition(t){const i=t?"all 0.3s ease":"none";requestAnimationFrame(()=>{const{btn:t,sliderImg:e}=this.elements;t.style.transition=e.style.transition=i})}updateUIState(t){const{elements:i}=this,e={dragging:()=>{i.hint.style.opacity="0",i.fingerAnimation.style.display="none"},success:()=>{Object.assign(i.btn.style,{background:"var(--sc-success)"}),Object.assign(i.icon.style,{color:"white"}),i.icon.textContent="✓"},fail:()=>{Object.assign(i.btn.style,{background:"var(--sc-danger)"}),Object.assign(i.icon.style,{color:"white"}),i.icon.textContent="✗"},reset:()=>{Object.assign(i.btn.style,{background:"white"}),Object.assign(i.icon.style,{color:"#666"}),i.icon.textContent="→",i.fingerAnimation.style.display="block",this.updateHintText("向右滑动完成验证","var(--sc-text-light)")},loading:()=>{i.hint.style.opacity="0",i.fingerAnimation.style.display="none",Object.assign(i.track.style,{pointerEvents:"none",opacity:"0.6"})}};e[t]&&requestAnimationFrame(()=>{e[t](),"loading"!==t&&Object.assign(i.track.style,{pointerEvents:"auto",opacity:"1"})})}updateHintText(t,i){requestAnimationFrame(()=>{Object.assign(this.elements.hint,{textContent:t}),Object.assign(this.elements.hint.style,{color:i,opacity:"1"})})}updateSliderPosition(){const{elements:t,options:i,state:e}=this,{maxX:s}=this.getDimensions(),r=e.currentX/s*(i.width-i.sliderSize),a=e.currentX/s;requestAnimationFrame(()=>{t.btn.style.transform=`translateX(${e.currentX}px)`,t.sliderImg.style.transform=`translateX(${r}px)`,t.fingerAnimation.style.opacity=.8>a?"0.6":"0"})}show(){this.state.isVisible=!0,this.elements.overlay.style.display="flex",requestAnimationFrame(()=>{this.elements.overlay.classList.add("show"),this.elements.modal.classList.add("show")}),this.loadCaptcha()}hide(){this.state.isVisible=!1,this.elements.overlay.classList.remove("show"),this.elements.modal.classList.remove("show"),this.safeSetTimeout(()=>{this.elements.overlay.style.display="none",document.body.removeChild(this.elements.overlay),this.reset(),this.options.onClose?.(),this.elements.overlay=null},300)}handleError(t,i,e=null){const s={[PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR]:"网络连接失败,请检查网络设置",[PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR]:"请求超时,请重试",[PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR]:"验证失败,请重试",[PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR]:"图片加载失败,请刷新重试",[PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR]:"验证码数据错误,请刷新重试"}[t]||i||"未知错误";this.options.onError&&this.options.onError({type:t,message:s,originalError:e}),this.showError(s)}async loadCaptcha(){try{this.showLoading(),this.startTime=Date.now(),this.abortController&&this.abortController.abort(),this.abortController=new AbortController;const t=await fetch(this.options.apiUrl,{method:"POST",headers:{"Content-Type":"application/json",...this.options.headers},body:JSON.stringify({timestamp:Date.now(),...this.options.requestData}),signal:this.abortController.signal});if(!t.ok)throw Error(`HTTP ${t.status}: ${t.statusText}`);const i=await t.json();if(!this.validateCaptchaData(i))throw Error("验证码数据格式错误");this.captchaData=i.data,this.showCaptcha(),await this.renderCaptcha()}catch(t){"AbortError"===t.name?this.handleError(PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR,"请求被取消"):t.message.includes("Failed to fetch")||t.message.includes("NetworkError")?this.handleError(PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR,"网络连接失败"):this.handleError(PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR,t.message,t)}}validateCaptchaData(t){if(!t||"object"!=typeof t)return!1;const i=t.data||t;return["canvasSrc","blockSrc","canvasWidth","canvasHeight","blockWidth","blockHeight","blockY","nonceStr"].every(t=>{const e=i[t];return null!=e&&""!==e})}renderCaptcha(){return new Promise((t,i)=>{let e=!1;const s=[this.loadImageAsync(this.elements.backgroundImg,this.captchaData.canvasSrc,{width:this.captchaData.canvasWidth,height:this.captchaData.canvasHeight}),this.loadImageAsync(this.elements.sliderImg,this.captchaData.blockSrc,{width:this.captchaData.blockWidth,height:this.captchaData.blockHeight,top:this.captchaData.blockY})];Promise.all(s).then(()=>{e||(this.hideLoading(),t())}).catch(t=>{e||(e=!0,this.handleError(PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR,"图片加载失败",t),i(t))})})}loadImageAsync(t,i,e){return new Promise((s,r)=>{if(this.imageCache.has(i)){const r=this.imageCache.get(i);return t.src=r.src,this.applyStyles(t,e),void s()}const a=this.safeSetTimeout(()=>{r(Error("图片加载超时"))},1e4);t.onload=function(){this.safeClearTimeout(a),this.imageCache.set(i,t.cloneNode()),s()}.bind(this),t.onerror=function(t){this.safeClearTimeout(a),r(t)}.bind(this),t.src=i,this.applyStyles(t,e)})}applyStyles(t,i){Object.entries(i).forEach(([i,e])=>{t.style[i]="number"==typeof e?e+"px":e})}showLoading(){this.state.isLoading=!0,this.batchUpdateStyles({container:{display:"block"},loadingText:{display:"flex"},error:{display:"none"}}),this.updateUIState("loading")}hideLoading(){this.state.isLoading=!1,this.batchUpdateStyles({loadingText:{display:"none"}}),this.updateUIState("reset")}showCaptcha(){this.batchUpdateStyles({container:{display:"block"},track:{display:"block"},error:{display:"none"}})}showError(t){this.hideLoading(),this.batchUpdateStyles({error:{display:"block",textContent:t}})}batchUpdateStyles(t){requestAnimationFrame(()=>{Object.entries(t).forEach(([t,i])=>{const e=this.elements[t];e&&Object.entries(i).forEach(([t,i])=>{"textContent"===t?e.textContent=i:e.style[t]=i})})})}async verify(){if(this.captchaData)try{this.abortController&&this.abortController.abort(),this.abortController=new AbortController;const t=await fetch(this.options.verifyUrl,{method:"POST",headers:{"Content-Type":"application/json",...this.options.headers},body:JSON.stringify({loginVo:{nonceStr:this.captchaData.nonceStr,value:this.getPosition()},dragEventList:[...this.times],...this.options.verifyData}),signal:this.abortController.signal});if(!t.ok)throw Error(`HTTP ${t.status}: ${t.statusText}`);const i=await t.json();this.isVerifySuccess(i)?this.onVerifySuccess(i.data||i.result):this.onVerifyFail(i.message||i.msg||"验证失败,请重试!")}catch(t){"AbortError"===t.name?this.handleError(PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR,"验证请求被取消"):t.message.includes("Failed to fetch")||t.message.includes("NetworkError")?this.handleError(PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR,"网络连接失败"):this.handleError(PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR,t.message,t)}else this.onVerifyFail("验证码数据丢失")}isVerifySuccess(t){return!(!t||"object"!=typeof t)&&["0"===t.code,0===t.code,!0===t.success,"success"===t.status,!0===t.result].some(t=>!0===t)}onVerifySuccess(t){const i=this.dragStartTime?Date.now()-this.dragStartTime:Date.now()-this.startTime,e=`验证成功!耗时:${(i/1e3).toFixed(2)}s`;this.updateUIState("success"),this.showFloatingTime(e,"success"),this.safeSetTimeout(()=>{this.options.onSuccess?.({ticket:t,timestamp:Date.now(),duration:i}),this.hide()},2e3)}showFloatingTime(t,i="success"){const{elements:e}=this;e.floatingTime.textContent=t,e.floatingTime.className="slider-captcha-floating-time "+i,this.safeSetTimeout(()=>e.floatingTime.classList.add("show"),100),this.safeSetTimeout(()=>{e.floatingTime.className="slider-captcha-floating-time"},2500)}onVerifyFail(t){this.state.retryCount++,this.updateUIState("fail"),this.showFloatingTime(t,"fail"),this.safeSetTimeout(()=>{this.state.retryCount<this.options.maxRetries?this.reset():this.refresh()},2500)}reset(){this.clearAllTimers(),Object.assign(this.state,{isDragging:!1,currentX:0,startX:0,isLoading:!1}),this.times=[],this.startTime=null,this.dragStartTime=null,this.cachedDimensions=null,requestAnimationFrame(()=>{this.setTransition(!0),this.elements.btn.style.transform="translateX(0px)",this.elements.sliderImg.style.transform="translateX(0px)",this.updateUIState("reset"),this.elements.error.style.display="none"})}refresh(){try{this.reset(),this.state.retryCount=0,this.loadCaptcha()}catch(t){}}safeSetTimeout(t,i){const e=setTimeout(()=>{this.timers.delete(e),t()},i);return this.timers.add(e),e}safeClearTimeout(t){t&&(clearTimeout(t),this.timers.delete(t))}clearAllTimers(){this.timers.forEach(t=>{clearTimeout(t),clearInterval(t)}),this.timers.clear(),this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=null)}cleanupImages(){this.elements.backgroundImg&&(this.elements.backgroundImg.src="",this.elements.backgroundImg.onload=null,this.elements.backgroundImg.onerror=null),this.elements.sliderImg&&(this.elements.sliderImg.src="",this.elements.sliderImg.onload=null,this.elements.sliderImg.onerror=null),this.imageCache.clear()}throttle(t,i){let e=0;return(...s)=>{const r=Date.now();if(r-e>=i)return e=r,t.apply(this,s)}}destroy(){try{this.abortController&&(this.abortController.abort(),this.abortController=null),document.body&&(document.body.style.userSelect="",document.body.style.cursor=""),this.clearAllTimers(),this.removeAllEventListeners(),this.cleanupImages(),this.elements?.overlay?.parentNode&&this.elements.overlay.parentNode.removeChild(this.elements.overlay);const t=document.getElementById("slider-captcha-styles");t&&t.remove(),this.imageCache.clear(),this.cachedDimensions=null,Object.keys(this).forEach(t=>{"constructor"!==t&&(this[t]=null)}),this.options.onDestroy&&this.options.onDestroy()}catch(t){}}static create(t){return new PopupSliderCaptcha(t)}static show(t){const i=new PopupSliderCaptcha(t);return i.show(),i}}"undefined"!=typeof module&&module.exports?(module.exports=PopupSliderCaptcha,module.exports.default=PopupSliderCaptcha):"function"==typeof define&&define.amd?define([],()=>PopupSliderCaptcha):"undefined"!=typeof window&&(window.PopupSliderCaptcha=PopupSliderCaptcha,window.SliderCaptcha=PopupSliderCaptcha),e=PopupSliderCaptcha;class PasswordValidator{static CONSTANTS={DEFAULT_TIMEOUT:1e4,CACHE_DURATION:3e5,MIN_PASSWORD_LENGTH:1};static ERROR_TYPES={NETWORK_ERROR:"NETWORK_ERROR",TIMEOUT_ERROR:"TIMEOUT_ERROR",ENCRYPTION_ERROR:"ENCRYPTION_ERROR",VALIDATION_ERROR:"VALIDATION_ERROR",PUBLIC_KEY_ERROR:"PUBLIC_KEY_ERROR"};constructor(t={}){this.options={publicKeyUrl:t.publicKeyUrl||"/externalapi/commonservice/strongPassword/getPublicKey",validateUrl:t.validateUrl||"/externalapi/commonservice/strongPassword/checkPassword",baseUrl:t.baseUrl||"",timeout:t.timeout||PasswordValidator.CONSTANTS.DEFAULT_TIMEOUT,headers:t.headers||{},cacheDuration:t.cacheDuration||PasswordValidator.CONSTANTS.CACHE_DURATION,...t},this.options.publicKeyUrl=this.normalizeUrl(this.options.publicKeyUrl,this.options.baseUrl),this.options.validateUrl=this.normalizeUrl(this.options.validateUrl,this.options.baseUrl),this.publicKeyCache=null,this.publicKeyExpiry=null}normalizeUrl(t,i){return t?/^https?:\/\//.test(t)||!i?t:(i.endsWith("/")?i:i+"/")+(t.startsWith("/")?t.substring(1):t):t}async getPublicKey(){if(this.publicKeyCache&&this.publicKeyExpiry&&Date.now()<this.publicKeyExpiry)return this.publicKeyCache;try{const t=await this.makeRequest(this.options.publicKeyUrl,{method:"GET"});if(!this.isSuccessResponse(t))throw Error("获取公钥失败:"+(t.message||t.msg||"未知错误"));const i=t.data||t.result;if(!i)throw Error("公钥为空");return this.publicKeyCache=i,this.publicKeyExpiry=Date.now()+this.options.cacheDuration,this.publicKeyCache}catch(t){throw this.handleError(PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR,t.message,t),Error("获取公钥失败: "+t.message)}}isSuccessResponse(t){return!(!t||"object"!=typeof t)&&["0"===t.code,0===t.code,!0===t.success,"success"===t.status,!0===t.result].some(t=>!0===t)}handleError(t,i,e=null){const s={[PasswordValidator.ERROR_TYPES.NETWORK_ERROR]:"网络连接失败,请检查网络设置",[PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR]:"请求超时,请重试",[PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR]:"密码加密失败",[PasswordValidator.ERROR_TYPES.VALIDATION_ERROR]:"密码验证失败",[PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR]:"获取公钥失败"}[t]||i||"未知错误";this.options.onError&&this.options.onError({type:t,message:s,originalError:e})}encryptPassword(t,i){try{if(!t)throw Error("密码不能为空");if(t.length<PasswordValidator.CONSTANTS.MIN_PASSWORD_LENGTH)throw Error("密码长度不足");if(!i)throw Error("公钥不能为空");if(void 0===JSEncrypt)throw Error("JSEncrypt库未正确加载,请确保已引入JSEncrypt库");const e=new JSEncrypt;e.setPublicKey(i);const s=e.encrypt(t);if(!s)throw Error("密码加密失败,可能是公钥格式不正确");return s}catch(t){throw this.handleError(PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR,t.message,t),Error("密码加密失败: "+t.message)}}async validatePassword(t,i,e={}){try{if(!t)throw Error("密码不能为空");if(!i)throw Error("用户名不能为空");const s=await this.getPublicKey(),r=this.encryptPassword(t,s),a=await this.makeRequest(this.options.validateUrl,{method:"POST",body:JSON.stringify({userName:i,password:r,timestamp:Date.now(),...e})});return{success:this.isSuccessResponse(a),data:a.data||a.result,message:a.message||a.msg||"验证完成",code:a.code||a.status,originalResponse:a}}catch(t){let i=PasswordValidator.ERROR_TYPES.VALIDATION_ERROR;return t.message.includes("网络")||t.message.includes("fetch")?i=PasswordValidator.ERROR_TYPES.NETWORK_ERROR:t.message.includes("超时")||t.message.includes("timeout")?i=PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR:t.message.includes("加密")&&(i=PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR),this.handleError(i,t.message,t),{success:!1,message:t.message||"密码校验失败",code:i,error:t}}}async makeRequest(t,i={}){const e=new AbortController,s=setTimeout(()=>{e.abort()},this.options.timeout);try{const r=await fetch(t,{...i,headers:{"Content-Type":"application/json",...this.options.headers,...i.headers},signal:e.signal});if(clearTimeout(s),!r.ok)throw Error(`HTTP错误: ${r.status} ${r.statusText}`);const a=await r.json();return this.options.debug,a}catch(t){if(clearTimeout(s),"AbortError"===t.name)throw this.handleError(PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR,"请求超时",t),Error("请求超时");if(t.message.includes("Failed to fetch")||t.message.includes("NetworkError"))throw this.handleError(PasswordValidator.ERROR_TYPES.NETWORK_ERROR,"网络连接失败",t),Error("网络连接失败");throw t}}clearCache(){this.publicKeyCache=null,this.publicKeyExpiry=null}updateOptions(t){this.options={...this.options,...t},this.clearCache()}getCacheStatus(){return{hasCache:!!this.publicKeyCache,isExpired:!this.publicKeyExpiry||Date.now()>this.publicKeyExpiry,expiryTime:this.publicKeyExpiry,remainingTime:this.publicKeyExpiry?Math.max(0,this.publicKeyExpiry-Date.now()):0}}destroy(){try{this.clearCache(),this.options.onDestroy&&this.options.onDestroy()}catch(t){}}}r={PopupSliderCaptcha:e,PasswordValidator:s=PasswordValidator,createPasswordValidator:t,validatePassword:i},"undefined"!=typeof window&&(window.SliderCaptcha=e,window.PopupSliderCaptcha=e,window.PasswordValidator=s,window.createPasswordValidator=t,window.validatePassword=i);export{s as PasswordValidator,e as PopupSliderCaptcha,t as createPasswordValidator,r as default,i as validatePassword};
1
+ import JSEncrypt from 'jsencrypt';
2
+
3
+ /**
4
+ * 纯JavaScript弹窗滑块验证码组件
5
+ */
6
+ class PopupSliderCaptcha {
7
+ static DEFAULTS = {
8
+ width: 400,
9
+ height: 240,
10
+ sliderSize: 40,
11
+ maxRetries: 3,
12
+ timeout: 30000,
13
+ apiUrl: '/externalapi/commonservice/captcha/get',
14
+ verifyUrl: '/externalapi/commonservice/captcha/check',
15
+ baseUrl: '', // 基础域名,如果apiUrl和verifyUrl不包含域名则拼接此域名
16
+ throttleDelay: 16,
17
+ clickMaskClose: false
18
+ }
19
+
20
+ static CSS_CLASSES = {
21
+ overlay: 'slider-captcha-overlay',
22
+ modal: 'slider-captcha-modal',
23
+ header: 'slider-captcha-header',
24
+ container: 'slider-captcha-container',
25
+ track: 'slider-captcha-track',
26
+ btn: 'slider-captcha-btn',
27
+ hint: 'slider-captcha-hint',
28
+ loading: 'slider-captcha-loading',
29
+ error: 'slider-captcha-error'
30
+ }
31
+
32
+ static getStyles() {
33
+ return `:root{--sc-primary:#409eff;--sc-success:#67c23a;--sc-danger:#f56c6c;--sc-border:#e4e7eb;--sc-bg:linear-gradient(90deg, #f7f9fa 0%, #e8f4fd 100%);--sc-text:#333;--sc-text-light:#999;--sc-shadow:0 4px 20px rgba(0,0,0,.3);--sc-radius:8px;--sc-transition:.3s ease}.slider-captcha-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);z-index:9999;display:none;justify-content:center;align-items:center;opacity:0;transition:opacity var(--sc-transition);will-change:opacity}.slider-captcha-overlay.show{opacity:1}.slider-captcha-modal{background:#fff;border-radius:var(--sc-radius);padding:16px 16px 5px;box-shadow:var(--sc-shadow);position:relative;max-width:90vw;max-height:90vh;transform:scale(.8) translateY(-20px);opacity:0;transition:all var(--sc-transition);will-change:transform,opacity}.slider-captcha-modal.show{transform:scale(1) translateY(-10%);opacity:1}.slider-captcha-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding-bottom:10px;border-bottom:1px solid var(--sc-border)}.slider-captcha-container{display:flex;align-items:center;position:relative;border-radius:4px;overflow:hidden;margin-bottom:15px;background:#837a7a;justify-content:center}.slider-captcha-track{width:100%;height:42px;line-height:42px;background:var(--sc-bg);border:1px solid var(--sc-border);border-radius:20px;position:relative;margin-bottom:15px;overflow:hidden}.slider-captcha-btn{width:40px;height:40px;background:#fff;border:1px solid #ccc;border-radius:50%;position:absolute;top:0;left:0;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 4px rgba(0,0,0,.1);transition:all var(--sc-transition);user-select:none;z-index:1;will-change:transform}.slider-captcha-loading{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(255,255,255,.6);display:flex;align-items:center;justify-content:center;flex-direction:column;color:#666;font-size:14px;z-index:10;border-radius:4px}.slider-captcha-error{color:var(--sc-danger);font-size:12px;text-align:center;margin-top:10px;display:none}.slider-captcha-title{margin:0;font-size:16px;color:var(--sc-text)}.slider-captcha-close,.slider-captcha-refresh{background:none;border:none;cursor:pointer;color:var(--sc-text-light);padding:0;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:50%;transition:all var(--sc-transition);position:relative;font-size:0}.slider-captcha-close::before,.slider-captcha-close::after{content:'';position:absolute;width:16px;height:2px;background-color:var(--sc-text-light);border-radius:1px;transition:all var(--sc-transition)}.slider-captcha-close::before{transform:rotate(45deg)}.slider-captcha-close::after{transform:rotate(-45deg)}.slider-captcha-close:hover{background:#f5f5f5;transform:scale(1.1)}.slider-captcha-close:hover::before,.slider-captcha-close:hover::after{background-color:var(--sc-danger)}.slider-captcha-refresh{margin-left:10px}.slider-captcha-refresh svg{width:20px;height:20px;fill:var(--sc-text-light);transition:all var(--sc-transition)}.slider-captcha-refresh:hover{background:#f5f5f5;transform:scale(1.1)}.slider-captcha-refresh:hover svg{fill:var(--sc-primary);transform:rotate(180deg)}.slider-captcha-floating-time{position:absolute;bottom:-40px;left:50%;transform:translateX(-50%);color:#fff;font-size:12px;line-height:12px;white-space:nowrap;opacity:0;pointer-events:none;z-index:10;transition:all var(--sc-transition);background:#fff;padding:4px 15px;border-radius:10px;will-change:transform,opacity}.slider-captcha-floating-time.show{opacity:1;transform:translateX(-50%) translateY(-45px)}.slider-captcha-floating-time.success{color:var(--sc-success)}.slider-captcha-floating-time.fail{color:var(--sc-danger)}.slider-captcha-bg{width:100%;height:100%;object-fit:cover;display:block}.slider-captcha-piece{position:absolute;top:0;left:0;cursor:pointer;transition:none;z-index:2;will-change:transform}.slider-captcha-finger{position:absolute;top:50%;left:10px;transform:translateY(-50%);font-size:20px;animation:fingerSlide 2s ease-in-out infinite;pointer-events:none;z-index:1;opacity:.6;will-change:left,opacity,transform}.slider-captcha-hint{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:var(--sc-text-light);font-size:14px;pointer-events:none;z-index:1;transition:all var(--sc-transition)}.slider-captcha-header-buttons{display:flex;align-items:center}@keyframes fingerSlide{0%{left:10px;opacity:.6;transform:translateY(-50%) scale(1)}50%{opacity:1;transform:translateY(-50%) scale(1.1)}100%{left:calc(50% - 10px);opacity:.6;transform:translateY(-50%) scale(1)}}`
34
+ }
35
+
36
+ static ERROR_TYPES = {
37
+ NETWORK_ERROR: 'NETWORK_ERROR',
38
+ TIMEOUT_ERROR: 'TIMEOUT_ERROR',
39
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
40
+ IMAGE_LOAD_ERROR: 'IMAGE_LOAD_ERROR',
41
+ CAPTCHA_DATA_ERROR: 'CAPTCHA_DATA_ERROR'
42
+ }
43
+
44
+ constructor(options = {}) {
45
+ this.options = { ...PopupSliderCaptcha.DEFAULTS, ...options };
46
+
47
+ // 处理URL,如果apiUrl和verifyUrl不包含域名则拼接baseUrl
48
+ this.options.apiUrl = this.normalizeUrl(this.options.apiUrl, this.options.baseUrl);
49
+ this.options.verifyUrl = this.normalizeUrl(this.options.verifyUrl, this.options.baseUrl);
50
+
51
+ // 初始化状态
52
+ this.elements = {};
53
+ this.state = this.createInitialState();
54
+ this.captchaData = null;
55
+ this.times = [];
56
+ this.startTime = null;
57
+ this.eventListeners = [];
58
+ this.timers = new Set();
59
+ this.rafId = null;
60
+ this.cachedDimensions = null;
61
+ this.imageCache = new Map();
62
+ this.abortController = null;
63
+ this.isInitialized = false;
64
+
65
+ this.throttledHandleMove = this.throttle((e) => this.handleMove(e), this.options.throttleDelay);
66
+
67
+ this.lazyInit();
68
+ }
69
+
70
+ // 懒加载初始化
71
+ lazyInit() {
72
+ // 预加载CSS样式
73
+ this.injectStyles();
74
+
75
+ // 标记为已初始化
76
+ this.isInitialized = true;
77
+ }
78
+
79
+ createInitialState() {
80
+ return {
81
+ isVisible: false,
82
+ isDragging: false,
83
+ currentX: 0,
84
+ startX: 0,
85
+ retryCount: 0,
86
+ isLoading: false
87
+ }
88
+ }
89
+
90
+ /**
91
+ * 标准化URL,如果URL不包含域名则拼接baseUrl
92
+ * @param {string} url - 原始URL
93
+ * @param {string} baseUrl - 基础域名
94
+ * @returns {string} 标准化后的URL
95
+ */
96
+ normalizeUrl(url, baseUrl) {
97
+ if (!url || !baseUrl || (/^https?:\/\//).test(url)) {
98
+ return url
99
+ }
100
+
101
+ const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
102
+ const normalizedUrl = url.startsWith('/') ? url.slice(1) : url;
103
+
104
+ return normalizedBaseUrl + normalizedUrl
105
+ }
106
+
107
+ injectStyles() {
108
+ if (document.querySelector('#slider-captcha-styles')) { return }
109
+
110
+ const style = Object.assign(document.createElement('style'), {
111
+ id: 'slider-captcha-styles',
112
+ textContent: PopupSliderCaptcha.getStyles()
113
+ });
114
+ document.head.appendChild(style);
115
+ }
116
+
117
+ // 创建dom
118
+ createElements() {
119
+ const { options } = this;
120
+ const containerWidth = options.width;
121
+ const containerHeight = options.height;
122
+
123
+ const html = `
124
+ <div class="${PopupSliderCaptcha.CSS_CLASSES.overlay}">
125
+ <div class="${PopupSliderCaptcha.CSS_CLASSES.modal}">
126
+ <div class="${PopupSliderCaptcha.CSS_CLASSES.header}">
127
+ <h3 class="slider-captcha-title">安全验证</h3>
128
+ <div class="slider-captcha-header-buttons">
129
+ <button class="slider-captcha-refresh" title="刷新">
130
+ <svg viewBox="0 0 24 24"><path d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z"/></svg>
131
+ </button>
132
+ <button class="slider-captcha-close" title="关闭"></button>
133
+ </div>
134
+ </div>
135
+ <div class="${PopupSliderCaptcha.CSS_CLASSES.container}" style="width:${containerWidth}px;height:${containerHeight}px;display:none;">
136
+ <img class="slider-captcha-bg" alt="背景图">
137
+ <img class="slider-captcha-piece" alt="滑块">
138
+ <div class="${PopupSliderCaptcha.CSS_CLASSES.loading}">加载中...</div>
139
+ <div class="slider-captcha-floating-time"></div>
140
+ </div>
141
+ <div class="${PopupSliderCaptcha.CSS_CLASSES.track}" style="display:none;">
142
+ <div class="slider-captcha-finger">👉</div>
143
+ <div class="${PopupSliderCaptcha.CSS_CLASSES.btn}">
144
+ <div class="slider-captcha-icon">→</div>
145
+ </div>
146
+ <div class="${PopupSliderCaptcha.CSS_CLASSES.hint}">向右滑动完成验证</div>
147
+ </div>
148
+ <div class="${PopupSliderCaptcha.CSS_CLASSES.error}"></div>
149
+ </div>
150
+ </div>
151
+ `;
152
+
153
+ const tempDiv = document.createElement('div');
154
+ tempDiv.innerHTML = html.trim();
155
+ const overlay = tempDiv.firstElementChild;
156
+ document.body.appendChild(overlay);
157
+
158
+ // 缓存元素引用
159
+ this.elements = {
160
+ overlay,
161
+ modal: overlay.querySelector(`.${PopupSliderCaptcha.CSS_CLASSES.modal}`),
162
+ header: overlay.querySelector(`.${PopupSliderCaptcha.CSS_CLASSES.header}`),
163
+ title: overlay.querySelector('.slider-captcha-title'),
164
+ closeBtn: overlay.querySelector('.slider-captcha-close'),
165
+ refreshBtn: overlay.querySelector('.slider-captcha-refresh'),
166
+ container: overlay.querySelector(`.${PopupSliderCaptcha.CSS_CLASSES.container}`),
167
+ backgroundImg: overlay.querySelector('.slider-captcha-bg'),
168
+ sliderImg: overlay.querySelector('.slider-captcha-piece'),
169
+ loadingText: overlay.querySelector(`.${PopupSliderCaptcha.CSS_CLASSES.loading}`),
170
+ floatingTime: overlay.querySelector('.slider-captcha-floating-time'),
171
+ track: overlay.querySelector(`.${PopupSliderCaptcha.CSS_CLASSES.track}`),
172
+ fingerAnimation: overlay.querySelector('.slider-captcha-finger'),
173
+ btn: overlay.querySelector(`.${PopupSliderCaptcha.CSS_CLASSES.btn}`),
174
+ icon: overlay.querySelector('.slider-captcha-icon'),
175
+ hint: overlay.querySelector(`.${PopupSliderCaptcha.CSS_CLASSES.hint}`),
176
+ error: overlay.querySelector(`.${PopupSliderCaptcha.CSS_CLASSES.error}`)
177
+ };
178
+ }
179
+
180
+ bindEvents() {
181
+ const { elements } = this;
182
+
183
+ const eventConfigs = [
184
+ [elements.closeBtn, 'click', () => this.hide()],
185
+ [elements.refreshBtn, 'click', () => this.refresh()],
186
+ [elements.overlay, 'click', (e) => {
187
+ if (e.target === elements.overlay && this.options.clickMaskClose) { this.hide(); }
188
+ }],
189
+ [document, 'keydown', (e) => {
190
+ if (e.key === 'Escape' && this.state.isVisible) { this.hide(); }
191
+ }],
192
+ [document, 'visibilitychange', () => this.handleVisibilityChange()]
193
+ ];
194
+
195
+ // 批量绑定事件
196
+ eventConfigs.forEach(([element, event, handler]) => {
197
+ this.addEventListener(element, event, handler);
198
+ });
199
+
200
+ this.bindSliderEvents();
201
+ }
202
+
203
+ bindSliderEvents() {
204
+ const { elements } = this;
205
+ const handlers = {
206
+ start: (e) => this.handleStart(e)
207
+ };
208
+
209
+ const sliderEventConfigs = [
210
+ [elements.btn, 'mousedown', handlers.start],
211
+ [elements.btn, 'touchstart', handlers.start],
212
+ [elements.sliderImg, 'mousedown', handlers.start],
213
+ [elements.sliderImg, 'touchstart', handlers.start]
214
+ ];
215
+
216
+ sliderEventConfigs.forEach(([element, event, handler, options]) => {
217
+ this.addEventListener(element, event, handler, options);
218
+ });
219
+ }
220
+
221
+ addEventListener(element, event, handler, options = {}) {
222
+ if (!element || typeof handler !== 'function') { return }
223
+
224
+ element.addEventListener(event, handler, options);
225
+ this.eventListeners.push({ element, event, handler, options });
226
+ }
227
+
228
+ removeEventListener(element, event, handler, options = {}) {
229
+ if (!element || typeof handler !== 'function') { return }
230
+ element.removeEventListener(event, handler, options);
231
+ this.eventListeners = this.eventListeners.filter(l =>
232
+ !(l.element === element && l.event === event && l.handler === handler)
233
+ );
234
+ }
235
+
236
+ removeAllEventListeners() {
237
+ this.eventListeners.forEach(({ element, event, handler, options }) => {
238
+ try {
239
+ element?.removeEventListener?.(event, handler, options);
240
+ } catch (error) {
241
+ console.warn('Failed to remove event listener:', error);
242
+ }
243
+ });
244
+ this.eventListeners.length = 0;
245
+ }
246
+
247
+ getDimensions() {
248
+ if (!this.cachedDimensions) {
249
+ const { track, btn } = this.elements;
250
+ this.cachedDimensions = {
251
+ trackWidth: track.offsetWidth,
252
+ btnWidth: btn.offsetWidth,
253
+ get maxX() { return this.trackWidth - this.btnWidth }
254
+ };
255
+ }
256
+ return this.cachedDimensions
257
+ }
258
+
259
+ getPosition() {
260
+ const { maxX } = this.getDimensions();
261
+ const percentage = this.state.currentX / maxX;
262
+ return Math.round(percentage * (this.options.width - this.options.sliderSize))
263
+ }
264
+
265
+ handleStart(e) {
266
+ if (!this.captchaData || this.state.isDragging || this.state.isLoading) { return }
267
+
268
+ e.preventDefault();
269
+
270
+ Object.assign(this.state, {
271
+ isDragging: true,
272
+ startX: this.getClientX(e) - this.state.currentX
273
+ });
274
+
275
+ this.dragStartTime = Date.now();
276
+ this.times = [{ time: Date.now(), position: this.getPosition() }];
277
+
278
+ this.setTransition(false);
279
+ this.updateUIState('dragging');
280
+ this.cachedDimensions = null; // 清除缓存
281
+
282
+ // 动态绑定移动和结束事件
283
+ this.boundHandleEnd = this.boundHandleEnd || this.handleEnd.bind(this);
284
+ document.addEventListener('mousemove', this.throttledHandleMove, { passive: false });
285
+ document.addEventListener('touchmove', this.throttledHandleMove, { passive: false });
286
+ document.addEventListener('mouseup', this.boundHandleEnd);
287
+ document.addEventListener('touchend', this.boundHandleEnd);
288
+ }
289
+
290
+ handleMove(e) {
291
+ if (!this.state.isDragging) { return }
292
+ e.preventDefault();
293
+
294
+ this.lastClientX = this.getClientX(e);
295
+
296
+ if (!this.rafId) {
297
+ this.rafId = requestAnimationFrame(() => {
298
+ const deltaX = this.lastClientX - this.state.startX;
299
+ const { maxX } = this.getDimensions();
300
+
301
+ this.state.currentX = Math.max(0, Math.min(deltaX, maxX));
302
+ this.times.push({ time: Date.now(), position: this.getPosition() });
303
+
304
+ this.updateSliderPosition();
305
+ this.rafId = null;
306
+ });
307
+ }
308
+ }
309
+
310
+ handleEnd() {
311
+ if (!this.state.isDragging) { return }
312
+
313
+ // 移除动态绑定的事件
314
+ document.removeEventListener('mousemove', this.throttledHandleMove);
315
+ document.removeEventListener('touchmove', this.throttledHandleMove);
316
+ document.removeEventListener('mouseup', this.boundHandleEnd);
317
+ document.removeEventListener('touchend', this.boundHandleEnd);
318
+
319
+ this.times.push({ time: Date.now(), position: this.getPosition() });
320
+ this.state.isDragging = false;
321
+
322
+ if (this.rafId) {
323
+ cancelAnimationFrame(this.rafId);
324
+ this.rafId = null;
325
+ }
326
+
327
+ this.verify();
328
+ }
329
+
330
+ handleVisibilityChange() {
331
+ const animationState = document.hidden ? 'paused' : 'running';
332
+ if (this.elements.fingerAnimation?.style) {
333
+ this.elements.fingerAnimation.style.animationPlayState = animationState;
334
+ }
335
+ }
336
+
337
+ getClientX = (e) => e.type.includes('touch') ? e.touches[0].clientX : e.clientX
338
+
339
+ setTransition(enabled) {
340
+ const transition = enabled ? 'all 0.3s ease' : 'none';
341
+ requestAnimationFrame(() => {
342
+ const { btn, sliderImg } = this.elements;
343
+ btn.style.transition = sliderImg.style.transition = transition;
344
+ });
345
+ }
346
+
347
+ updateUIState(state) {
348
+ const { elements } = this;
349
+ const updates = {
350
+ dragging: () => {
351
+ elements.hint.style.opacity = '0';
352
+ elements.fingerAnimation.style.display = 'none';
353
+ },
354
+ success: () => {
355
+ Object.assign(elements.btn.style, { background: 'var(--sc-success)' });
356
+ Object.assign(elements.icon.style, { color: 'white' });
357
+ elements.icon.textContent = '✓';
358
+ },
359
+ fail: () => {
360
+ Object.assign(elements.btn.style, { background: 'var(--sc-danger)' });
361
+ Object.assign(elements.icon.style, { color: 'white' });
362
+ elements.icon.textContent = '✗';
363
+ },
364
+ reset: () => {
365
+ Object.assign(elements.btn.style, { background: 'white' });
366
+ Object.assign(elements.icon.style, { color: '#666' });
367
+ elements.icon.textContent = '→';
368
+ elements.fingerAnimation.style.display = 'block';
369
+ this.updateHintText('向右滑动完成验证', 'var(--sc-text-light)');
370
+ },
371
+ loading: () => {
372
+ elements.hint.style.opacity = '0';
373
+ elements.fingerAnimation.style.display = 'none';
374
+ Object.assign(elements.track.style, { pointerEvents: 'none', opacity: '0.6' });
375
+ }
376
+ };
377
+
378
+ if (updates[state]) {
379
+ requestAnimationFrame(() => {
380
+ updates[state]();
381
+ if (state !== 'loading') {
382
+ Object.assign(elements.track.style, { pointerEvents: 'auto', opacity: '1' });
383
+ }
384
+ });
385
+ }
386
+ }
387
+
388
+ updateHintText(text, color) {
389
+ requestAnimationFrame(() => {
390
+ Object.assign(this.elements.hint, { textContent: text });
391
+ Object.assign(this.elements.hint.style, { color, opacity: '1' });
392
+ });
393
+ }
394
+
395
+ updateSliderPosition() {
396
+ const { elements, options, state } = this;
397
+ const { maxX } = this.getDimensions();
398
+ const pieceX = (state.currentX / maxX) * (options.width - options.sliderSize);
399
+ const progress = state.currentX / maxX;
400
+
401
+ // 优化:使用transform3d触发GPU加速,减少重绘
402
+ requestAnimationFrame(() => {
403
+ elements.btn.style.transform = `translate3d(${state.currentX}px, 0, 0)`;
404
+ elements.sliderImg.style.transform = `translate3d(${pieceX}px, 0, 0)`;
405
+ elements.fingerAnimation.style.opacity = progress >= 0.8 ? '0' : '0.6';
406
+ });
407
+ }
408
+
409
+ // 优化:简化显示/隐藏逻辑
410
+ show() {
411
+ this.state.isVisible = true;
412
+
413
+ if (!this.elements.overlay) {
414
+ this.createElements();
415
+ this.bindEvents();
416
+ }
417
+
418
+ // 优化:使用transform3d触发GPU加速
419
+ this.elements.overlay.style.display = 'flex';
420
+ this.elements.overlay.style.transform = 'translate3d(0,0,0)';
421
+
422
+ // 立即显示,减少重绘
423
+ requestAnimationFrame(() => {
424
+ this.elements.overlay.classList.add('show');
425
+ this.elements.modal.classList.add('show');
426
+
427
+ // 预加载验证码数据
428
+ this.preloadCaptcha();
429
+ });
430
+ }
431
+
432
+ // 预加载验证码数据,提高首次显示速度
433
+ async preloadCaptcha() {
434
+ try {
435
+ // 显示加载状态
436
+ this.showLoading();
437
+
438
+ // 并行执行:获取验证码数据 + 预加载图片
439
+ const [captchaData] = await Promise.all([
440
+ this.fetchCaptchaData(),
441
+ this.preloadImages()
442
+ ]);
443
+
444
+ this.captchaData = captchaData;
445
+ this.showCaptcha();
446
+ await this.renderCaptcha();
447
+ } catch (error) {
448
+ this.handleError(PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR, error.message, error);
449
+ }
450
+ }
451
+
452
+ // 通用请求方法
453
+ async makeRequest(url, body = {}) {
454
+ // 取消之前的请求
455
+ if (this.abortController) {
456
+ this.abortController.abort();
457
+ }
458
+ this.abortController = new AbortController();
459
+
460
+ const response = await fetch(url, {
461
+ method: 'POST',
462
+ headers: {
463
+ 'Content-Type': 'application/json',
464
+ ...this.options.headers
465
+ },
466
+ body: JSON.stringify({
467
+ canvasWidth: this.options.width,
468
+ canvasHeight: this.options.height,
469
+ timestamp: Date.now(),
470
+ ...this.options.requestData,
471
+ ...body
472
+ }),
473
+ signal: this.abortController.signal
474
+ });
475
+
476
+ if (!response.ok) {
477
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
478
+ }
479
+
480
+ return response.json()
481
+ }
482
+
483
+ // 获取验证码数据
484
+ async fetchCaptchaData() {
485
+ this.startTime = Date.now();
486
+ const data = await this.makeRequest(this.options.apiUrl);
487
+
488
+ if (!this.validateCaptchaData(data)) {
489
+ throw new Error('验证码数据格式错误')
490
+ }
491
+
492
+ return data.data
493
+ }
494
+
495
+ // 预加载图片资源
496
+ preloadImages() {
497
+ const imageUrls = [
498
+ 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE3LjY1LDYuMzVDMTYuMiw0LjkgMTQuMjEsNCAxMiw0QTgsOCAwIDAsMCA0LDEyQTgsOCAwIDAsMCAxMiwyMEMxNS43MywyMCAxOC44NCwxNy40NSAxOS43MywxNEgxNy42NUMxNi44MywxNi4zMyAxNC42MSwxOCAxMiwxOEE2LDYgMCAwLDEgNiwxMkE2LDYgMCAwLDEgMTIsNkMxMy42Niw2IDE1LjE0LDYuNjkgMTYuMjIsNy43OEwxMywxMUgyMFY0TDE3LjY1LDYuMzVaIiBmaWxsPSIjOTk5Ii8+Cjwvc3ZnPgo='
499
+ ];
500
+
501
+ return Promise.all(imageUrls.map(url => {
502
+ return new Promise((resolve) => {
503
+ const img = new Image();
504
+ img.onload = img.onerror = () => resolve(); // 忽略错误,继续执行
505
+ img.src = url;
506
+ })
507
+ }))
508
+ }
509
+
510
+ hide() {
511
+ this.state.isVisible = false;
512
+ this.elements.overlay.classList.remove('show');
513
+ this.elements.modal.classList.remove('show');
514
+
515
+ this.safeSetTimeout(() => {
516
+ this.elements.overlay.style.display = 'none';
517
+ document.body.removeChild(this.elements.overlay);
518
+ this.reset();
519
+ this.options.onClose?.();
520
+ this.elements.overlay = null;
521
+ }, 300);
522
+ }
523
+
524
+ // 处理请求错误
525
+ handleRequestError(error) {
526
+ if (error.name === 'AbortError') {
527
+ this.handleError(PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR, '请求被取消', error);
528
+ } else if (
529
+ error.message?.includes('Failed to fetch') ||
530
+ error.message?.includes('NetworkError')
531
+ ) {
532
+ this.handleError(PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR, '网络连接失败', error);
533
+ } else {
534
+ this.handleError(PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR, error.message, error);
535
+ }
536
+ }
537
+
538
+ handleError(errorType, message, originalError = null) {
539
+ const errorMessages = {
540
+ [PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR]: '网络连接失败,请检查网络设置',
541
+ [PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR]: '请求超时,请重试',
542
+ [PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR]: '验证失败,请重试',
543
+ [PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR]: '图片加载失败,请刷新重试',
544
+ [PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR]: '验证码数据错误,请刷新重试'
545
+ };
546
+
547
+ const errorMessage = errorMessages[errorType] || message || '未知错误';
548
+
549
+ // 调用用户自定义错误处理
550
+ if (this.options.onError) {
551
+ this.options.onError({
552
+ type: errorType,
553
+ message: errorMessage,
554
+ originalError
555
+ });
556
+ }
557
+
558
+ this.showError(errorMessage);
559
+ console.error(`滑块验证码错误 [${errorType}]:`, errorMessage, originalError);
560
+ }
561
+
562
+ async loadCaptcha() {
563
+ try {
564
+ this.showLoading();
565
+ this.startTime = Date.now();
566
+
567
+ const data = await this.makeRequest(this.options.apiUrl);
568
+
569
+ if (!this.validateCaptchaData(data)) {
570
+ throw new Error('验证码数据格式错误')
571
+ }
572
+
573
+ this.captchaData = data.data;
574
+ this.showCaptcha();
575
+ await this.renderCaptcha();
576
+ } catch (error) {
577
+ this.handleRequestError(error);
578
+ }
579
+ }
580
+
581
+ // 优化:添加验证码数据验证方法
582
+ validateCaptchaData(data) {
583
+ if (!data || typeof data !== 'object') { return false }
584
+
585
+ const requiredFields = [
586
+ 'canvasSrc',
587
+ 'blockSrc',
588
+ 'canvasWidth',
589
+ 'canvasHeight',
590
+ 'blockWidth',
591
+ 'blockHeight',
592
+ 'blockY',
593
+ 'nonceStr'
594
+ ];
595
+ const dataObj = data.data || data;
596
+
597
+ return requiredFields.every((field) => {
598
+ const value = dataObj[field];
599
+ return value !== null && value !== undefined && value !== ''
600
+ })
601
+ }
602
+
603
+ renderCaptcha() {
604
+ return new Promise((resolve, reject) => {
605
+ let hasError = false;
606
+
607
+ // 优化:并行加载图片,提高性能
608
+ const loadPromises = [
609
+ this.loadImageAsync(this.elements.backgroundImg, this.captchaData.canvasSrc, {
610
+ width: this.captchaData.canvasWidth,
611
+ height: this.captchaData.canvasHeight
612
+ }),
613
+ this.loadImageAsync(this.elements.sliderImg, this.captchaData.blockSrc, {
614
+ width: this.captchaData.blockWidth,
615
+ height: this.captchaData.blockHeight,
616
+ top: this.captchaData.blockY
617
+ })
618
+ ];
619
+
620
+ Promise.all(loadPromises)
621
+ .then(() => {
622
+ if (!hasError) {
623
+ this.hideLoading();
624
+ resolve();
625
+ }
626
+ })
627
+ .catch((error) => {
628
+ if (!hasError) {
629
+ hasError = true;
630
+ this.handleError(PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR, '图片加载失败', error);
631
+ reject(error);
632
+ }
633
+ });
634
+ })
635
+ }
636
+
637
+ loadImageAsync(imgElement, src, styles) {
638
+ return new Promise((resolve, reject) => {
639
+ // 检查缓存
640
+ if (this.imageCache.has(src)) {
641
+ const cachedImg = this.imageCache.get(src);
642
+ imgElement.src = cachedImg.src;
643
+ this.applyStyles(imgElement, styles);
644
+ resolve();
645
+ return
646
+ }
647
+
648
+ // 优化:减少超时时间,提高响应速度
649
+ const IMAGE_LOAD_TIMEOUT = 5000;
650
+ const timeoutId = this.safeSetTimeout(() => {
651
+ reject(new Error('图片加载超时'));
652
+ }, IMAGE_LOAD_TIMEOUT);
653
+
654
+ // 优化:使用更高效的图片加载
655
+ const img = new Image();
656
+ img.crossOrigin = 'anonymous'; // 支持跨域
657
+ img.decoding = 'async'; // 异步解码
658
+
659
+
660
+ img.onload = function onImageLoad() {
661
+ this.safeClearTimeout(timeoutId);
662
+
663
+ // 优化:使用OffscreenCanvas进行图片处理(如果支持)
664
+ if (typeof OffscreenCanvas !== 'undefined') {
665
+ try {
666
+ const canvas = new OffscreenCanvas(img.width, img.height);
667
+ const ctx = canvas.getContext('2d');
668
+ ctx.drawImage(img, 0, 0);
669
+ const imageData = ctx.getImageData(0, 0, img.width, img.height);
670
+ this.imageCache.set(src, { src: img.src, imageData });
671
+ } catch (e) {
672
+ this.imageCache.set(src, img.cloneNode());
673
+ }
674
+ } else {
675
+ this.imageCache.set(src, img.cloneNode());
676
+ }
677
+
678
+ imgElement.src = img.src;
679
+ this.applyStyles(imgElement, styles);
680
+ resolve();
681
+ }.bind(this);
682
+
683
+ img.onerror = function onImageError(error) {
684
+ this.safeClearTimeout(timeoutId);
685
+ reject(error);
686
+ }.bind(this);
687
+
688
+ img.src = src;
689
+ })
690
+ }
691
+
692
+ applyStyles(element, styles) {
693
+ Object.entries(styles).forEach(([key, value]) => {
694
+ element.style[key] = typeof value === 'number' ? `${value}px` : value;
695
+ });
696
+ }
697
+
698
+ showLoading() {
699
+ this.state.isLoading = true;
700
+ this.batchUpdateStyles({
701
+ container: { display: 'block' },
702
+ loadingText: { display: 'flex' },
703
+ error: { display: 'none' }
704
+ });
705
+ this.updateUIState('loading');
706
+ }
707
+
708
+ hideLoading() {
709
+ this.state.isLoading = false;
710
+ this.batchUpdateStyles({ loadingText: { display: 'none' } });
711
+ this.updateUIState('reset');
712
+ }
713
+
714
+ showCaptcha() {
715
+ this.batchUpdateStyles({
716
+ container: { display: 'block' },
717
+ track: { display: 'block' },
718
+ error: { display: 'none' }
719
+ });
720
+ }
721
+
722
+ showError(message) {
723
+ this.hideLoading();
724
+ this.batchUpdateStyles({
725
+ error: { display: 'block', textContent: message }
726
+ });
727
+ }
728
+
729
+ batchUpdateStyles(updates) {
730
+ requestAnimationFrame(() => {
731
+ Object.entries(updates).forEach(([elementKey, styles]) => {
732
+ const element = this.elements[elementKey];
733
+ if (element) {
734
+ Object.entries(styles).forEach(([prop, value]) => {
735
+ if (prop === 'textContent') {
736
+ element.textContent = value;
737
+ } else {
738
+ element.style[prop] = value;
739
+ }
740
+ });
741
+ }
742
+ });
743
+ });
744
+ }
745
+
746
+ async verify() {
747
+ if (!this.captchaData) {
748
+ this.onVerifyFail('验证码数据丢失');
749
+ return
750
+ }
751
+
752
+ try {
753
+ const data = await this.makeRequest(this.options.verifyUrl, {
754
+ loginVo: {
755
+ nonceStr: this.captchaData.nonceStr,
756
+ value: this.getPosition()
757
+ },
758
+ dragEventList: [...this.times],
759
+ ...this.options.verifyData
760
+ });
761
+
762
+ // 优化:更灵活的验证结果判断
763
+ if (this.isVerifySuccess(data)) {
764
+ this.onVerifySuccess(data.data || data.result);
765
+ } else {
766
+ this.onVerifyFail(data.message || data.msg || '验证失败,请重试!');
767
+ }
768
+ } catch (error) {
769
+ this.handleRequestError(error);
770
+ }
771
+ }
772
+
773
+ isVerifySuccess(data) {
774
+ if (!data || typeof data !== 'object') { return false }
775
+
776
+ // 支持多种成功标识
777
+ const successIndicators = [
778
+ data.code === '0',
779
+ data.code === 0,
780
+ data.success === true,
781
+ data.status === 'success',
782
+ data.result === true
783
+ ];
784
+
785
+ return successIndicators.some((indicator) => indicator === true)
786
+ }
787
+
788
+ onVerifySuccess(ticket) {
789
+ const duration = this.dragStartTime ?
790
+ Date.now() - this.dragStartTime :
791
+ Date.now() - this.startTime;
792
+ const durationText = `验证成功!耗时:${(duration / 1000).toFixed(2)}s`;
793
+
794
+ this.updateUIState('success');
795
+ this.showFloatingTime(durationText, 'success');
796
+
797
+ this.safeSetTimeout(() => {
798
+ this.options.onSuccess?.({
799
+ ticket,
800
+ timestamp: Date.now(),
801
+ duration
802
+ });
803
+ this.hide();
804
+ }, 2000);
805
+ }
806
+
807
+ showFloatingTime(text, type = 'success') {
808
+ const { elements } = this;
809
+ elements.floatingTime.textContent = text;
810
+ elements.floatingTime.className = `slider-captcha-floating-time ${type}`;
811
+
812
+ this.safeSetTimeout(() => elements.floatingTime.classList.add('show'), 100);
813
+ this.safeSetTimeout(() => {
814
+ elements.floatingTime.className = 'slider-captcha-floating-time';
815
+ }, 2500); // 优化:延长显示时间,避免被reset清除
816
+ }
817
+
818
+ onVerifyFail(message) {
819
+ this.state.retryCount++;
820
+ this.updateUIState('fail');
821
+ this.showFloatingTime(message, 'fail');
822
+
823
+ this.safeSetTimeout(() => {
824
+ if (this.state.retryCount >= this.options.maxRetries) {
825
+ this.refresh();
826
+ } else {
827
+ this.reset();
828
+ }
829
+ }, 2500); // 优化:延长等待时间,确保浮动提示完整显示
830
+ }
831
+
832
+ reset() {
833
+ this.clearAllTimers();
834
+
835
+ // 重置状态
836
+ Object.assign(this.state, {
837
+ isDragging: false,
838
+ currentX: 0,
839
+ startX: 0,
840
+ isLoading: false
841
+ });
842
+
843
+ this.times = [];
844
+ this.startTime = null;
845
+ this.dragStartTime = null; // 优化:重置拖拽开始时间
846
+ this.cachedDimensions = null;
847
+
848
+ requestAnimationFrame(() => {
849
+ this.setTransition(true);
850
+ this.elements.btn.style.transform = 'translate3d(0px, 0, 0)';
851
+ this.elements.sliderImg.style.transform = 'translate3d(0px, 0, 0)';
852
+ this.updateUIState('reset');
853
+ this.elements.error.style.display = 'none';
854
+ });
855
+ }
856
+
857
+ refresh() {
858
+ try {
859
+ this.reset();
860
+ this.state.retryCount = 0;
861
+ this.loadCaptcha();
862
+ } catch (e) {
863
+ console.error(e);
864
+ }
865
+ }
866
+
867
+ // 安全的定时器管理
868
+ safeSetTimeout(callback, delay) {
869
+ const timerId = setTimeout(() => {
870
+ this.timers.delete(timerId);
871
+ callback();
872
+ }, delay);
873
+ this.timers.add(timerId);
874
+ return timerId
875
+ }
876
+
877
+ safeClearTimeout(timerId) {
878
+ if (timerId) {
879
+ clearTimeout(timerId);
880
+ this.timers.delete(timerId);
881
+ }
882
+ }
883
+
884
+ clearAllTimers() {
885
+ this.timers.forEach((timer) => {
886
+ clearTimeout(timer);
887
+ clearInterval(timer);
888
+ });
889
+ this.timers.clear();
890
+
891
+ if (this.rafId) {
892
+ cancelAnimationFrame(this.rafId);
893
+ this.rafId = null;
894
+ }
895
+ }
896
+
897
+ // 清理图片资源
898
+ cleanupImages() {
899
+ if (this.elements.backgroundImg) {
900
+ this.elements.backgroundImg.src = '';
901
+ this.elements.backgroundImg.onload = null;
902
+ this.elements.backgroundImg.onerror = null;
903
+ }
904
+ if (this.elements.sliderImg) {
905
+ this.elements.sliderImg.src = '';
906
+ this.elements.sliderImg.onload = null;
907
+ this.elements.sliderImg.onerror = null;
908
+ }
909
+ this.imageCache.clear();
910
+ }
911
+
912
+ throttle(func, delay) {
913
+ let lastCall = 0;
914
+ return (...args) => {
915
+ const now = Date.now();
916
+ if (now - lastCall >= delay) {
917
+ lastCall = now;
918
+ return func.apply(this, args)
919
+ }
920
+ return undefined
921
+ }
922
+ }
923
+
924
+ destroy() {
925
+ try {
926
+ // 停止当前的拖拽状态并清理相关事件
927
+ if (this.state.isDragging) {
928
+ document.removeEventListener('mousemove', this.throttledHandleMove);
929
+ document.removeEventListener('touchmove', this.throttledHandleMove);
930
+ if (this.boundHandleEnd) {
931
+ document.removeEventListener('mouseup', this.boundHandleEnd);
932
+ document.removeEventListener('touchend', this.boundHandleEnd);
933
+ }
934
+ this.state.isDragging = false;
935
+ }
936
+
937
+ // 取消所有进行中的请求
938
+ if (this.abortController) {
939
+ this.abortController.abort();
940
+ this.abortController = null;
941
+ }
942
+
943
+ // 清理所有定时器
944
+ this.clearAllTimers();
945
+
946
+ // 移除所有事件监听器
947
+ this.removeAllEventListeners();
948
+
949
+ // 清理图片资源
950
+ this.cleanupImages();
951
+
952
+ // 移除DOM元素
953
+ if (this.elements?.overlay?.parentNode) {
954
+ this.elements.overlay.parentNode.removeChild(this.elements.overlay);
955
+ }
956
+
957
+ // 清理样式表
958
+ const styleElement = document.getElementById('slider-captcha-styles');
959
+ if (styleElement) {
960
+ styleElement.remove();
961
+ }
962
+
963
+ // 彻底清理引用
964
+ this.imageCache.clear();
965
+ this.cachedDimensions = null;
966
+ this.elements = {};
967
+ this.state = {};
968
+ this.times = [];
969
+ this.eventListeners = [];
970
+ this.timers.clear();
971
+ this.throttledHandleMove = null;
972
+ this.boundHandleEnd = null;
973
+
974
+ // 调用销毁回调
975
+ if (this.options.onDestroy) {
976
+ this.options.onDestroy();
977
+ }
978
+
979
+ // 清空 options 以释放内存
980
+ this.options = null;
981
+ } catch (error) {
982
+ console.error('销毁滑块验证码时出错:', error);
983
+ }
984
+ }
985
+
986
+ static create(options) {
987
+ return new PopupSliderCaptcha(options)
988
+ }
989
+
990
+ static show(options) {
991
+ const instance = new PopupSliderCaptcha(options);
992
+ instance.show();
993
+ return instance
994
+ }
995
+ }
996
+
997
+ // 模块导出
998
+ if (typeof module !== 'undefined' && module.exports) {
999
+ module.exports = PopupSliderCaptcha;
1000
+ module.exports.default = PopupSliderCaptcha;
1001
+ } else if (typeof define === 'function' && define.amd) {
1002
+ define([], () => PopupSliderCaptcha);
1003
+ } else if (typeof window !== 'undefined') {
1004
+ window.PopupSliderCaptcha = PopupSliderCaptcha;
1005
+ window.SliderCaptcha = PopupSliderCaptcha;
1006
+ }
1007
+
1008
+ var PopupSliderCaptcha$1 = PopupSliderCaptcha;
1009
+
1010
+ /**
1011
+ * 密码校验工具类
1012
+ * 提供密码加密和校验功能
1013
+ */
1014
+ class PasswordValidator {
1015
+ static CONSTANTS = {
1016
+ DEFAULT_TIMEOUT: 10000,
1017
+ CACHE_DURATION: 5 * 60 * 1000, // 5分钟缓存
1018
+ MIN_PASSWORD_LENGTH: 1
1019
+ }
1020
+
1021
+ static ERROR_TYPES = {
1022
+ NETWORK_ERROR: 'NETWORK_ERROR',
1023
+ TIMEOUT_ERROR: 'TIMEOUT_ERROR',
1024
+ ENCRYPTION_ERROR: 'ENCRYPTION_ERROR',
1025
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
1026
+ PUBLIC_KEY_ERROR: 'PUBLIC_KEY_ERROR'
1027
+ }
1028
+
1029
+ constructor(options = {}) {
1030
+ this.options = {
1031
+ publicKeyUrl: options.publicKeyUrl || '/externalapi/commonservice/strongPassword/getPublicKey',
1032
+ validateUrl: options.validateUrl || '/externalapi/commonservice/strongPassword/checkPassword',
1033
+ baseUrl: options.baseUrl || '', // 基础域名,如果URL不包含域名则拼接此域名
1034
+ timeout: options.timeout || PasswordValidator.CONSTANTS.DEFAULT_TIMEOUT,
1035
+ headers: options.headers || {},
1036
+ cacheDuration: options.cacheDuration || PasswordValidator.CONSTANTS.CACHE_DURATION,
1037
+ ...options
1038
+ };
1039
+
1040
+ // 处理URL,如果URL不包含域名则拼接baseUrl
1041
+ this.options.publicKeyUrl = this.normalizeUrl(this.options.publicKeyUrl, this.options.baseUrl);
1042
+ this.options.validateUrl = this.normalizeUrl(this.options.validateUrl, this.options.baseUrl);
1043
+
1044
+ // 缓存公钥,避免重复请求
1045
+ this.publicKeyCache = null;
1046
+ this.publicKeyExpiry = null;
1047
+ this.encryptor = null; // 缓存加密实例
1048
+ }
1049
+
1050
+ /**
1051
+ * 标准化URL,如果URL不包含域名则拼接baseUrl
1052
+ * @param {string} url - 原始URL
1053
+ * @param {string} baseUrl - 基础域名
1054
+ * @returns {string} 标准化后的URL
1055
+ */
1056
+ normalizeUrl(url, baseUrl) {
1057
+ if (!url) {
1058
+ return url
1059
+ }
1060
+
1061
+ // 如果URL已经包含协议(http://或https://),直接返回
1062
+ if ((/^https?:\/\//).test(url) || !baseUrl) {
1063
+ return url
1064
+ }
1065
+
1066
+ // 确保baseUrl以/结尾,url以/开头
1067
+ const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
1068
+ const normalizedUrl = url.startsWith('/') ? url.substring(1) : url;
1069
+
1070
+ return normalizedBaseUrl + normalizedUrl
1071
+ }
1072
+
1073
+ /**
1074
+ * 获取公钥
1075
+ * @returns {Promise<string>} 公钥字符串
1076
+ */
1077
+ async getPublicKey() {
1078
+ // 检查缓存是否有效
1079
+ if (this.publicKeyCache && this.publicKeyExpiry && Date.now() < this.publicKeyExpiry) {
1080
+ return this.publicKeyCache
1081
+ }
1082
+
1083
+ try {
1084
+ const response = await this.makeRequest(this.options.publicKeyUrl, {
1085
+ method: 'GET'
1086
+ });
1087
+
1088
+ if (!this.isSuccessResponse(response)) {
1089
+ throw new Error(`获取公钥失败:${response.message || response.msg || '未知错误'}`)
1090
+ }
1091
+
1092
+ // 验证公钥格式
1093
+ const publicKey = response.data || response.result;
1094
+ if (!publicKey) {
1095
+ throw new Error('公钥为空')
1096
+ }
1097
+
1098
+ // 缓存公钥
1099
+ this.publicKeyCache = publicKey;
1100
+ this.publicKeyExpiry = Date.now() + this.options.cacheDuration;
1101
+
1102
+ return this.publicKeyCache
1103
+ } catch (error) {
1104
+ console.error('获取公钥失败:', error);
1105
+ this.handleError(PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR, error.message, error);
1106
+ throw new Error(`获取公钥失败: ${error.message}`)
1107
+ }
1108
+ }
1109
+
1110
+ // 优化:添加响应成功判断方法
1111
+ isSuccessResponse(response) {
1112
+ if (!response || typeof response !== 'object') {
1113
+ return false
1114
+ }
1115
+
1116
+ const successIndicators = [
1117
+ response.code === '0',
1118
+ response.code === 0,
1119
+ response.success === true,
1120
+ response.status === 'success',
1121
+ response.result === true
1122
+ ];
1123
+
1124
+ return successIndicators.some((indicator) => indicator === true)
1125
+ }
1126
+
1127
+ handleError(errorType, message, originalError = null) {
1128
+ const errorMessages = {
1129
+ [PasswordValidator.ERROR_TYPES.NETWORK_ERROR]: '网络连接失败,请检查网络设置',
1130
+ [PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR]: '请求超时,请重试',
1131
+ [PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR]: '密码加密失败',
1132
+ [PasswordValidator.ERROR_TYPES.VALIDATION_ERROR]: '密码验证失败',
1133
+ [PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR]: '获取公钥失败'
1134
+ };
1135
+
1136
+ const errorMessage = errorMessages[errorType] || message || '未知错误';
1137
+
1138
+ // 调用用户自定义错误处理
1139
+ if (this.options.onError) {
1140
+ this.options.onError({
1141
+ type: errorType,
1142
+ message: errorMessage,
1143
+ originalError
1144
+ });
1145
+ }
1146
+
1147
+ console.error(`密码校验器错误 [${errorType}]:`, errorMessage, originalError);
1148
+ }
1149
+
1150
+ /**
1151
+ * 使用RSA公钥加密密码
1152
+ * @param {string} password 原始密码
1153
+ * @param {string} publicKey 公钥字符串
1154
+ * @returns {string} 加密后的密码
1155
+ */
1156
+ encryptPassword(password, publicKey) {
1157
+ try {
1158
+ if (!password) {
1159
+ throw new Error('密码不能为空')
1160
+ }
1161
+
1162
+ if (password.length < PasswordValidator.CONSTANTS.MIN_PASSWORD_LENGTH) {
1163
+ throw new Error('密码长度不足')
1164
+ }
1165
+
1166
+ if (!publicKey) {
1167
+ throw new Error('公钥不能为空')
1168
+ }
1169
+
1170
+ if (typeof JSEncrypt === 'undefined') {
1171
+ throw new Error('JSEncrypt库未正确加载,请确保已引入JSEncrypt库')
1172
+ }
1173
+
1174
+ // 优化:重用 JSEncrypt 实例
1175
+ if (!this.encryptor) {
1176
+ this.encryptor = new JSEncrypt();
1177
+ }
1178
+
1179
+ // 如果公钥发生变化,更新公钥
1180
+ if (this.lastPublicKey !== publicKey) {
1181
+ this.encryptor.setPublicKey(publicKey);
1182
+ this.lastPublicKey = publicKey;
1183
+ }
1184
+
1185
+ const encrypted = this.encryptor.encrypt(password);
1186
+
1187
+ if (!encrypted) {
1188
+ throw new Error('密码加密失败,可能是公钥格式不正确')
1189
+ }
1190
+
1191
+ return encrypted
1192
+ } catch (error) {
1193
+ console.error('密码加密失败:', error);
1194
+ this.handleError(PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR, error.message, error);
1195
+ throw new Error(`密码加密失败: ${error.message}`)
1196
+ }
1197
+ }
1198
+
1199
+ /**
1200
+ * 校验密码
1201
+ * @param {string} password 原始密码
1202
+ * @param {string} userName 用户名
1203
+ * @param {Object} additionalData 额外的校验数据
1204
+ * @returns {Promise<Object>} 校验结果
1205
+ */
1206
+ async validatePassword(password, userName, additionalData = {}) {
1207
+ try {
1208
+ if (!password) {
1209
+ throw new Error('密码不能为空')
1210
+ }
1211
+
1212
+ if (!userName) {
1213
+ throw new Error('用户名不能为空')
1214
+ }
1215
+
1216
+ // 1. 获取公钥
1217
+ const publicKey = await this.getPublicKey();
1218
+
1219
+ // 2. 加密密码
1220
+ const encryptedPassword = this.encryptPassword(password, publicKey);
1221
+
1222
+ // 3. 调用校验接口
1223
+ const response = await this.makeRequest(this.options.validateUrl, {
1224
+ method: 'POST',
1225
+ body: JSON.stringify({
1226
+ userName,
1227
+ password: encryptedPassword,
1228
+ timestamp: Date.now(),
1229
+ ...additionalData
1230
+ })
1231
+ });
1232
+
1233
+ return {
1234
+ success: this.isSuccessResponse(response),
1235
+ data: response.data || response.result,
1236
+ message: response.message || response.msg || '验证完成',
1237
+ code: response.code || response.status,
1238
+ originalResponse: response
1239
+ }
1240
+ } catch (error) {
1241
+ console.error('密码校验失败:', error);
1242
+
1243
+ // 优化:根据错误类型返回不同的错误信息
1244
+ let errorType = PasswordValidator.ERROR_TYPES.VALIDATION_ERROR;
1245
+ if (error.message.includes('网络') || error.message.includes('fetch')) {
1246
+ errorType = PasswordValidator.ERROR_TYPES.NETWORK_ERROR;
1247
+ } else if (error.message.includes('超时') || error.message.includes('timeout')) {
1248
+ errorType = PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR;
1249
+ } else if (error.message.includes('加密')) {
1250
+ errorType = PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR;
1251
+ }
1252
+
1253
+ this.handleError(errorType, error.message, error);
1254
+
1255
+ return {
1256
+ success: false,
1257
+ message: error.message || '密码校验失败',
1258
+ code: errorType,
1259
+ error
1260
+ }
1261
+ }
1262
+ }
1263
+
1264
+ /**
1265
+ * 发送HTTP请求的通用方法
1266
+ * @param {string} url 请求地址
1267
+ * @param {Object} options 请求选项
1268
+ * @returns {Promise<Object>} 响应结果
1269
+ */
1270
+ async makeRequest(url, options = {}) {
1271
+ // 优化:为每个请求创建独立的AbortController,避免多个请求互相取消
1272
+ const abortController = new AbortController();
1273
+
1274
+ const timeoutId = setTimeout(() => {
1275
+ abortController.abort();
1276
+ }, this.options.timeout);
1277
+
1278
+ try {
1279
+ const response = await fetch(url, {
1280
+ ...options,
1281
+ headers: {
1282
+ 'Content-Type': 'application/json',
1283
+ ...this.options.headers,
1284
+ ...options.headers
1285
+ },
1286
+ signal: abortController.signal
1287
+ });
1288
+
1289
+ clearTimeout(timeoutId);
1290
+
1291
+ if (!response.ok) {
1292
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`)
1293
+ }
1294
+
1295
+ const data = await response.json();
1296
+
1297
+ // 优化:记录请求日志(开发环境)
1298
+ if (this.options.debug) {
1299
+ console.log('密码校验器请求:', { url, options, response: data });
1300
+ }
1301
+
1302
+ return data
1303
+ } catch (error) {
1304
+ clearTimeout(timeoutId);
1305
+
1306
+ if (error.name === 'AbortError') {
1307
+ this.handleError(PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR, '请求超时', error);
1308
+ throw new Error('请求超时')
1309
+ }
1310
+
1311
+ if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
1312
+ this.handleError(PasswordValidator.ERROR_TYPES.NETWORK_ERROR, '网络连接失败', error);
1313
+ throw new Error('网络连接失败')
1314
+ }
1315
+
1316
+ throw error
1317
+ }
1318
+ }
1319
+
1320
+ /**
1321
+ * 清除公钥缓存
1322
+ */
1323
+ clearCache() {
1324
+ this.publicKeyCache = null;
1325
+ this.publicKeyExpiry = null;
1326
+ }
1327
+
1328
+ /**
1329
+ * 更新配置
1330
+ * @param {Object} newOptions 新的配置选项
1331
+ */
1332
+ updateOptions(newOptions) {
1333
+ this.options = { ...this.options, ...newOptions };
1334
+ // 清除缓存,使用新配置重新获取
1335
+ this.clearCache();
1336
+ }
1337
+
1338
+ /**
1339
+ * 获取缓存状态
1340
+ * @returns {Object} 缓存状态信息
1341
+ */
1342
+ getCacheStatus() {
1343
+ return {
1344
+ hasCache: Boolean(this.publicKeyCache),
1345
+ isExpired: this.publicKeyExpiry ? Date.now() > this.publicKeyExpiry : true,
1346
+ expiryTime: this.publicKeyExpiry,
1347
+ remainingTime: this.publicKeyExpiry ? Math.max(0, this.publicKeyExpiry - Date.now()) : 0
1348
+ }
1349
+ }
1350
+
1351
+ /**
1352
+ * 销毁实例,清理资源
1353
+ */
1354
+ destroy() {
1355
+ try {
1356
+ // 清除缓存
1357
+ this.clearCache();
1358
+ this.encryptor = null;
1359
+ this.lastPublicKey = null;
1360
+
1361
+ // 调用销毁回调
1362
+ if (this.options.onDestroy) {
1363
+ this.options.onDestroy();
1364
+ }
1365
+
1366
+ this.options = null;
1367
+ } catch (error) {
1368
+ console.error('销毁密码校验器时出错:', error);
1369
+ }
1370
+ }
1371
+ }
1372
+
1373
+ // 导出类和创建实例的工厂函数
1374
+ var PasswordValidator$1 = PasswordValidator;
1375
+
1376
+ /**
1377
+ * 创建密码校验器实例的工厂函数
1378
+ * @param {Object} options 配置选项
1379
+ * @returns {PasswordValidator} 密码校验器实例
1380
+ */
1381
+ function createPasswordValidator(options) {
1382
+ return new PasswordValidator(options)
1383
+ }
1384
+
1385
+ /**
1386
+ * 快速校验密码的便捷函数
1387
+ * @param {string} password 密码
1388
+ * @param {string} userName 用户名
1389
+ * @param {Object} options 配置选项
1390
+ * @param {Object} additionalData 额外数据
1391
+ * @returns {Promise<Object>} 校验结果
1392
+ */
1393
+ function validatePassword(password, userName, options = {}, additionalData = {}) {
1394
+ const validator = new PasswordValidator(options);
1395
+ return validator.validatePassword(password, userName, additionalData)
1396
+ }
1397
+
1398
+ // 导入滑块验证码组件(使用默认导入)
1399
+
1400
+ // 默认导出(向后兼容)
1401
+ var index = {
1402
+ PopupSliderCaptcha: PopupSliderCaptcha$1,
1403
+ PasswordValidator: PasswordValidator$1,
1404
+ createPasswordValidator,
1405
+ validatePassword
1406
+ };
1407
+
1408
+ // 全局注册(用于UMD构建)
1409
+ if (typeof window !== 'undefined') {
1410
+ window.SliderCaptcha = PopupSliderCaptcha$1;
1411
+ window.PopupSliderCaptcha = PopupSliderCaptcha$1;
1412
+ window.PasswordValidator = PasswordValidator$1;
1413
+ window.createPasswordValidator = createPasswordValidator;
1414
+ window.validatePassword = validatePassword;
1415
+ }
1416
+
1417
+ export { PasswordValidator$1 as PasswordValidator, PopupSliderCaptcha$1 as PopupSliderCaptcha, createPasswordValidator, index as default, validatePassword };