slider-captcha-sdk 1.0.13 → 1.0.15

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.min.js CHANGED
@@ -1 +1 @@
1
- !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jsencrypt")):"function"==typeof define&&define.amd?define(["exports","jsencrypt"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).SliderCaptchaSDK={},t.JSEncrypt)}(this,function(t,e){"use strict";function i(t){return new PasswordValidator(t)}async function s(t,e={},i={}){const s=new PasswordValidator(e),a=i.userName||"";return await s.validatePassword(t,a,i)}class PopupSliderCaptcha{static DEFAULTS={width:320,height:155,sliderSize:38,maxRetries:3,timeout:3e4,apiUrl:"/api/captcha",verifyUrl:"/api/captcha/verify",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;white-space:nowrap;opacity:0;pointer-events:none;z-index:10;transition:all var(--sc-transition);background:#fff;padding:2px 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}}"}constructor(t={}){this.options={...PopupSliderCaptcha.DEFAULTS,...t},this.elements={},this.state=this.createInitialState(),this.captchaData=null,this.times=[],this.startTime=null,this.eventListeners=[],this.timers=new Set,this.rafId=null,this.cachedDimensions=null,this.imageCache=new Map,this.throttledHandleMove=this.throttle(t=>this.handleMove(t),this.options.throttleDelay),this.init()}createInitialState(){return{isVisible:!1,isDragging:!1,currentX:0,startX:0,retryCount:0,isLoading:!1}}init(){this.injectStyles(),this.createElements(),this.bindEvents()}injectStyles(){if(document.querySelector("#slider-captcha-styles"))return;const t=document.createElement("style");t.id="slider-captcha-styles",t.textContent=PopupSliderCaptcha.getStyles(),document.head.appendChild(t)}createElements(){const{elements:t,options:e}=this;[["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]].forEach(([e,i,s,a])=>{t[e]=this.createElement(i,s,a)}),t.container.style.cssText=`width:${e.width}px;height:${e.height}px`,t.refreshBtn.innerHTML='<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>',this.assembleDOM(),this.setInitialState()}createElement(t,e="",i=""){const s=document.createElement(t);return e&&(s.className=e),i&&(s.textContent=i),s}assembleDOM(){const{elements:t}=this,e=this.createElement("div","slider-captcha-header-buttons");e.append(t.refreshBtn,t.closeBtn),t.header.append(t.title,e),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;this.addEventListener(t.closeBtn,"click",()=>this.hide()),this.addEventListener(t.refreshBtn,"click",()=>this.refresh()),this.addEventListener(t.overlay,"click",e=>{e.target===t.overlay&&this.options.clickMaskClose&&this.hide()}),this.addEventListener(document,"keydown",t=>{"Escape"===t.key&&this.state.isVisible&&this.hide()}),this.addEventListener(document,"visibilitychange",()=>this.handleVisibilityChange()),this.bindSliderEvents()}bindSliderEvents(){const{elements:t}=this,e={start:t=>this.handleStart(t),move:this.throttledHandleMove,end:()=>this.handleEnd()};this.addEventListener(t.btn,"mousedown",e.start),this.addEventListener(t.btn,"touchstart",e.start),this.addEventListener(t.sliderImg,"mousedown",e.start),this.addEventListener(t.sliderImg,"touchstart",e.start),this.addEventListener(document,"mousemove",e.move,{passive:!1}),this.addEventListener(document,"touchmove",e.move,{passive:!1}),this.addEventListener(document,"mouseup",e.end),this.addEventListener(document,"touchend",e.end)}addEventListener(t,e,i,s={}){t&&"function"==typeof i&&(t.addEventListener(e,i,s),this.eventListeners.push({element:t,event:e,handler:i,options:s}))}removeAllEventListeners(){this.eventListeners.forEach(({element:t,event:e,handler:i,options:s})=>{try{t?.removeEventListener?.(e,i,s)}catch(t){}}),this.eventListeners.length=0}getDimensions(){if(!this.cachedDimensions){const t=this.elements.track.offsetWidth,e=this.elements.btn.offsetWidth;this.cachedDimensions={trackWidth:t,btnWidth:e,maxX:t-e}}return this.cachedDimensions}getPosition(){const{maxX:t}=this.getDimensions(),e=this.state.currentX/t;return Math.round(e*(this.options.width-this.options.sliderSize))}handleStart(t){!this.captchaData||this.state.isDragging||this.state.isLoading||(t.preventDefault(),this.state.isDragging=!0,this.state.startX=this.getClientX(t)-this.state.currentX,this.startTime=this.startTime||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 e=this.getClientX(t)-this.state.startX,{maxX:i}=this.getDimensions();this.state.currentX=Math.max(0,Math.min(e,i)),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&&(this.elements.fingerAnimation.style.animationPlayState=t)}getClientX(t){return t.type.includes("touch")?t.touches[0].clientX:t.clientX}setTransition(t){const e=t?"all 0.3s ease":"none";requestAnimationFrame(()=>{this.elements.btn.style.transition=e,this.elements.sliderImg.style.transition=e})}updateUIState(t){const{elements:e}=this,i={dragging:()=>{e.hint.style.opacity="0",e.fingerAnimation.style.display="none"},success:()=>{Object.assign(e.btn.style,{background:"var(--sc-success)"}),Object.assign(e.icon.style,{innerHTML:"✓",color:"white"}),e.icon.innerHTML="✓"},fail:()=>{Object.assign(e.btn.style,{background:"var(--sc-danger)"}),Object.assign(e.icon.style,{innerHTML:"✗",color:"white"}),e.icon.innerHTML="✗"},reset:()=>{Object.assign(e.btn.style,{background:"white"}),Object.assign(e.icon.style,{color:"#666"}),e.icon.innerHTML="→",e.fingerAnimation.style.display="block",this.updateHintText("向右滑动完成验证","var(--sc-text-light)")},loading:()=>{e.hint.style.opacity="0",e.fingerAnimation.style.display="none",Object.assign(e.track.style,{pointerEvents:"none",opacity:"0.6"})}};i[t]&&requestAnimationFrame(()=>{i[t](),"loading"!==t&&Object.assign(e.track.style,{pointerEvents:"auto",opacity:"1"})})}updateHintText(t,e){requestAnimationFrame(()=>{Object.assign(this.elements.hint,{textContent:t}),Object.assign(this.elements.hint.style,{color:e,opacity:"1"})})}updateSliderPosition(){const{elements:t,options:e,state:i}=this,{maxX:s}=this.getDimensions(),a=i.currentX/s*(e.width-e.sliderSize),n=i.currentX/s;requestAnimationFrame(()=>{t.btn.style.transform=`translateX(${i.currentX}px)`,t.sliderImg.style.transform=`translateX(${a}px)`,t.fingerAnimation.style.opacity=.8>n?"0.6":"0"})}show(){this.state.isVisible=!0,this.elements.overlay.style.display="flex",this.elements.overlay.offsetHeight,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)}async loadCaptcha(){try{this.showLoading(),this.startTime=Date.now();const t=new AbortController,e=this.safeSetTimeout(()=>t.abort(),this.options.timeout),i=await fetch(this.options.apiUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({place:2,timestamp:Date.now()}),signal:t.signal});if(this.safeClearTimeout(e),!i.ok)throw Error("HTTP "+i.status);const s=await i.json();if(!s.data?.canvasSrc||!s.data?.blockSrc)throw Error("验证码数据格式错误");this.captchaData=s.data,this.showCaptcha(),await this.renderCaptcha()}catch(t){const e="AbortError"===t.name?"请求超时,请重试":"加载验证码失败: "+t.message;this.showError(e)}}async renderCaptcha(){return new Promise((t,e)=>{let i=0;const s=()=>2===++i&&(this.hideLoading(),t()),a=()=>e(Error("图片加载失败"));this.loadImage(this.elements.backgroundImg,this.captchaData.canvasSrc,{width:this.captchaData.canvasWidth,height:this.captchaData.canvasHeight},s,a),this.loadImage(this.elements.sliderImg,this.captchaData.blockSrc,{width:this.captchaData.blockWidth,height:this.captchaData.blockHeight,top:this.captchaData.blockY},s,a)})}loadImage(t,e,i,s,a){if(this.imageCache.has(e)){const a=this.imageCache.get(e);return t.src=a.src,this.applyStyles(t,i),void s()}t.onload=()=>{this.imageCache.set(e,t.cloneNode()),s()},t.onerror=a,t.src=e,this.applyStyles(t,i)}applyStyles(t,e){Object.entries(e).forEach(([e,i])=>{t.style[e]="number"==typeof i?i+"px":i})}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,e])=>{const i=this.elements[t];i&&Object.entries(e).forEach(([t,e])=>{"textContent"===t?i.textContent=e:i.style[t]=e})})})}async verify(){if(this.captchaData)try{const t=new AbortController,e=this.safeSetTimeout(()=>t.abort(),this.options.timeout),i=await fetch(this.options.verifyUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({loginVo:{nonceStr:this.captchaData.nonceStr,value:this.getPosition()},dragEventList:[...this.times]}),signal:t.signal});if(this.safeClearTimeout(e),!i.ok)throw Error("HTTP "+i.status);const s=await i.json();"0"===s.code||!0===s.success?this.onVerifySuccess(s.data):this.onVerifyFail(s.message||"验证失败,请重试!")}catch(t){const e="AbortError"===t.name?"验证超时,请重试":"网络错误";this.onVerifyFail(e)}else this.onVerifyFail("验证码数据丢失")}onVerifySuccess(t){const e=Date.now()-this.startTime,i=`验证成功!耗时:${(e/1e3).toFixed(2)}s`;this.updateUIState("success"),this.showFloatingTime(i,"success"),this.safeSetTimeout(()=>{this.options.onSuccess?.({ticket:t,timestamp:Date.now(),duration:e}),this.hide()},2e3)}showFloatingTime(t,e="success"){const{elements:i}=this;i.floatingTime.textContent=t,i.floatingTime.className="slider-captcha-floating-time "+e,this.safeSetTimeout(()=>i.floatingTime.classList.add("show"),100),this.safeSetTimeout(()=>{i.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.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(){this.reset(),this.state.retryCount=0,this.loadCaptcha()}safeSetTimeout(t,e){const i=setTimeout(()=>{this.timers.delete(i),t()},e);return this.timers.add(i),i}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,e){let i=0;return function(...s){const a=Date.now();if(a-i>=e)return i=a,t.apply(this,s)}}debounce(t,e){let i;return function(...s){clearTimeout(i),i=setTimeout(()=>t.apply(this,s),e)}}destroy(){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(),Object.keys(this).forEach(t=>{"constructor"!==t&&(this[t]=null)})}static create(t){return new PopupSliderCaptcha(t)}static show(t){const e=new PopupSliderCaptcha(t);return e.show(),e}}"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);class PasswordValidator{constructor(t={}){this.options={publicKeyUrl:t.publicKeyUrl||"/microservice/strongPassword/getPublicKey",validateUrl:t.validateUrl||"/microservice/strongPassword/checkPassword",timeout:t.timeout||1e4,headers:t.headers||{},...t},this.publicKeyCache=null,this.publicKeyExpiry=null}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("0"!==t.code)throw Error("获取公钥失败:"+(t.message||"未知错误"));return this.publicKeyCache=t.data,this.publicKeyExpiry=Date.now()+3e5,this.publicKeyCache}catch(t){throw Error("获取公钥失败: "+t.message)}}encryptPassword(t,i){try{if(!e)throw Error("JSEncrypt库未正确加载");const s=new e;s.setPublicKey(i);const a=s.encrypt(t);if(!a)throw Error("密码加密失败");return a}catch(t){throw Error("密码加密失败: "+t.message)}}async validatePassword(t,e,i={}){try{const s=await this.getPublicKey(),a=this.encryptPassword(t,s);return{...await this.makeRequest(this.options.validateUrl,{method:"POST",body:JSON.stringify({userName:e,password:a,timestamp:Date.now(),...i})})}}catch(t){return{success:!1,message:t.msg,code:"VALIDATION_ERROR"}}}async makeRequest(t,e={}){const i=new AbortController,s=setTimeout(()=>i.abort(),this.options.timeout);try{const a=await fetch(t,{...e,headers:{"Content-Type":"application/json",...this.options.headers,...e.headers},signal:i.signal});if(clearTimeout(s),!a.ok)throw Error(`HTTP错误: ${a.status} ${a.statusText}`);return await a.json()}catch(t){if(clearTimeout(s),"AbortError"===t.name)throw Error("请求超时");throw t}}clearCache(){this.publicKeyCache=null,this.publicKeyExpiry=null}updateOptions(t){this.options={...this.options,...t},this.clearCache()}}var a={PopupSliderCaptcha:PopupSliderCaptcha,PasswordValidator:PasswordValidator,createPasswordValidator:i,validatePassword:s};"undefined"!=typeof window&&(window.SliderCaptcha=PopupSliderCaptcha,window.PopupSliderCaptcha=PopupSliderCaptcha,window.PasswordValidator=PasswordValidator,window.createPasswordValidator=i,window.validatePassword=s),t.PasswordValidator=PasswordValidator,t.PopupSliderCaptcha=PopupSliderCaptcha,t.createPasswordValidator=i,t.default=a,t.validatePassword=s,Object.defineProperty(t,"__esModule",{value:!0})});
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jsencrypt")):"function"==typeof define&&define.amd?define(["exports","jsencrypt"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).SliderCaptchaSDK={},t.JSEncrypt)}(this,function(t,JSEncrypt){"use strict";function e(t){return new PasswordValidator(t)}async function s(t,e={},s={}){const i=new PasswordValidator(e),a=s.userName||"";return await i.validatePassword(t,a,s)}var i,a,r;class PopupSliderCaptcha{static DEFAULTS={width:320,height:155,sliderSize:38,maxRetries:3,timeout:3e4,apiUrl:"/api/captcha",verifyUrl:"/api/captcha/verify",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;white-space:nowrap;opacity:0;pointer-events:none;z-index:10;transition:all var(--sc-transition);background:#fff;padding:2px 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 CONSTANTS={CACHE_DURATION:3e5,MAX_RETRY_ATTEMPTS:3,DEFAULT_TIMEOUT:3e4,ANIMATION_DURATION:300,FLOATING_TIME_DURATION:2500,THROTTLE_DELAY:16};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,timeout:t.timeout||PopupSliderCaptcha.CONSTANTS.DEFAULT_TIMEOUT,maxRetries:t.maxRetries||PopupSliderCaptcha.CONSTANTS.MAX_RETRY_ATTEMPTS,throttleDelay:t.throttleDelay||PopupSliderCaptcha.CONSTANTS.THROTTLE_DELAY},this.elements={},this.state=this.createInitialState(),this.captchaData=null,this.times=[],this.startTime=null,this.eventListeners=[],this.timers=new Set,this.rafId=null,this.cachedDimensions=null,this.imageCache=new Map,this.abortController=null,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}}init(){this.injectStyles(),this.createElements(),this.bindEvents()}injectStyles(){if(document.querySelector("#slider-captcha-styles"))return;const t=document.createElement("style");t.id="slider-captcha-styles",t.textContent=PopupSliderCaptcha.getStyles(),document.head.appendChild(t)}createElements(){const{elements:t,options:e}=this;[["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]].forEach(([e,s,i,a])=>{t[e]=this.createElement(s,i,a)}),t.container.style.cssText=`width:${e.width}px;height:${e.height}px`,t.refreshBtn.innerHTML='<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>',this.assembleDOM(),this.setInitialState()}createElement(t,e="",s=""){const i=document.createElement(t);return e&&(i.className=e),s&&(i.textContent=s),i}assembleDOM(){const{elements:t}=this,e=this.createElement("div","slider-captcha-header-buttons");e.append(t.refreshBtn,t.closeBtn),t.header.append(t.title,e),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;this.addEventListener(t.closeBtn,"click",()=>this.hide()),this.addEventListener(t.refreshBtn,"click",()=>this.refresh()),this.addEventListener(t.overlay,"click",e=>{e.target===t.overlay&&this.options.clickMaskClose&&this.hide()}),this.addEventListener(document,"keydown",t=>{"Escape"===t.key&&this.state.isVisible&&this.hide()}),this.addEventListener(document,"visibilitychange",()=>this.handleVisibilityChange()),this.bindSliderEvents()}bindSliderEvents(){const{elements:t}=this,e={start:t=>this.handleStart(t),move:this.throttledHandleMove,end:()=>this.handleEnd()};this.addEventListener(t.btn,"mousedown",e.start),this.addEventListener(t.btn,"touchstart",e.start),this.addEventListener(t.sliderImg,"mousedown",e.start),this.addEventListener(t.sliderImg,"touchstart",e.start),this.addEventListener(document,"mousemove",e.move,{passive:!1}),this.addEventListener(document,"touchmove",e.move,{passive:!1}),this.addEventListener(document,"mouseup",e.end),this.addEventListener(document,"touchend",e.end)}addEventListener(t,e,s,i={}){t&&"function"==typeof s&&(t.addEventListener(e,s,i),this.eventListeners.push({element:t,event:e,handler:s,options:i}))}removeAllEventListeners(){this.eventListeners.forEach(({element:t,event:e,handler:s,options:i})=>{try{t?.removeEventListener?.(e,s,i)}catch(t){}}),this.eventListeners.length=0}getDimensions(){if(!this.cachedDimensions){const t=this.elements.track.offsetWidth,e=this.elements.btn.offsetWidth;this.cachedDimensions={trackWidth:t,btnWidth:e,maxX:t-e}}return this.cachedDimensions}getPosition(){const{maxX:t}=this.getDimensions(),e=this.state.currentX/t;return Math.round(e*(this.options.width-this.options.sliderSize))}handleStart(t){!this.captchaData||this.state.isDragging||this.state.isLoading||(t.preventDefault(),this.state.isDragging=!0,this.state.startX=this.getClientX(t)-this.state.currentX,this.startTime=this.startTime||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 e=this.getClientX(t)-this.state.startX,{maxX:s}=this.getDimensions();this.state.currentX=Math.max(0,Math.min(e,s)),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&&(this.elements.fingerAnimation.style.animationPlayState=t)}getClientX(t){return t.type.includes("touch")?t.touches[0].clientX:t.clientX}setTransition(t){const e=t?"all 0.3s ease":"none";requestAnimationFrame(()=>{this.elements.btn.style.transition=e,this.elements.sliderImg.style.transition=e})}updateUIState(t){const{elements:e}=this,s={dragging:()=>{e.hint.style.opacity="0",e.fingerAnimation.style.display="none"},success:()=>{Object.assign(e.btn.style,{background:"var(--sc-success)"}),Object.assign(e.icon.style,{innerHTML:"✓",color:"white"}),e.icon.innerHTML="✓"},fail:()=>{Object.assign(e.btn.style,{background:"var(--sc-danger)"}),Object.assign(e.icon.style,{innerHTML:"✗",color:"white"}),e.icon.innerHTML="✗"},reset:()=>{Object.assign(e.btn.style,{background:"white"}),Object.assign(e.icon.style,{color:"#666"}),e.icon.innerHTML="→",e.fingerAnimation.style.display="block",this.updateHintText("向右滑动完成验证","var(--sc-text-light)")},loading:()=>{e.hint.style.opacity="0",e.fingerAnimation.style.display="none",Object.assign(e.track.style,{pointerEvents:"none",opacity:"0.6"})}};s[t]&&requestAnimationFrame(()=>{s[t](),"loading"!==t&&Object.assign(e.track.style,{pointerEvents:"auto",opacity:"1"})})}updateHintText(t,e){requestAnimationFrame(()=>{Object.assign(this.elements.hint,{textContent:t}),Object.assign(this.elements.hint.style,{color:e,opacity:"1"})})}updateSliderPosition(){const{elements:t,options:e,state:s}=this,{maxX:i}=this.getDimensions(),a=s.currentX/i*(e.width-e.sliderSize),r=s.currentX/i;requestAnimationFrame(()=>{t.btn.style.transform=`translateX(${s.currentX}px)`,t.sliderImg.style.transform=`translateX(${a}px)`,t.fingerAnimation.style.opacity=.8>r?"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,e,s=null){const i={[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]||e||"未知错误";this.options.onError&&this.options.onError({type:t,message:i,originalError:s}),this.showError(i)}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({place:2,timestamp:Date.now(),...this.options.requestData}),signal:this.abortController.signal});if(!t.ok)throw Error(`HTTP ${t.status}: ${t.statusText}`);const e=await t.json();if(!this.validateCaptchaData(e))throw Error("验证码数据格式错误");this.captchaData=e.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 e=t.data||t;return["canvasSrc","blockSrc","canvasWidth","canvasHeight","blockWidth","blockHeight","blockY","nonceStr"].every(t=>{const s=e[t];return null!=s&&""!==s})}async renderCaptcha(){return new Promise((t,e)=>{let s=!1;const i=[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(i).then(()=>{s||(this.hideLoading(),t())}).catch(t=>{s||(s=!0,this.handleError(PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR,"图片加载失败",t),e(t))})})}async loadImageAsync(t,e,s){return new Promise((i,a)=>{if(this.imageCache.has(e)){const a=this.imageCache.get(e);return t.src=a.src,this.applyStyles(t,s),void i()}const r=this.safeSetTimeout(()=>{a(Error("图片加载超时"))},1e4);t.onload=()=>{this.safeClearTimeout(r),this.imageCache.set(e,t.cloneNode()),i()},t.onerror=t=>{this.safeClearTimeout(r),a(t)},t.src=e,this.applyStyles(t,s)})}loadImage(t,e,s,i,a){if(this.imageCache.has(e)){const a=this.imageCache.get(e);return t.src=a.src,this.applyStyles(t,s),void i()}t.onload=()=>{this.imageCache.set(e,t.cloneNode()),i()},t.onerror=a,t.src=e,this.applyStyles(t,s)}applyStyles(t,e){Object.entries(e).forEach(([e,s])=>{t.style[e]="number"==typeof s?s+"px":s})}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,e])=>{const s=this.elements[t];s&&Object.entries(e).forEach(([t,e])=>{"textContent"===t?s.textContent=e:s.style[t]=e})})})}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 e=await t.json();this.isVerifySuccess(e)?this.onVerifySuccess(e.data||e.result):this.onVerifyFail(e.message||e.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 e=Date.now()-this.startTime,s=`验证成功!耗时:${(e/1e3).toFixed(2)}s`;this.updateUIState("success"),this.showFloatingTime(s,"success"),this.safeSetTimeout(()=>{this.options.onSuccess?.({ticket:t,timestamp:Date.now(),duration:e}),this.hide()},2e3)}showFloatingTime(t,e="success"){const{elements:s}=this;s.floatingTime.textContent=t,s.floatingTime.className="slider-captcha-floating-time "+e,this.safeSetTimeout(()=>s.floatingTime.classList.add("show"),100),this.safeSetTimeout(()=>{s.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.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(){this.reset(),this.state.retryCount=0,this.loadCaptcha()}safeSetTimeout(t,e){const s=setTimeout(()=>{this.timers.delete(s),t()},e);return this.timers.add(s),s}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,e){let s=0;return function(...i){const a=Date.now();if(a-s>=e)return s=a,t.apply(this,i)}}debounce(t,e){let s;return function(...i){clearTimeout(s),s=setTimeout(()=>t.apply(this,i),e)}}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&&!this.hasOtherInstances()&&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){}}hasOtherInstances(){return"undefined"!=typeof window&&window.sliderCaptchaInstanceCount>1}static create(t){return new PopupSliderCaptcha(t)}static show(t){const e=new PopupSliderCaptcha(t);return e.show(),e}}"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),i=PopupSliderCaptcha;class PasswordValidator{static CONSTANTS={DEFAULT_TIMEOUT:1e4,CACHE_DURATION:3e5,MAX_RETRY_ATTEMPTS:3,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||"/microservice/strongPassword/getPublicKey",validateUrl:t.validateUrl||"/microservice/strongPassword/checkPassword",timeout:t.timeout||PasswordValidator.CONSTANTS.DEFAULT_TIMEOUT,headers:t.headers||{},retryAttempts:t.retryAttempts||PasswordValidator.CONSTANTS.MAX_RETRY_ATTEMPTS,cacheDuration:t.cacheDuration||PasswordValidator.CONSTANTS.CACHE_DURATION,...t},this.publicKeyCache=null,this.publicKeyExpiry=null,this.retryCount=0,this.abortController=null}async getPublicKey(){if(this.publicKeyCache&&this.publicKeyExpiry&&Date.now()<this.publicKeyExpiry)return this.publicKeyCache;try{this.abortController&&this.abortController.abort(),this.abortController=new AbortController;const t=await this.makeRequest(this.options.publicKeyUrl,{method:"GET",signal:this.abortController.signal});if(!this.isSuccessResponse(t))throw Error("获取公钥失败:"+(t.message||t.msg||"未知错误"));const e=t.data||t.result;if(!this.validatePublicKey(e))throw Error("公钥格式无效");return this.publicKeyCache=e,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)}validatePublicKey(t){return!(!t||"string"!=typeof t)&&/^-----BEGIN PUBLIC KEY-----[\s\S]*-----END PUBLIC KEY-----$/.test(t.trim())}handleError(t,e,s=null){const i={[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]||e||"未知错误";this.options.onError&&this.options.onError({type:t,message:i,originalError:s})}encryptPassword(t,e){try{if(!t||"string"!=typeof t)throw Error("密码不能为空");if(t.length<PasswordValidator.CONSTANTS.MIN_PASSWORD_LENGTH)throw Error("密码长度不足");if(!e||"string"!=typeof e)throw Error("公钥不能为空");if(void 0===JSEncrypt)throw Error("JSEncrypt库未正确加载,请确保已引入JSEncrypt库");const s=new JSEncrypt;s.setPublicKey(e);const i=s.encrypt(t);if(!i)throw Error("密码加密失败,可能是公钥格式不正确");return i}catch(t){throw this.handleError(PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR,t.message,t),Error("密码加密失败: "+t.message)}}async validatePassword(t,e,s={}){try{if(!t||"string"!=typeof t)throw Error("密码不能为空");if(!e||"string"!=typeof e)throw Error("用户名不能为空");this.retryCount=0;const i=await this.getPublicKey(),a=this.encryptPassword(t,i),r=await this.makeRequest(this.options.validateUrl,{method:"POST",body:JSON.stringify({userName:e,password:a,timestamp:Date.now(),...s})});return{success:this.isSuccessResponse(r),data:r.data||r.result,message:r.message||r.msg||"验证完成",code:r.code||r.status,originalResponse:r}}catch(t){let e=PasswordValidator.ERROR_TYPES.VALIDATION_ERROR;return t.message.includes("网络")||t.message.includes("fetch")?e=PasswordValidator.ERROR_TYPES.NETWORK_ERROR:t.message.includes("超时")||t.message.includes("timeout")?e=PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR:t.message.includes("加密")&&(e=PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR),this.handleError(e,t.message,t),{success:!1,message:t.message||"密码校验失败",code:e,error:t}}}async makeRequest(t,e={}){this.abortController&&this.abortController.abort(),this.abortController=new AbortController;const s=setTimeout(()=>{this.abortController.abort()},this.options.timeout);try{const i=await fetch(t,{...e,headers:{"Content-Type":"application/json",...this.options.headers,...e.headers},signal:this.abortController.signal});if(clearTimeout(s),!i.ok)throw Error(`HTTP错误: ${i.status} ${i.statusText}`);const a=await i.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()}cancelRequest(){this.abortController&&(this.abortController.abort(),this.abortController=null)}isRequesting(){return this.abortController&&!this.abortController.signal.aborted}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.cancelRequest(),this.clearCache(),this.retryCount=0,this.options.onDestroy&&this.options.onDestroy()}catch(t){}}}r={PopupSliderCaptcha:i,PasswordValidator:a=PasswordValidator,createPasswordValidator:e,validatePassword:s},"undefined"!=typeof window&&(window.SliderCaptcha=i,window.PopupSliderCaptcha=i,window.PasswordValidator=a,window.createPasswordValidator=e,window.validatePassword=s),t.PasswordValidator=a,t.PopupSliderCaptcha=i,t.createPasswordValidator=e,t.default=r,t.validatePassword=s,Object.defineProperty(t,"__esModule",{value:!0})});
@@ -1,27 +1,106 @@
1
1
  declare module 'slider-captcha-sdk/password-validator' {
2
+ // 优化:添加更详细的类型定义
2
3
  export interface PasswordValidatorOptions {
4
+ // API配置
3
5
  publicKeyUrl?: string
4
6
  validateUrl?: string
7
+
8
+ // 请求配置
5
9
  timeout?: number
6
10
  headers?: Record<string, string>
11
+ retryAttempts?: number
12
+ cacheDuration?: number
13
+
14
+ // 调试配置
15
+ debug?: boolean
16
+
17
+ // 回调函数
18
+ onError?: (error: ErrorInfo) => void
19
+ onDestroy?: () => void
7
20
  }
8
21
 
9
22
  export interface ValidationResult {
10
23
  success: boolean
11
24
  message: string
12
25
  data?: any
13
- code: string
26
+ code: string | number
27
+ originalResponse?: any
28
+ error?: Error
29
+ }
30
+
31
+ export interface ErrorInfo {
32
+ type: string
33
+ message: string
34
+ originalError?: Error
35
+ }
36
+
37
+ export interface CacheStatus {
38
+ hasCache: boolean
39
+ isExpired: boolean
40
+ expiryTime?: number
41
+ remainingTime: number
42
+ }
43
+
44
+ export interface PublicKeyResponse {
45
+ code: string | number
46
+ data?: string
47
+ result?: string
48
+ message?: string
49
+ msg?: string
50
+ success?: boolean
51
+ status?: string
52
+ }
53
+
54
+ export interface ValidateResponse {
55
+ code: string | number
56
+ data?: any
57
+ result?: any
58
+ message?: string
59
+ msg?: string
60
+ success?: boolean
61
+ status?: string
14
62
  }
15
63
 
16
64
  export default class PasswordValidator {
17
65
  constructor(options?: PasswordValidatorOptions)
66
+
67
+ // 核心方法
18
68
  getPublicKey(): Promise<string>
19
69
  encryptPassword(password: string, publicKey: string): string
20
- validatePassword(password: string, userName: string, additionalData?: any): Promise<ValidationResult>
70
+ validatePassword(password: string, userName: string, additionalData?: Record<string, any>): Promise<ValidationResult>
71
+
72
+ // 缓存管理
21
73
  clearCache(): void
22
- updateOptions(newOptions: PasswordValidatorOptions): void
74
+ getCacheStatus(): CacheStatus
75
+
76
+ // 配置管理
77
+ updateOptions(newOptions: Partial<PasswordValidatorOptions>): void
78
+
79
+ // 请求控制
80
+ cancelRequest(): void
81
+ isRequesting(): boolean
82
+
83
+ // 生命周期
84
+ destroy(): void
85
+
86
+ // 工具方法
87
+ isSuccessResponse(response: any): boolean
88
+ validatePublicKey(publicKey: string): boolean
89
+ handleError(errorType: string, message: string, originalError?: Error): void
90
+
91
+ // 常量
92
+ static readonly CONSTANTS: Record<string, number>
93
+ static readonly ERROR_TYPES: Record<string, string>
23
94
  }
24
95
 
96
+ // 工厂函数
25
97
  export function createPasswordValidator(options?: PasswordValidatorOptions): PasswordValidator
26
- export function validatePassword(password: string, options?: PasswordValidatorOptions, additionalData?: any): Promise<ValidationResult>
98
+
99
+ // 便捷函数
100
+ export function validatePassword(
101
+ password: string,
102
+ userName: string,
103
+ options?: PasswordValidatorOptions,
104
+ additionalData?: Record<string, any>
105
+ ): Promise<ValidationResult>
27
106
  }
@@ -5,22 +5,40 @@ import JSEncrypt from 'jsencrypt';
5
5
  * 提供密码加密和校验功能
6
6
  */
7
7
  class PasswordValidator {
8
+ // 优化:添加常量定义
9
+ static CONSTANTS = {
10
+ DEFAULT_TIMEOUT: 10000,
11
+ CACHE_DURATION: 5 * 60 * 1000, // 5分钟缓存
12
+ MAX_RETRY_ATTEMPTS: 3,
13
+ MIN_PASSWORD_LENGTH: 1
14
+ }
15
+
16
+ // 优化:添加错误类型枚举
17
+ static ERROR_TYPES = {
18
+ NETWORK_ERROR: 'NETWORK_ERROR',
19
+ TIMEOUT_ERROR: 'TIMEOUT_ERROR',
20
+ ENCRYPTION_ERROR: 'ENCRYPTION_ERROR',
21
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
22
+ PUBLIC_KEY_ERROR: 'PUBLIC_KEY_ERROR'
23
+ }
24
+
8
25
  constructor(options = {}) {
26
+ // 优化:合并配置,使用常量
9
27
  this.options = {
10
- // 获取公钥的接口地址
11
28
  publicKeyUrl: options.publicKeyUrl || '/microservice/strongPassword/getPublicKey',
12
- // 密码校验接口地址
13
29
  validateUrl: options.validateUrl || '/microservice/strongPassword/checkPassword',
14
- // 请求超时时间
15
- timeout: options.timeout || 10000,
16
- // 自定义请求头
30
+ timeout: options.timeout || PasswordValidator.CONSTANTS.DEFAULT_TIMEOUT,
17
31
  headers: options.headers || {},
32
+ retryAttempts: options.retryAttempts || PasswordValidator.CONSTANTS.MAX_RETRY_ATTEMPTS,
33
+ cacheDuration: options.cacheDuration || PasswordValidator.CONSTANTS.CACHE_DURATION,
18
34
  ...options
19
35
  };
20
36
 
21
37
  // 缓存公钥,避免重复请求
22
38
  this.publicKeyCache = null;
23
39
  this.publicKeyExpiry = null;
40
+ this.retryCount = 0;
41
+ this.abortController = null;
24
42
  }
25
43
 
26
44
  /**
@@ -28,31 +46,94 @@ class PasswordValidator {
28
46
  * @returns {Promise<string>} 公钥字符串
29
47
  */
30
48
  async getPublicKey() {
31
- // 检查缓存是否有效(5分钟有效期)
49
+ // 检查缓存是否有效
32
50
  if (this.publicKeyCache && this.publicKeyExpiry && Date.now() < this.publicKeyExpiry) {
33
51
  return this.publicKeyCache
34
52
  }
35
53
 
36
54
  try {
55
+ // 取消之前的请求
56
+ if (this.abortController) {
57
+ this.abortController.abort();
58
+ }
59
+ this.abortController = new AbortController();
60
+
37
61
  const response = await this.makeRequest(this.options.publicKeyUrl, {
38
- method: 'GET'
62
+ method: 'GET',
63
+ signal: this.abortController.signal
39
64
  });
40
65
 
41
- if (response.code !== '0' ) {
42
- throw new Error('获取公钥失败:' + (response.message || '未知错误'))
66
+ // 优化:更灵活的成功判断
67
+ if (!this.isSuccessResponse(response)) {
68
+ throw new Error('获取公钥失败:' + (response.message || response.msg || '未知错误'))
69
+ }
70
+
71
+ // 验证公钥格式
72
+ const publicKey = response.data || response.result;
73
+ if (!this.validatePublicKey(publicKey)) {
74
+ throw new Error('公钥格式无效')
43
75
  }
44
76
 
45
- // 缓存公钥,设置5分钟过期时间
46
- this.publicKeyCache = response.data;
47
- this.publicKeyExpiry = Date.now() + 5 * 60 * 1000;
77
+ // 缓存公钥
78
+ this.publicKeyCache = publicKey;
79
+ this.publicKeyExpiry = Date.now() + this.options.cacheDuration;
48
80
 
49
81
  return this.publicKeyCache
50
82
  } catch (error) {
51
83
  console.error('获取公钥失败:', error);
84
+ this.handleError(PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR, error.message, error);
52
85
  throw new Error('获取公钥失败: ' + error.message)
53
86
  }
54
87
  }
55
88
 
89
+ // 优化:添加响应成功判断方法
90
+ isSuccessResponse(response) {
91
+ if (!response || typeof response !== 'object') return false
92
+
93
+ const successIndicators = [
94
+ response.code === '0',
95
+ response.code === 0,
96
+ response.success === true,
97
+ response.status === 'success',
98
+ response.result === true
99
+ ];
100
+
101
+ return successIndicators.some(indicator => indicator === true)
102
+ }
103
+
104
+ // 优化:添加公钥验证方法
105
+ validatePublicKey(publicKey) {
106
+ if (!publicKey || typeof publicKey !== 'string') return false
107
+
108
+ // 基本的RSA公钥格式检查
109
+ const rsaPattern = /^-----BEGIN PUBLIC KEY-----[\s\S]*-----END PUBLIC KEY-----$/;
110
+ return rsaPattern.test(publicKey.trim())
111
+ }
112
+
113
+ // 优化:添加统一错误处理方法
114
+ handleError(errorType, message, originalError = null) {
115
+ const errorMessages = {
116
+ [PasswordValidator.ERROR_TYPES.NETWORK_ERROR]: '网络连接失败,请检查网络设置',
117
+ [PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR]: '请求超时,请重试',
118
+ [PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR]: '密码加密失败',
119
+ [PasswordValidator.ERROR_TYPES.VALIDATION_ERROR]: '密码验证失败',
120
+ [PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR]: '获取公钥失败'
121
+ };
122
+
123
+ const errorMessage = errorMessages[errorType] || message || '未知错误';
124
+
125
+ // 调用用户自定义错误处理
126
+ if (this.options.onError) {
127
+ this.options.onError({
128
+ type: errorType,
129
+ message: errorMessage,
130
+ originalError
131
+ });
132
+ }
133
+
134
+ console.error(`密码校验器错误 [${errorType}]:`, errorMessage, originalError);
135
+ }
136
+
56
137
  /**
57
138
  * 使用RSA公钥加密密码
58
139
  * @param {string} password 原始密码
@@ -61,9 +142,22 @@ class PasswordValidator {
61
142
  */
62
143
  encryptPassword(password, publicKey) {
63
144
  try {
145
+ // 优化:输入验证
146
+ if (!password || typeof password !== 'string') {
147
+ throw new Error('密码不能为空')
148
+ }
149
+
150
+ if (password.length < PasswordValidator.CONSTANTS.MIN_PASSWORD_LENGTH) {
151
+ throw new Error('密码长度不足')
152
+ }
153
+
154
+ if (!publicKey || typeof publicKey !== 'string') {
155
+ throw new Error('公钥不能为空')
156
+ }
157
+
64
158
  // 使用导入的JSEncrypt进行RSA加密
65
- if (!JSEncrypt) {
66
- throw new Error('JSEncrypt库未正确加载')
159
+ if (typeof JSEncrypt === 'undefined') {
160
+ throw new Error('JSEncrypt库未正确加载,请确保已引入JSEncrypt库')
67
161
  }
68
162
 
69
163
  const encrypt = new JSEncrypt();
@@ -71,12 +165,13 @@ class PasswordValidator {
71
165
  const encrypted = encrypt.encrypt(password);
72
166
 
73
167
  if (!encrypted) {
74
- throw new Error('密码加密失败')
168
+ throw new Error('密码加密失败,可能是公钥格式不正确')
75
169
  }
76
170
 
77
171
  return encrypted
78
172
  } catch (error) {
79
173
  console.error('密码加密失败:', error);
174
+ this.handleError(PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR, error.message, error);
80
175
  throw new Error('密码加密失败: ' + error.message)
81
176
  }
82
177
  }
@@ -84,12 +179,24 @@ class PasswordValidator {
84
179
  /**
85
180
  * 校验密码
86
181
  * @param {string} password 原始密码
87
- * @param {string} userName 用户
182
+ * @param {string} userName 用户名
88
183
  * @param {Object} additionalData 额外的校验数据
89
184
  * @returns {Promise<Object>} 校验结果
90
185
  */
91
186
  async validatePassword(password, userName, additionalData = {}) {
92
187
  try {
188
+ // 优化:输入验证
189
+ if (!password || typeof password !== 'string') {
190
+ throw new Error('密码不能为空')
191
+ }
192
+
193
+ if (!userName || typeof userName !== 'string') {
194
+ throw new Error('用户名不能为空')
195
+ }
196
+
197
+ // 重置重试计数
198
+ this.retryCount = 0;
199
+
93
200
  // 1. 获取公钥
94
201
  const publicKey = await this.getPublicKey();
95
202
 
@@ -107,15 +214,34 @@ class PasswordValidator {
107
214
  })
108
215
  });
109
216
 
217
+ // 优化:统一返回格式
110
218
  return {
111
- ...response
219
+ success: this.isSuccessResponse(response),
220
+ data: response.data || response.result,
221
+ message: response.message || response.msg || '验证完成',
222
+ code: response.code || response.status,
223
+ originalResponse: response
112
224
  }
113
225
  } catch (error) {
114
226
  console.error('密码校验失败:', error);
227
+
228
+ // 优化:根据错误类型返回不同的错误信息
229
+ let errorType = PasswordValidator.ERROR_TYPES.VALIDATION_ERROR;
230
+ if (error.message.includes('网络') || error.message.includes('fetch')) {
231
+ errorType = PasswordValidator.ERROR_TYPES.NETWORK_ERROR;
232
+ } else if (error.message.includes('超时') || error.message.includes('timeout')) {
233
+ errorType = PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR;
234
+ } else if (error.message.includes('加密')) {
235
+ errorType = PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR;
236
+ }
237
+
238
+ this.handleError(errorType, error.message, error);
239
+
115
240
  return {
116
241
  success: false,
117
- message: error.msg,
118
- code: 'VALIDATION_ERROR'
242
+ message: error.message || '密码校验失败',
243
+ code: errorType,
244
+ error: error
119
245
  }
120
246
  }
121
247
  }
@@ -127,8 +253,15 @@ class PasswordValidator {
127
253
  * @returns {Promise<Object>} 响应结果
128
254
  */
129
255
  async makeRequest(url, options = {}) {
130
- const controller = new AbortController();
131
- const timeoutId = setTimeout(() => controller.abort(), this.options.timeout);
256
+ // 优化:使用实例的abortController
257
+ if (this.abortController) {
258
+ this.abortController.abort();
259
+ }
260
+ this.abortController = new AbortController();
261
+
262
+ const timeoutId = setTimeout(() => {
263
+ this.abortController.abort();
264
+ }, this.options.timeout);
132
265
 
133
266
  try {
134
267
  const response = await fetch(url, {
@@ -138,7 +271,7 @@ class PasswordValidator {
138
271
  ...this.options.headers,
139
272
  ...options.headers
140
273
  },
141
- signal: controller.signal
274
+ signal: this.abortController.signal
142
275
  });
143
276
 
144
277
  clearTimeout(timeoutId);
@@ -147,14 +280,27 @@ class PasswordValidator {
147
280
  throw new Error(`HTTP错误: ${response.status} ${response.statusText}`)
148
281
  }
149
282
 
150
- return await response.json()
283
+ const data = await response.json();
284
+
285
+ // 优化:记录请求日志(开发环境)
286
+ if (this.options.debug) {
287
+ console.log('密码校验器请求:', { url, options, response: data });
288
+ }
289
+
290
+ return data
151
291
  } catch (error) {
152
292
  clearTimeout(timeoutId);
153
293
 
154
294
  if (error.name === 'AbortError') {
295
+ this.handleError(PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR, '请求超时', error);
155
296
  throw new Error('请求超时')
156
297
  }
157
298
 
299
+ if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
300
+ this.handleError(PasswordValidator.ERROR_TYPES.NETWORK_ERROR, '网络连接失败', error);
301
+ throw new Error('网络连接失败')
302
+ }
303
+
158
304
  throw error
159
305
  }
160
306
  }
@@ -176,6 +322,60 @@ class PasswordValidator {
176
322
  // 清除缓存,使用新配置重新获取
177
323
  this.clearCache();
178
324
  }
325
+
326
+ /**
327
+ * 取消当前请求
328
+ */
329
+ cancelRequest() {
330
+ if (this.abortController) {
331
+ this.abortController.abort();
332
+ this.abortController = null;
333
+ }
334
+ }
335
+
336
+ /**
337
+ * 检查是否正在请求中
338
+ * @returns {boolean} 是否正在请求
339
+ */
340
+ isRequesting() {
341
+ return this.abortController && !this.abortController.signal.aborted
342
+ }
343
+
344
+ /**
345
+ * 获取缓存状态
346
+ * @returns {Object} 缓存状态信息
347
+ */
348
+ getCacheStatus() {
349
+ return {
350
+ hasCache: !!this.publicKeyCache,
351
+ isExpired: this.publicKeyExpiry ? Date.now() > this.publicKeyExpiry : true,
352
+ expiryTime: this.publicKeyExpiry,
353
+ remainingTime: this.publicKeyExpiry ? Math.max(0, this.publicKeyExpiry - Date.now()) : 0
354
+ }
355
+ }
356
+
357
+ /**
358
+ * 销毁实例,清理资源
359
+ */
360
+ destroy() {
361
+ try {
362
+ // 取消所有进行中的请求
363
+ this.cancelRequest();
364
+
365
+ // 清除缓存
366
+ this.clearCache();
367
+
368
+ // 重置状态
369
+ this.retryCount = 0;
370
+
371
+ // 调用销毁回调
372
+ if (this.options.onDestroy) {
373
+ this.options.onDestroy();
374
+ }
375
+ } catch (error) {
376
+ console.error('销毁密码校验器时出错:', error);
377
+ }
378
+ }
179
379
  }
180
380
 
181
381
  /**
@@ -1 +1 @@
1
- !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("jsencrypt")):"function"==typeof define&&define.amd?define(["exports","jsencrypt"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).PasswordValidator={},e.JSEncrypt)}(this,function(e,t){"use strict";class PasswordValidator{constructor(e={}){this.options={publicKeyUrl:e.publicKeyUrl||"/microservice/strongPassword/getPublicKey",validateUrl:e.validateUrl||"/microservice/strongPassword/checkPassword",timeout:e.timeout||1e4,headers:e.headers||{},...e},this.publicKeyCache=null,this.publicKeyExpiry=null}async getPublicKey(){if(this.publicKeyCache&&this.publicKeyExpiry&&Date.now()<this.publicKeyExpiry)return this.publicKeyCache;try{const e=await this.makeRequest(this.options.publicKeyUrl,{method:"GET"});if("0"!==e.code)throw Error("获取公钥失败:"+(e.message||"未知错误"));return this.publicKeyCache=e.data,this.publicKeyExpiry=Date.now()+3e5,this.publicKeyCache}catch(e){throw Error("获取公钥失败: "+e.message)}}encryptPassword(e,r){try{if(!t)throw Error("JSEncrypt库未正确加载");const s=new t;s.setPublicKey(r);const i=s.encrypt(e);if(!i)throw Error("密码加密失败");return i}catch(e){throw Error("密码加密失败: "+e.message)}}async validatePassword(e,t,r={}){try{const s=await this.getPublicKey(),i=this.encryptPassword(e,s);return{...await this.makeRequest(this.options.validateUrl,{method:"POST",body:JSON.stringify({userName:t,password:i,timestamp:Date.now(),...r})})}}catch(e){return{success:!1,message:e.msg,code:"VALIDATION_ERROR"}}}async makeRequest(e,t={}){const r=new AbortController,s=setTimeout(()=>r.abort(),this.options.timeout);try{const i=await fetch(e,{...t,headers:{"Content-Type":"application/json",...this.options.headers,...t.headers},signal:r.signal});if(clearTimeout(s),!i.ok)throw Error(`HTTP错误: ${i.status} ${i.statusText}`);return await i.json()}catch(e){if(clearTimeout(s),"AbortError"===e.name)throw Error("请求超时");throw e}}clearCache(){this.publicKeyCache=null,this.publicKeyExpiry=null}updateOptions(e){this.options={...this.options,...e},this.clearCache()}}e.createPasswordValidator=function(e){return new PasswordValidator(e)},e.default=PasswordValidator,e.validatePassword=async function(e,t={},r={}){const s=new PasswordValidator(t),i=r.userName||"";return await s.validatePassword(e,i,r)},Object.defineProperty(e,"__esModule",{value:!0})});
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jsencrypt")):"function"==typeof define&&define.amd?define(["exports","jsencrypt"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).PasswordValidator={},t.JSEncrypt)}(this,function(t,JSEncrypt){"use strict";class PasswordValidator{static CONSTANTS={DEFAULT_TIMEOUT:1e4,CACHE_DURATION:3e5,MAX_RETRY_ATTEMPTS:3,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||"/microservice/strongPassword/getPublicKey",validateUrl:t.validateUrl||"/microservice/strongPassword/checkPassword",timeout:t.timeout||PasswordValidator.CONSTANTS.DEFAULT_TIMEOUT,headers:t.headers||{},retryAttempts:t.retryAttempts||PasswordValidator.CONSTANTS.MAX_RETRY_ATTEMPTS,cacheDuration:t.cacheDuration||PasswordValidator.CONSTANTS.CACHE_DURATION,...t},this.publicKeyCache=null,this.publicKeyExpiry=null,this.retryCount=0,this.abortController=null}async getPublicKey(){if(this.publicKeyCache&&this.publicKeyExpiry&&Date.now()<this.publicKeyExpiry)return this.publicKeyCache;try{this.abortController&&this.abortController.abort(),this.abortController=new AbortController;const t=await this.makeRequest(this.options.publicKeyUrl,{method:"GET",signal:this.abortController.signal});if(!this.isSuccessResponse(t))throw Error("获取公钥失败:"+(t.message||t.msg||"未知错误"));const e=t.data||t.result;if(!this.validatePublicKey(e))throw Error("公钥格式无效");return this.publicKeyCache=e,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)}validatePublicKey(t){return!(!t||"string"!=typeof t)&&/^-----BEGIN PUBLIC KEY-----[\s\S]*-----END PUBLIC KEY-----$/.test(t.trim())}handleError(t,e,r=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]||e||"未知错误";this.options.onError&&this.options.onError({type:t,message:s,originalError:r})}encryptPassword(t,e){try{if(!t||"string"!=typeof t)throw Error("密码不能为空");if(t.length<PasswordValidator.CONSTANTS.MIN_PASSWORD_LENGTH)throw Error("密码长度不足");if(!e||"string"!=typeof e)throw Error("公钥不能为空");if(void 0===JSEncrypt)throw Error("JSEncrypt库未正确加载,请确保已引入JSEncrypt库");const r=new JSEncrypt;r.setPublicKey(e);const s=r.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,e,r={}){try{if(!t||"string"!=typeof t)throw Error("密码不能为空");if(!e||"string"!=typeof e)throw Error("用户名不能为空");this.retryCount=0;const s=await this.getPublicKey(),o=this.encryptPassword(t,s),i=await this.makeRequest(this.options.validateUrl,{method:"POST",body:JSON.stringify({userName:e,password:o,timestamp:Date.now(),...r})});return{success:this.isSuccessResponse(i),data:i.data||i.result,message:i.message||i.msg||"验证完成",code:i.code||i.status,originalResponse:i}}catch(t){let e=PasswordValidator.ERROR_TYPES.VALIDATION_ERROR;return t.message.includes("网络")||t.message.includes("fetch")?e=PasswordValidator.ERROR_TYPES.NETWORK_ERROR:t.message.includes("超时")||t.message.includes("timeout")?e=PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR:t.message.includes("加密")&&(e=PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR),this.handleError(e,t.message,t),{success:!1,message:t.message||"密码校验失败",code:e,error:t}}}async makeRequest(t,e={}){this.abortController&&this.abortController.abort(),this.abortController=new AbortController;const r=setTimeout(()=>{this.abortController.abort()},this.options.timeout);try{const s=await fetch(t,{...e,headers:{"Content-Type":"application/json",...this.options.headers,...e.headers},signal:this.abortController.signal});if(clearTimeout(r),!s.ok)throw Error(`HTTP错误: ${s.status} ${s.statusText}`);const o=await s.json();return this.options.debug,o}catch(t){if(clearTimeout(r),"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()}cancelRequest(){this.abortController&&(this.abortController.abort(),this.abortController=null)}isRequesting(){return this.abortController&&!this.abortController.signal.aborted}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.cancelRequest(),this.clearCache(),this.retryCount=0,this.options.onDestroy&&this.options.onDestroy()}catch(t){}}}t.createPasswordValidator=function(t){return new PasswordValidator(t)},t.default=PasswordValidator,t.validatePassword=async function(t,e={},r={}){const s=new PasswordValidator(e),o=r.userName||"";return await s.validatePassword(t,o,r)},Object.defineProperty(t,"__esModule",{value:!0})});