slider-captcha-sdk 1.0.26 → 1.0.28

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,JSEncrypt){"use strict";class PopupSliderCaptcha{static DEFAULTS={width:400,height:240,sliderSize:40,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);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)}}"}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),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.isInitialized=!1,this.throttledHandleMove=this.throttle(t=>this.handleMove(t),this.options.throttleDelay),this.lazyInit()}lazyInit(){this.injectStyles(),this.isInitialized=!0}createInitialState(){return{isVisible:!1,isDragging:!1,currentX:0,startX:0,retryCount:0,isLoading:!1}}normalizeUrl(t,e){if(!t||!e||/^https?:\/\//.test(t))return t;return(e.endsWith("/")?e:`${e}/`)+(t.startsWith("/")?t.slice(1):t)}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{options:t}=this,e=t.width,s=t.height,i=`\n <div class="${PopupSliderCaptcha.CSS_CLASSES.overlay}">\n <div class="${PopupSliderCaptcha.CSS_CLASSES.modal}">\n <div class="${PopupSliderCaptcha.CSS_CLASSES.header}">\n <h3 class="slider-captcha-title">安全验证</h3>\n <div class="slider-captcha-header-buttons">\n <button class="slider-captcha-refresh" title="刷新">\n <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>\n </button>\n <button class="slider-captcha-close" title="关闭"></button>\n </div>\n </div>\n <div class="${PopupSliderCaptcha.CSS_CLASSES.container}" style="width:${e}px;height:${s}px;display:none;">\n <img class="slider-captcha-bg" alt="背景图">\n <img class="slider-captcha-piece" alt="滑块">\n <div class="${PopupSliderCaptcha.CSS_CLASSES.loading}">加载中...</div>\n <div class="slider-captcha-floating-time"></div>\n </div>\n <div class="${PopupSliderCaptcha.CSS_CLASSES.track}" style="display:none;">\n <div class="slider-captcha-finger">👉</div>\n <div class="${PopupSliderCaptcha.CSS_CLASSES.btn}">\n <div class="slider-captcha-icon">→</div>\n </div>\n <div class="${PopupSliderCaptcha.CSS_CLASSES.hint}">向右滑动完成验证</div>\n </div>\n <div class="${PopupSliderCaptcha.CSS_CLASSES.error}"></div>\n </div>\n </div>\n `,a=document.createElement("div");a.innerHTML=i.trim();const r=a.firstElementChild;document.body.appendChild(r),this.elements={overlay:r,modal:r.querySelector(`.${PopupSliderCaptcha.CSS_CLASSES.modal}`),header:r.querySelector(`.${PopupSliderCaptcha.CSS_CLASSES.header}`),title:r.querySelector(".slider-captcha-title"),closeBtn:r.querySelector(".slider-captcha-close"),refreshBtn:r.querySelector(".slider-captcha-refresh"),container:r.querySelector(`.${PopupSliderCaptcha.CSS_CLASSES.container}`),backgroundImg:r.querySelector(".slider-captcha-bg"),sliderImg:r.querySelector(".slider-captcha-piece"),loadingText:r.querySelector(`.${PopupSliderCaptcha.CSS_CLASSES.loading}`),floatingTime:r.querySelector(".slider-captcha-floating-time"),track:r.querySelector(`.${PopupSliderCaptcha.CSS_CLASSES.track}`),fingerAnimation:r.querySelector(".slider-captcha-finger"),btn:r.querySelector(`.${PopupSliderCaptcha.CSS_CLASSES.btn}`),icon:r.querySelector(".slider-captcha-icon"),hint:r.querySelector(`.${PopupSliderCaptcha.CSS_CLASSES.hint}`),error:r.querySelector(`.${PopupSliderCaptcha.CSS_CLASSES.error}`)}}bindEvents(){const{elements:t}=this;[[t.closeBtn,"click",()=>this.hide()],[t.refreshBtn,"click",()=>this.refresh()],[t.overlay,"click",e=>{e.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,e,s])=>{this.addEventListener(t,e,s)}),this.bindSliderEvents()}bindSliderEvents(){const{elements:t}=this,e={start:t=>this.handleStart(t)};[[t.btn,"mousedown",e.start],[t.btn,"touchstart",e.start],[t.sliderImg,"mousedown",e.start],[t.sliderImg,"touchstart",e.start]].forEach(([t,e,s,i])=>{this.addEventListener(t,e,s,i)})}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}))}removeEventListener(t,e,s,i={}){t&&"function"==typeof s&&(t.removeEventListener(e,s,i),this.eventListeners=this.eventListeners.filter(i=>!(i.element===t&&i.event===e&&i.handler===s)))}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{track:t,btn:e}=this.elements;this.cachedDimensions={trackWidth:t.offsetWidth,btnWidth:e.offsetWidth,get maxX(){return this.trackWidth-this.btnWidth}}}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(),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,this.boundHandleEnd=this.boundHandleEnd||this.handleEnd.bind(this),document.addEventListener("mousemove",this.throttledHandleMove,{passive:!1}),document.addEventListener("touchmove",this.throttledHandleMove,{passive:!1}),document.addEventListener("mouseup",this.boundHandleEnd),document.addEventListener("touchend",this.boundHandleEnd))}handleMove(t){this.state.isDragging&&(t.preventDefault(),this.lastClientX=this.getClientX(t),this.rafId||(this.rafId=requestAnimationFrame(()=>{const t=this.lastClientX-this.state.startX,{maxX:e}=this.getDimensions();this.state.currentX=Math.max(0,Math.min(t,e)),this.times.push({time:Date.now(),position:this.getPosition()}),this.updateSliderPosition(),this.rafId=null})))}handleEnd(){this.state.isDragging&&(document.removeEventListener("mousemove",this.throttledHandleMove),document.removeEventListener("touchmove",this.throttledHandleMove),document.removeEventListener("mouseup",this.boundHandleEnd),document.removeEventListener("touchend",this.boundHandleEnd),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 e=t?"all 0.3s ease":"none";requestAnimationFrame(()=>{const{btn:t,sliderImg:s}=this.elements;t.style.transition=s.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,{color:"white"}),e.icon.textContent="✓"},fail:()=>{Object.assign(e.btn.style,{background:"var(--sc-danger)"}),Object.assign(e.icon.style,{color:"white"}),e.icon.textContent="✗"},reset:()=>{Object.assign(e.btn.style,{background:"white"}),Object.assign(e.icon.style,{color:"#666"}),e.icon.textContent="→",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=`translate3d(${s.currentX}px, 0, 0)`,t.sliderImg.style.transform=`translate3d(${a}px, 0, 0)`,t.fingerAnimation.style.opacity=r>=.8?"0":"0.6"})}show(){this.state.isVisible=!0,this.elements.overlay||(this.createElements(),this.bindEvents()),this.elements.overlay.style.display="flex",this.elements.overlay.style.transform="translate3d(0,0,0)",requestAnimationFrame(()=>{this.elements.overlay.classList.add("show"),this.elements.modal.classList.add("show"),this.preloadCaptcha()})}async preloadCaptcha(){try{this.showLoading();const[t]=await Promise.all([this.fetchCaptchaData(),this.preloadImages()]);this.captchaData=t,this.showCaptcha(),await this.renderCaptcha()}catch(t){this.handleError(PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR,t.message,t)}}async makeRequest(t,e={}){this.abortController&&this.abortController.abort(),this.abortController=new AbortController;const s=await fetch(t,{method:"POST",headers:{"Content-Type":"application/json",...this.options.headers},body:JSON.stringify({canvasWidth:this.options.width,canvasHeight:this.options.height,timestamp:Date.now(),...this.options.requestData,...e}),signal:this.abortController.signal});if(!s.ok)throw new Error(`HTTP ${s.status}: ${s.statusText}`);return s.json()}async fetchCaptchaData(){this.startTime=Date.now();const t=await this.makeRequest(this.options.apiUrl);if(!this.validateCaptchaData(t))throw new Error("验证码数据格式错误");return t.data}preloadImages(){return Promise.all(["data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE3LjY1LDYuMzVDMTYuMiw0LjkgMTQuMjEsNCAxMiw0QTgsOCAwIDAsMCA0LDEyQTgsOCAwIDAsMCAxMiwyMEMxNS43MywyMCAxOC44NCwxNy40NSAxOS43MywxNEgxNy42NUMxNi44MywxNi4zMyAxNC42MSwxOCAxMiwxOEE2LDYgMCAwLDEgNiwxMkE2LDYgMCAwLDEgMTIsNkMxMy42Niw2IDE1LjE0LDYuNjkgMTYuMjIsNy43OEwxMywxMUgyMFY0TDE3LjY1LDYuMzVaIiBmaWxsPSIjOTk5Ii8+Cjwvc3ZnPgo="].map(t=>new Promise(e=>{const s=new Image;s.onload=s.onerror=()=>e(),s.src=t})))}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)}handleRequestError(t){"AbortError"===t.name?this.handleError(PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR,"请求被取消",t):t.message?.includes("Failed to fetch")||t.message?.includes("NetworkError")?this.handleError(PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR,"网络连接失败",t):this.handleError(PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR,t.message,t)}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();const t=await this.makeRequest(this.options.apiUrl);if(!this.validateCaptchaData(t))throw new Error("验证码数据格式错误");this.captchaData=t.data,this.showCaptcha(),await this.renderCaptcha()}catch(t){this.handleRequestError(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})}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))})})}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(new Error("图片加载超时"))},5e3),n=new Image;n.crossOrigin="anonymous",n.decoding="async",n.onload=function(){if(this.safeClearTimeout(r),"undefined"!=typeof OffscreenCanvas)try{const t=new OffscreenCanvas(n.width,n.height).getContext("2d");t.drawImage(n,0,0);const s=t.getImageData(0,0,n.width,n.height);this.imageCache.set(e,{src:n.src,imageData:s})}catch(t){this.imageCache.set(e,n.cloneNode())}else this.imageCache.set(e,n.cloneNode());t.src=n.src,this.applyStyles(t,s),i()}.bind(this),n.onerror=function(t){this.safeClearTimeout(r),a(t)}.bind(this),n.src=e})}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{const t=await this.makeRequest(this.options.verifyUrl,{loginVo:{nonceStr:this.captchaData.nonceStr,value:this.getPosition()},dragEventList:[...this.times],...this.options.verifyData});this.isVerifySuccess(t)?this.onVerifySuccess(t.data||t.result):this.onVerifyFail(t.message||t.msg||"验证失败,请重试!")}catch(t){this.handleRequestError(t)}else this.onVerifyFail("验证码数据丢失")}isVerifySuccess(t){if(!t||"object"!=typeof t)return!1;return["0"===t.code,0===t.code,!0===t.success,"success"===t.status,!0===t.result].some(t=>!0===t)}onVerifySuccess(t){const e=this.dragStartTime?Date.now()-this.dragStartTime: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.refresh():this.reset()},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="translate3d(0px, 0, 0)",this.elements.sliderImg.style.transform="translate3d(0px, 0, 0)",this.updateUIState("reset"),this.elements.error.style.display="none"})}refresh(){try{this.reset(),this.state.retryCount=0,this.loadCaptcha()}catch(t){}}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(...i)=>{const a=Date.now();if(a-s>=e)return s=a,t.apply(this,i)}}destroy(){try{this.state.isDragging&&(document.removeEventListener("mousemove",this.throttledHandleMove),document.removeEventListener("touchmove",this.throttledHandleMove),this.boundHandleEnd&&(document.removeEventListener("mouseup",this.boundHandleEnd),document.removeEventListener("touchend",this.boundHandleEnd)),this.state.isDragging=!1),this.abortController&&(this.abortController.abort(),this.abortController=null),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,this.elements={},this.state={},this.times=[],this.eventListeners=[],this.timers.clear(),this.throttledHandleMove=null,this.boundHandleEnd=null,this.options.onDestroy&&this.options.onDestroy(),this.options=null}catch(t){}}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);var 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,this.encryptor=null}normalizeUrl(t,e){if(!t)return t;if(/^https?:\/\//.test(t)||!e)return t;return(e.endsWith("/")?e:`${e}/`)+(t.startsWith("/")?t.substring(1):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 new Error(`获取公钥失败:${t.message||t.msg||"未知错误"}`);const e=t.data||t.result;if(!e)throw new 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),new Error(`获取公钥失败: ${t.message}`)}}isSuccessResponse(t){if(!t||"object"!=typeof t)return!1;return["0"===t.code,0===t.code,!0===t.success,"success"===t.status,!0===t.result].some(t=>!0===t)}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)throw new Error("密码不能为空");if(t.length<PasswordValidator.CONSTANTS.MIN_PASSWORD_LENGTH)throw new Error("密码长度不足");if(!e)throw new Error("公钥不能为空");if(void 0===JSEncrypt)throw new Error("JSEncrypt库未正确加载,请确保已引入JSEncrypt库");this.encryptor||(this.encryptor=new JSEncrypt),this.lastPublicKey!==e&&(this.encryptor.setPublicKey(e),this.lastPublicKey=e);const s=this.encryptor.encrypt(t);if(!s)throw new Error("密码加密失败,可能是公钥格式不正确");return s}catch(t){throw this.handleError(PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR,t.message,t),new Error(`密码加密失败: ${t.message}`)}}async validatePassword(t,e,s={}){try{if(!t)throw new Error("密码不能为空");if(!e)throw new Error("用户名不能为空");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={}){const s=new AbortController,i=setTimeout(()=>{s.abort()},this.options.timeout);try{const a=await fetch(t,{...e,headers:{"Content-Type":"application/json",...this.options.headers,...e.headers},signal:s.signal});if(clearTimeout(i),!a.ok)throw new Error(`HTTP错误: ${a.status} ${a.statusText}`);const r=await a.json();return this.options.debug,r}catch(t){if(clearTimeout(i),"AbortError"===t.name)throw this.handleError(PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR,"请求超时",t),new Error("请求超时");if(t.message.includes("Failed to fetch")||t.message.includes("NetworkError"))throw this.handleError(PasswordValidator.ERROR_TYPES.NETWORK_ERROR,"网络连接失败",t),new Error("网络连接失败");throw t}}clearCache(){this.publicKeyCache=null,this.publicKeyExpiry=null}updateOptions(t){this.options={...this.options,...t},this.clearCache()}getCacheStatus(){return{hasCache:Boolean(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.encryptor=null,this.lastPublicKey=null,this.options.onDestroy&&this.options.onDestroy(),this.options=null}catch(t){}}}var s=PasswordValidator;function i(t){return new PasswordValidator(t)}function a(t,e,s={},i={}){return new PasswordValidator(s).validatePassword(t,e,i)}var r={PopupSliderCaptcha:e,PasswordValidator:s,createPasswordValidator:i,validatePassword:a};"undefined"!=typeof window&&(window.SliderCaptcha=e,window.PopupSliderCaptcha=e,window.PasswordValidator=s,window.createPasswordValidator=i,window.validatePassword=a),t.PasswordValidator=s,t.PopupSliderCaptcha=e,t.createPasswordValidator=i,t.default=r,t.validatePassword=a,Object.defineProperty(t,"__esModule",{value:!0})});
1
+ !function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports,require("jsencrypt")):"function"==typeof define&&define.amd?define(["exports","jsencrypt"],i):i((t="undefined"!=typeof globalThis?globalThis:t||self).SliderCaptchaSDK={},t.JSEncrypt)}(this,function(t,JSEncrypt){"use strict";function i(t){return new PasswordValidator(t)}function s(t,i,s={},e={}){return new PasswordValidator(s).validatePassword(t,i,e)}var e,a,r;class PopupSliderCaptcha{static DEFAULTS={width:320,height:240,sliderSize:40,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:#666;--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;}.slider-captcha-container{display:flex;align-items:center;position:relative;border-radius:4px;overflow:hidden;margin-bottom:12px;background:#837a7a;justify-content:center}.slider-captcha-track{width:100%;height:48px;min-height:48px;max-height:48px;line-height:48px;background:#e8f5e9;border: 1px solid rgba(0,200,112,0.15);border-radius:40px;position:relative;margin-bottom:15px;overflow:hidden;box-sizing:border-box;flex-shrink:0}.slider-captcha-btn{width:40px;height:40px;background: #00C870;border-radius:50%;position:absolute;top:4px;left:4px;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:none}.slider-captcha-piece{position:absolute;top:0;left:0;cursor:pointer;transition:none;z-index:2;will-change:transform;display:none}.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;white-space:nowrap}.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:opacity var(--sc-transition);white-space:nowrap}.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)}}"}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),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.isInitialized=!1,this.throttledHandleMove=this.throttle(t=>this.handleMove(t),this.options.throttleDelay),this.lazyInit()}lazyInit(){this.injectStyles(),this.isInitialized=!0}createInitialState(){return{isVisible:!1,isDragging:!1,currentX:0,startX:0,retryCount:0,isLoading:!1}}normalizeUrl(t,i){return t&&i&&!/^https?:\/\//.test(t)?(i.endsWith("/")?i:i+"/")+(t.startsWith("/")?t.slice(1):t):t}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{options:t}=this,i=t.width,s=t.height,e=`\n <div class="${PopupSliderCaptcha.CSS_CLASSES.overlay}">\n <div class="${PopupSliderCaptcha.CSS_CLASSES.modal}">\n <div class="${PopupSliderCaptcha.CSS_CLASSES.header}">\n <h3 class="slider-captcha-title">安全验证</h3>\n <div class="slider-captcha-header-buttons">\n <button class="slider-captcha-refresh" title="刷新">\n <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>\n </button>\n <button class="slider-captcha-close" title="关闭"></button>\n </div>\n </div>\n <div class="${PopupSliderCaptcha.CSS_CLASSES.container}" style="width:${i}px;height:${s}px;display:none;">\n <img class="slider-captcha-bg" alt="" style="display:none;">\n <img class="slider-captcha-piece" alt="" style="display:none;">\n <div class="${PopupSliderCaptcha.CSS_CLASSES.loading}">加载中...</div>\n <div class="slider-captcha-floating-time"></div>\n </div>\n <div class="${PopupSliderCaptcha.CSS_CLASSES.track}">\n <div class="slider-captcha-finger">👉</div>\n <div class="${PopupSliderCaptcha.CSS_CLASSES.btn}">\n <div class="slider-captcha-icon">→</div>\n </div>\n <div class="${PopupSliderCaptcha.CSS_CLASSES.hint}">向右滑动完成验证</div>\n </div>\n <div class="${PopupSliderCaptcha.CSS_CLASSES.error}"></div>\n </div>\n </div>\n `,a=document.createElement("div");a.innerHTML=e.trim();const r=a.firstElementChild;document.body.appendChild(r),this.elements={overlay:r,modal:r.querySelector("."+PopupSliderCaptcha.CSS_CLASSES.modal),header:r.querySelector("."+PopupSliderCaptcha.CSS_CLASSES.header),title:r.querySelector(".slider-captcha-title"),closeBtn:r.querySelector(".slider-captcha-close"),refreshBtn:r.querySelector(".slider-captcha-refresh"),container:r.querySelector("."+PopupSliderCaptcha.CSS_CLASSES.container),backgroundImg:r.querySelector(".slider-captcha-bg"),sliderImg:r.querySelector(".slider-captcha-piece"),loadingText:r.querySelector("."+PopupSliderCaptcha.CSS_CLASSES.loading),floatingTime:r.querySelector(".slider-captcha-floating-time"),track:r.querySelector("."+PopupSliderCaptcha.CSS_CLASSES.track),fingerAnimation:r.querySelector(".slider-captcha-finger"),btn:r.querySelector("."+PopupSliderCaptcha.CSS_CLASSES.btn),icon:r.querySelector(".slider-captcha-icon"),hint:r.querySelector("."+PopupSliderCaptcha.CSS_CLASSES.hint),error:r.querySelector("."+PopupSliderCaptcha.CSS_CLASSES.error)}}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,s])=>{this.addEventListener(t,i,s)}),this.bindSliderEvents()}bindSliderEvents(){const{elements:t}=this,i={start:t=>this.handleStart(t)};[[t.btn,"mousedown",i.start],[t.btn,"touchstart",i.start],[t.sliderImg,"mousedown",i.start],[t.sliderImg,"touchstart",i.start]].forEach(([t,i,s,e])=>{this.addEventListener(t,i,s,e)})}addEventListener(t,i,s,e={}){t&&"function"==typeof s&&(t.addEventListener(i,s,e),this.eventListeners.push({element:t,event:i,handler:s,options:e}))}removeEventListener(t,i,s,e={}){t&&"function"==typeof s&&(t.removeEventListener(i,s,e),this.eventListeners=this.eventListeners.filter(e=>!(e.element===t&&e.event===i&&e.handler===s)))}removeAllEventListeners(){this.eventListeners.forEach(({element:t,event:i,handler:s,options:e})=>{try{t?.removeEventListener?.(i,s,e)}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,this.boundHandleEnd=this.boundHandleEnd||this.handleEnd.bind(this),document.addEventListener("mousemove",this.throttledHandleMove,{passive:!1}),document.addEventListener("touchmove",this.throttledHandleMove,{passive:!1}),document.addEventListener("mouseup",this.boundHandleEnd),document.addEventListener("touchend",this.boundHandleEnd))}handleMove(t){this.state.isDragging&&(t.preventDefault(),this.lastClientX=this.getClientX(t),this.rafId||(this.rafId=requestAnimationFrame(()=>{const t=this.lastClientX-this.state.startX,{maxX:i}=this.getDimensions();this.state.currentX=Math.max(0,Math.min(t,i)),this.times.push({time:Date.now(),position:this.getPosition()}),this.updateSliderPosition(),this.rafId=null})))}handleEnd(){this.state.isDragging&&(document.removeEventListener("mousemove",this.throttledHandleMove),document.removeEventListener("touchmove",this.throttledHandleMove),document.removeEventListener("mouseup",this.boundHandleEnd),document.removeEventListener("touchend",this.boundHandleEnd),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:s}=this.elements;t.style.transition=s.style.transition=i})}updateUIState(t){const{elements:i}=this,s={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:"#00C870"}),Object.assign(i.icon.style,{color:"white"}),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"})}};s[t]&&requestAnimationFrame(()=>{s[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:s}=this,{maxX:e}=this.getDimensions(),a=s.currentX/e*(i.width-i.sliderSize),r=s.currentX/e;requestAnimationFrame(()=>{t.btn.style.transform=`translate3d(${s.currentX}px, 0, 0)`,t.sliderImg.style.transform=`translate3d(${a}px, 0, 0)`,t.fingerAnimation.style.opacity=.8>r?"0.6":"0"})}show(){if(this.state.isVisible=!0,!this.elements.overlay){this.createElements(),this.bindEvents();const{track:t}=this.elements;t&&(t.style.height="48px",t.style.minHeight="48px",t.style.maxHeight="48px")}this.elements.overlay.style.display="flex",this.elements.overlay.style.transform="translate3d(0,0,0)",requestAnimationFrame(()=>{this.elements.overlay.classList.add("show"),this.elements.modal.classList.add("show"),this.preloadCaptcha()})}async preloadCaptcha(){try{this.showLoading();const[t]=await Promise.all([this.fetchCaptchaData(),this.preloadImages()]);this.captchaData=t,this.showCaptcha(),await this.renderCaptcha()}catch(t){this.handleError(PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR,t.message,t)}}async makeRequest(t,i={}){this.abortController&&this.abortController.abort(),this.abortController=new AbortController;const s=await fetch(t,{method:"POST",headers:{"Content-Type":"application/json",...this.options.headers},body:JSON.stringify({canvasWidth:this.options.width,canvasHeight:this.options.height,timestamp:Date.now(),...this.options.requestData,...i}),signal:this.abortController.signal});if(!s.ok)throw Error(`HTTP ${s.status}: ${s.statusText}`);return s.json()}async fetchCaptchaData(){this.startTime=Date.now();const t=await this.makeRequest(this.options.apiUrl);if(!this.validateCaptchaData(t))throw Error("验证码数据格式错误");return t.data}preloadImages(){return Promise.all(["data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE3LjY1LDYuMzVDMTYuMiw0LjkgMTQuMjEsNCAxMiw0QTgsOCAwIDAsMCA0LDEyQTgsOCAwIDAsMCAxMiwyMEMxNS43MywyMCAxOC44NCwxNy40NSAxOS43MywxNEgxNy42NUMxNi44MywxNi4zMyAxNC42MSwxOCAxMiwxOEE2LDYgMCAwLDEgNiwxMkE2LDYgMCAwLDEgMTIsNkMxMy42Niw2IDE1LjE0LDYuNjkgMTYuMjIsNy43OEwxMywxMUgyMFY0TDE3LjY1LDYuMzVaIiBmaWxsPSIjOTk5Ii8+Cjwvc3ZnPgo="].map(t=>new Promise(i=>{const s=new Image;s.onload=s.onerror=()=>i(),s.src=t})))}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)}handleRequestError(t){"AbortError"===t.name?this.handleError(PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR,"请求被取消",t):t.message?.includes("Failed to fetch")||t.message?.includes("NetworkError")?this.handleError(PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR,"网络连接失败",t):this.handleError(PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR,t.message,t)}handleError(t,i,s=null){const e={[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:e,originalError:s}),this.showError(e)}async loadCaptcha(){try{this.showLoading(),this.startTime=Date.now();const t=await this.makeRequest(this.options.apiUrl);if(!this.validateCaptchaData(t))throw Error("验证码数据格式错误");this.captchaData=t.data,this.showCaptcha(),await this.renderCaptcha()}catch(t){this.handleRequestError(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 s=i[t];return null!=s&&""!==s})}renderCaptcha(){return new Promise((t,i)=>{let s=!1;const e=[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(e).then(()=>{s||(this.hideLoading(),t())}).catch(t=>{s||(s=!0,this.handleError(PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR,"图片加载失败",t),i(t))})})}loadImageAsync(t,i,s){return new Promise((e,a)=>{if(this.imageCache.has(i)){const a=this.imageCache.get(i);return t.src=a.src,this.applyStyles(t,s),t.style.display="block",void e()}const r=this.safeSetTimeout(()=>{a(Error("图片加载超时"))},5e3),n=new Image;n.crossOrigin="anonymous",n.decoding="async",n.onload=function(){if(this.safeClearTimeout(r),"undefined"!=typeof OffscreenCanvas)try{const t=new OffscreenCanvas(n.width,n.height).getContext("2d");t.drawImage(n,0,0);const s=t.getImageData(0,0,n.width,n.height);this.imageCache.set(i,{src:n.src,imageData:s})}catch(t){this.imageCache.set(i,n.cloneNode())}else this.imageCache.set(i,n.cloneNode());t.src=n.src,this.applyStyles(t,s),t.style.display="block",e()}.bind(this),n.onerror=function(t){this.safeClearTimeout(r),a(t)}.bind(this),n.src=i})}applyStyles(t,i){Object.entries(i).forEach(([i,s])=>{t.style[i]="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"},error:{display:"none"}})}showError(t){this.hideLoading(),this.batchUpdateStyles({error:{display:"block",textContent:t}})}batchUpdateStyles(t){requestAnimationFrame(()=>{Object.entries(t).forEach(([t,i])=>{const s=this.elements[t];s&&Object.entries(i).forEach(([t,i])=>{"textContent"===t?s.textContent=i:s.style[t]=i})})})}async verify(){if(this.captchaData)try{const t=await this.makeRequest(this.options.verifyUrl,{loginVo:{nonceStr:this.captchaData.nonceStr,value:this.getPosition()},dragEventList:[...this.times],...this.options.verifyData});this.isVerifySuccess(t)?this.onVerifySuccess(t.data||t.result):this.onVerifyFail(t.message||t.msg||"验证失败,请重试!")}catch(t){this.handleRequestError(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,s=`验证成功!耗时:${(i/1e3).toFixed(2)}s`;this.updateUIState("success"),this.showFloatingTime(s,"success"),this.safeSetTimeout(()=>{this.options.onSuccess?.({ticket:t,timestamp:Date.now(),duration:i}),this.hide()},2e3)}showFloatingTime(t,i="success"){const{elements:s}=this;s.floatingTime.textContent=t,s.floatingTime.className="slider-captcha-floating-time "+i,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.dragStartTime=null,this.cachedDimensions=null,requestAnimationFrame(()=>{this.setTransition(!0),this.elements.btn.style.transform="translate3d(0px, 0, 0)",this.elements.sliderImg.style.transform="translate3d(0px, 0, 0)",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 s=setTimeout(()=>{this.timers.delete(s),t()},i);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,i){let s=0;return(...e)=>{const a=Date.now();if(a-s>=i)return s=a,t.apply(this,e)}}destroy(){try{this.state.isDragging&&(document.removeEventListener("mousemove",this.throttledHandleMove),document.removeEventListener("touchmove",this.throttledHandleMove),this.boundHandleEnd&&(document.removeEventListener("mouseup",this.boundHandleEnd),document.removeEventListener("touchend",this.boundHandleEnd)),this.state.isDragging=!1),this.abortController&&(this.abortController.abort(),this.abortController=null),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,this.elements={},this.state={},this.times=[],this.eventListeners=[],this.timers.clear(),this.throttledHandleMove=null,this.boundHandleEnd=null,this.options.onDestroy&&this.options.onDestroy(),this.options=null}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,this.encryptor=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,s=null){const e={[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:e,originalError:s})}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库");this.encryptor||(this.encryptor=new JSEncrypt),this.lastPublicKey!==i&&(this.encryptor.setPublicKey(i),this.lastPublicKey=i);const s=this.encryptor.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,s={}){try{if(!t)throw Error("密码不能为空");if(!i)throw Error("用户名不能为空");const e=await this.getPublicKey(),a=this.encryptPassword(t,e),r=await this.makeRequest(this.options.validateUrl,{method:"POST",body:JSON.stringify({userName:i,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 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 s=new AbortController,e=setTimeout(()=>{s.abort()},this.options.timeout);try{const a=await fetch(t,{...i,headers:{"Content-Type":"application/json",...this.options.headers,...i.headers},signal:s.signal});if(clearTimeout(e),!a.ok)throw Error(`HTTP错误: ${a.status} ${a.statusText}`);const r=await a.json();return this.options.debug,r}catch(t){if(clearTimeout(e),"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.encryptor=null,this.lastPublicKey=null,this.options.onDestroy&&this.options.onDestroy(),this.options=null}catch(t){}}}r={PopupSliderCaptcha:e,PasswordValidator:a=PasswordValidator,createPasswordValidator:i,validatePassword:s},"undefined"!=typeof window&&(window.SliderCaptcha=e,window.PopupSliderCaptcha=e,window.PasswordValidator=a,window.createPasswordValidator=i,window.validatePassword=s),t.PasswordValidator=a,t.PopupSliderCaptcha=e,t.createPasswordValidator=i,t.default=r,t.validatePassword=s,Object.defineProperty(t,"t",{value:!0})});
@@ -1,388 +1 @@
1
- import JSEncrypt from 'jsencrypt';
2
-
3
- /**
4
- * 密码校验工具类
5
- * 提供密码加密和校验功能
6
- */
7
- class PasswordValidator {
8
- static CONSTANTS = {
9
- DEFAULT_TIMEOUT: 10000,
10
- CACHE_DURATION: 5 * 60 * 1000, // 5分钟缓存
11
- MIN_PASSWORD_LENGTH: 1
12
- }
13
-
14
- static ERROR_TYPES = {
15
- NETWORK_ERROR: 'NETWORK_ERROR',
16
- TIMEOUT_ERROR: 'TIMEOUT_ERROR',
17
- ENCRYPTION_ERROR: 'ENCRYPTION_ERROR',
18
- VALIDATION_ERROR: 'VALIDATION_ERROR',
19
- PUBLIC_KEY_ERROR: 'PUBLIC_KEY_ERROR'
20
- }
21
-
22
- constructor(options = {}) {
23
- this.options = {
24
- publicKeyUrl: options.publicKeyUrl || '/externalapi/commonservice/strongPassword/getPublicKey',
25
- validateUrl: options.validateUrl || '/externalapi/commonservice/strongPassword/checkPassword',
26
- baseUrl: options.baseUrl || '', // 基础域名,如果URL不包含域名则拼接此域名
27
- timeout: options.timeout || PasswordValidator.CONSTANTS.DEFAULT_TIMEOUT,
28
- headers: options.headers || {},
29
- cacheDuration: options.cacheDuration || PasswordValidator.CONSTANTS.CACHE_DURATION,
30
- ...options
31
- };
32
-
33
- // 处理URL,如果URL不包含域名则拼接baseUrl
34
- this.options.publicKeyUrl = this.normalizeUrl(this.options.publicKeyUrl, this.options.baseUrl);
35
- this.options.validateUrl = this.normalizeUrl(this.options.validateUrl, this.options.baseUrl);
36
-
37
- // 缓存公钥,避免重复请求
38
- this.publicKeyCache = null;
39
- this.publicKeyExpiry = null;
40
- this.encryptor = null; // 缓存加密实例
41
- }
42
-
43
- /**
44
- * 标准化URL,如果URL不包含域名则拼接baseUrl
45
- * @param {string} url - 原始URL
46
- * @param {string} baseUrl - 基础域名
47
- * @returns {string} 标准化后的URL
48
- */
49
- normalizeUrl(url, baseUrl) {
50
- if (!url) {
51
- return url
52
- }
53
-
54
- // 如果URL已经包含协议(http://或https://),直接返回
55
- if ((/^https?:\/\//).test(url) || !baseUrl) {
56
- return url
57
- }
58
-
59
- // 确保baseUrl以/结尾,url以/开头
60
- const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
61
- const normalizedUrl = url.startsWith('/') ? url.substring(1) : url;
62
-
63
- return normalizedBaseUrl + normalizedUrl
64
- }
65
-
66
- /**
67
- * 获取公钥
68
- * @returns {Promise<string>} 公钥字符串
69
- */
70
- async getPublicKey() {
71
- // 检查缓存是否有效
72
- if (this.publicKeyCache && this.publicKeyExpiry && Date.now() < this.publicKeyExpiry) {
73
- return this.publicKeyCache
74
- }
75
-
76
- try {
77
- const response = await this.makeRequest(this.options.publicKeyUrl, {
78
- method: 'GET'
79
- });
80
-
81
- if (!this.isSuccessResponse(response)) {
82
- throw new Error(`获取公钥失败:${response.message || response.msg || '未知错误'}`)
83
- }
84
-
85
- // 验证公钥格式
86
- const publicKey = response.data || response.result;
87
- if (!publicKey) {
88
- throw new Error('公钥为空')
89
- }
90
-
91
- // 缓存公钥
92
- this.publicKeyCache = publicKey;
93
- this.publicKeyExpiry = Date.now() + this.options.cacheDuration;
94
-
95
- return this.publicKeyCache
96
- } catch (error) {
97
- console.error('获取公钥失败:', error);
98
- this.handleError(PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR, error.message, error);
99
- throw new Error(`获取公钥失败: ${error.message}`)
100
- }
101
- }
102
-
103
- // 优化:添加响应成功判断方法
104
- isSuccessResponse(response) {
105
- if (!response || typeof response !== 'object') {
106
- return false
107
- }
108
-
109
- const successIndicators = [
110
- response.code === '0',
111
- response.code === 0,
112
- response.success === true,
113
- response.status === 'success',
114
- response.result === true
115
- ];
116
-
117
- return successIndicators.some((indicator) => indicator === true)
118
- }
119
-
120
- handleError(errorType, message, originalError = null) {
121
- const errorMessages = {
122
- [PasswordValidator.ERROR_TYPES.NETWORK_ERROR]: '网络连接失败,请检查网络设置',
123
- [PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR]: '请求超时,请重试',
124
- [PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR]: '密码加密失败',
125
- [PasswordValidator.ERROR_TYPES.VALIDATION_ERROR]: '密码验证失败',
126
- [PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR]: '获取公钥失败'
127
- };
128
-
129
- const errorMessage = errorMessages[errorType] || message || '未知错误';
130
-
131
- // 调用用户自定义错误处理
132
- if (this.options.onError) {
133
- this.options.onError({
134
- type: errorType,
135
- message: errorMessage,
136
- originalError
137
- });
138
- }
139
-
140
- console.error(`密码校验器错误 [${errorType}]:`, errorMessage, originalError);
141
- }
142
-
143
- /**
144
- * 使用RSA公钥加密密码
145
- * @param {string} password 原始密码
146
- * @param {string} publicKey 公钥字符串
147
- * @returns {string} 加密后的密码
148
- */
149
- encryptPassword(password, publicKey) {
150
- try {
151
- if (!password) {
152
- throw new Error('密码不能为空')
153
- }
154
-
155
- if (password.length < PasswordValidator.CONSTANTS.MIN_PASSWORD_LENGTH) {
156
- throw new Error('密码长度不足')
157
- }
158
-
159
- if (!publicKey) {
160
- throw new Error('公钥不能为空')
161
- }
162
-
163
- if (typeof JSEncrypt === 'undefined') {
164
- throw new Error('JSEncrypt库未正确加载,请确保已引入JSEncrypt库')
165
- }
166
-
167
- // 优化:重用 JSEncrypt 实例
168
- if (!this.encryptor) {
169
- this.encryptor = new JSEncrypt();
170
- }
171
-
172
- // 如果公钥发生变化,更新公钥
173
- if (this.lastPublicKey !== publicKey) {
174
- this.encryptor.setPublicKey(publicKey);
175
- this.lastPublicKey = publicKey;
176
- }
177
-
178
- const encrypted = this.encryptor.encrypt(password);
179
-
180
- if (!encrypted) {
181
- throw new Error('密码加密失败,可能是公钥格式不正确')
182
- }
183
-
184
- return encrypted
185
- } catch (error) {
186
- console.error('密码加密失败:', error);
187
- this.handleError(PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR, error.message, error);
188
- throw new Error(`密码加密失败: ${error.message}`)
189
- }
190
- }
191
-
192
- /**
193
- * 校验密码
194
- * @param {string} password 原始密码
195
- * @param {string} userName 用户名
196
- * @param {Object} additionalData 额外的校验数据
197
- * @returns {Promise<Object>} 校验结果
198
- */
199
- async validatePassword(password, userName, additionalData = {}) {
200
- try {
201
- if (!password) {
202
- throw new Error('密码不能为空')
203
- }
204
-
205
- if (!userName) {
206
- throw new Error('用户名不能为空')
207
- }
208
-
209
- // 1. 获取公钥
210
- const publicKey = await this.getPublicKey();
211
-
212
- // 2. 加密密码
213
- const encryptedPassword = this.encryptPassword(password, publicKey);
214
-
215
- // 3. 调用校验接口
216
- const response = await this.makeRequest(this.options.validateUrl, {
217
- method: 'POST',
218
- body: JSON.stringify({
219
- userName,
220
- password: encryptedPassword,
221
- timestamp: Date.now(),
222
- ...additionalData
223
- })
224
- });
225
-
226
- return {
227
- success: this.isSuccessResponse(response),
228
- data: response.data || response.result,
229
- message: response.message || response.msg || '验证完成',
230
- code: response.code || response.status,
231
- originalResponse: response
232
- }
233
- } catch (error) {
234
- console.error('密码校验失败:', error);
235
-
236
- // 优化:根据错误类型返回不同的错误信息
237
- let errorType = PasswordValidator.ERROR_TYPES.VALIDATION_ERROR;
238
- if (error.message.includes('网络') || error.message.includes('fetch')) {
239
- errorType = PasswordValidator.ERROR_TYPES.NETWORK_ERROR;
240
- } else if (error.message.includes('超时') || error.message.includes('timeout')) {
241
- errorType = PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR;
242
- } else if (error.message.includes('加密')) {
243
- errorType = PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR;
244
- }
245
-
246
- this.handleError(errorType, error.message, error);
247
-
248
- return {
249
- success: false,
250
- message: error.message || '密码校验失败',
251
- code: errorType,
252
- error
253
- }
254
- }
255
- }
256
-
257
- /**
258
- * 发送HTTP请求的通用方法
259
- * @param {string} url 请求地址
260
- * @param {Object} options 请求选项
261
- * @returns {Promise<Object>} 响应结果
262
- */
263
- async makeRequest(url, options = {}) {
264
- // 优化:为每个请求创建独立的AbortController,避免多个请求互相取消
265
- const abortController = new AbortController();
266
-
267
- const timeoutId = setTimeout(() => {
268
- abortController.abort();
269
- }, this.options.timeout);
270
-
271
- try {
272
- const response = await fetch(url, {
273
- ...options,
274
- headers: {
275
- 'Content-Type': 'application/json',
276
- ...this.options.headers,
277
- ...options.headers
278
- },
279
- signal: abortController.signal
280
- });
281
-
282
- clearTimeout(timeoutId);
283
-
284
- if (!response.ok) {
285
- throw new Error(`HTTP错误: ${response.status} ${response.statusText}`)
286
- }
287
-
288
- const data = await response.json();
289
-
290
- // 优化:记录请求日志(开发环境)
291
- if (this.options.debug) {
292
- console.log('密码校验器请求:', { url, options, response: data });
293
- }
294
-
295
- return data
296
- } catch (error) {
297
- clearTimeout(timeoutId);
298
-
299
- if (error.name === 'AbortError') {
300
- this.handleError(PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR, '请求超时', error);
301
- throw new Error('请求超时')
302
- }
303
-
304
- if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
305
- this.handleError(PasswordValidator.ERROR_TYPES.NETWORK_ERROR, '网络连接失败', error);
306
- throw new Error('网络连接失败')
307
- }
308
-
309
- throw error
310
- }
311
- }
312
-
313
- /**
314
- * 清除公钥缓存
315
- */
316
- clearCache() {
317
- this.publicKeyCache = null;
318
- this.publicKeyExpiry = null;
319
- }
320
-
321
- /**
322
- * 更新配置
323
- * @param {Object} newOptions 新的配置选项
324
- */
325
- updateOptions(newOptions) {
326
- this.options = { ...this.options, ...newOptions };
327
- // 清除缓存,使用新配置重新获取
328
- this.clearCache();
329
- }
330
-
331
- /**
332
- * 获取缓存状态
333
- * @returns {Object} 缓存状态信息
334
- */
335
- getCacheStatus() {
336
- return {
337
- hasCache: Boolean(this.publicKeyCache),
338
- isExpired: this.publicKeyExpiry ? Date.now() > this.publicKeyExpiry : true,
339
- expiryTime: this.publicKeyExpiry,
340
- remainingTime: this.publicKeyExpiry ? Math.max(0, this.publicKeyExpiry - Date.now()) : 0
341
- }
342
- }
343
-
344
- /**
345
- * 销毁实例,清理资源
346
- */
347
- destroy() {
348
- try {
349
- // 清除缓存
350
- this.clearCache();
351
- this.encryptor = null;
352
- this.lastPublicKey = null;
353
-
354
- // 调用销毁回调
355
- if (this.options.onDestroy) {
356
- this.options.onDestroy();
357
- }
358
-
359
- this.options = null;
360
- } catch (error) {
361
- console.error('销毁密码校验器时出错:', error);
362
- }
363
- }
364
- }
365
-
366
- /**
367
- * 创建密码校验器实例的工厂函数
368
- * @param {Object} options 配置选项
369
- * @returns {PasswordValidator} 密码校验器实例
370
- */
371
- function createPasswordValidator(options) {
372
- return new PasswordValidator(options)
373
- }
374
-
375
- /**
376
- * 快速校验密码的便捷函数
377
- * @param {string} password 密码
378
- * @param {string} userName 用户名
379
- * @param {Object} options 配置选项
380
- * @param {Object} additionalData 额外数据
381
- * @returns {Promise<Object>} 校验结果
382
- */
383
- function validatePassword(password, userName, options = {}, additionalData = {}) {
384
- const validator = new PasswordValidator(options);
385
- return validator.validatePassword(password, userName, additionalData)
386
- }
387
-
388
- export { createPasswordValidator, PasswordValidator as default, validatePassword };
1
+ function t(t){return new PasswordValidator(t)}function r(t,r,s={},i={}){return new PasswordValidator(s).validatePassword(t,r,i)}import JSEncrypt from"jsencrypt";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,this.encryptor=null}normalizeUrl(t,r){return t?/^https?:\/\//.test(t)||!r?t:(r.endsWith("/")?r:r+"/")+(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 r=t.data||t.result;if(!r)throw Error("公钥为空");return this.publicKeyCache=r,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,r,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]||r||"未知错误";this.options.onError&&this.options.onError({type:t,message:i,originalError:s})}encryptPassword(t,r){try{if(!t)throw Error("密码不能为空");if(t.length<PasswordValidator.CONSTANTS.MIN_PASSWORD_LENGTH)throw Error("密码长度不足");if(!r)throw Error("公钥不能为空");if(void 0===JSEncrypt)throw Error("JSEncrypt库未正确加载,请确保已引入JSEncrypt库");this.encryptor||(this.encryptor=new JSEncrypt),this.lastPublicKey!==r&&(this.encryptor.setPublicKey(r),this.lastPublicKey=r);const s=this.encryptor.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,r,s={}){try{if(!t)throw Error("密码不能为空");if(!r)throw Error("用户名不能为空");const i=await this.getPublicKey(),e=this.encryptPassword(t,i),h=await this.makeRequest(this.options.validateUrl,{method:"POST",body:JSON.stringify({userName:r,password:e,timestamp:Date.now(),...s})});return{success:this.isSuccessResponse(h),data:h.data||h.result,message:h.message||h.msg||"验证完成",code:h.code||h.status,originalResponse:h}}catch(t){let r=PasswordValidator.ERROR_TYPES.VALIDATION_ERROR;return t.message.includes("网络")||t.message.includes("fetch")?r=PasswordValidator.ERROR_TYPES.NETWORK_ERROR:t.message.includes("超时")||t.message.includes("timeout")?r=PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR:t.message.includes("加密")&&(r=PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR),this.handleError(r,t.message,t),{success:!1,message:t.message||"密码校验失败",code:r,error:t}}}async makeRequest(t,r={}){const s=new AbortController,i=setTimeout(()=>{s.abort()},this.options.timeout);try{const e=await fetch(t,{...r,headers:{"Content-Type":"application/json",...this.options.headers,...r.headers},signal:s.signal});if(clearTimeout(i),!e.ok)throw Error(`HTTP错误: ${e.status} ${e.statusText}`);const h=await e.json();return this.options.debug,h}catch(t){if(clearTimeout(i),"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.encryptor=null,this.lastPublicKey=null,this.options.onDestroy&&this.options.onDestroy(),this.options=null}catch(t){}}}export{t as createPasswordValidator,PasswordValidator as default,r as validatePassword};
@@ -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,JSEncrypt){"use strict";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(e={}){this.options={publicKeyUrl:e.publicKeyUrl||"/externalapi/commonservice/strongPassword/getPublicKey",validateUrl:e.validateUrl||"/externalapi/commonservice/strongPassword/checkPassword",baseUrl:e.baseUrl||"",timeout:e.timeout||PasswordValidator.CONSTANTS.DEFAULT_TIMEOUT,headers:e.headers||{},cacheDuration:e.cacheDuration||PasswordValidator.CONSTANTS.CACHE_DURATION,...e},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,this.encryptor=null}normalizeUrl(e,t){if(!e)return e;if(/^https?:\/\//.test(e)||!t)return e;return(t.endsWith("/")?t:`${t}/`)+(e.startsWith("/")?e.substring(1):e)}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(!this.isSuccessResponse(e))throw new Error(`获取公钥失败:${e.message||e.msg||"未知错误"}`);const t=e.data||e.result;if(!t)throw new Error("公钥为空");return this.publicKeyCache=t,this.publicKeyExpiry=Date.now()+this.options.cacheDuration,this.publicKeyCache}catch(e){throw this.handleError(PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR,e.message,e),new Error(`获取公钥失败: ${e.message}`)}}isSuccessResponse(e){if(!e||"object"!=typeof e)return!1;return["0"===e.code,0===e.code,!0===e.success,"success"===e.status,!0===e.result].some(e=>!0===e)}handleError(e,t,s=null){const r={[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]:"获取公钥失败"}[e]||t||"未知错误";this.options.onError&&this.options.onError({type:e,message:r,originalError:s})}encryptPassword(e,t){try{if(!e)throw new Error("密码不能为空");if(e.length<PasswordValidator.CONSTANTS.MIN_PASSWORD_LENGTH)throw new Error("密码长度不足");if(!t)throw new Error("公钥不能为空");if(void 0===JSEncrypt)throw new Error("JSEncrypt库未正确加载,请确保已引入JSEncrypt库");this.encryptor||(this.encryptor=new JSEncrypt),this.lastPublicKey!==t&&(this.encryptor.setPublicKey(t),this.lastPublicKey=t);const s=this.encryptor.encrypt(e);if(!s)throw new Error("密码加密失败,可能是公钥格式不正确");return s}catch(e){throw this.handleError(PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR,e.message,e),new Error(`密码加密失败: ${e.message}`)}}async validatePassword(e,t,s={}){try{if(!e)throw new Error("密码不能为空");if(!t)throw new Error("用户名不能为空");const r=await this.getPublicKey(),i=this.encryptPassword(e,r),o=await this.makeRequest(this.options.validateUrl,{method:"POST",body:JSON.stringify({userName:t,password:i,timestamp:Date.now(),...s})});return{success:this.isSuccessResponse(o),data:o.data||o.result,message:o.message||o.msg||"验证完成",code:o.code||o.status,originalResponse:o}}catch(e){let t=PasswordValidator.ERROR_TYPES.VALIDATION_ERROR;return e.message.includes("网络")||e.message.includes("fetch")?t=PasswordValidator.ERROR_TYPES.NETWORK_ERROR:e.message.includes("超时")||e.message.includes("timeout")?t=PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR:e.message.includes("加密")&&(t=PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR),this.handleError(t,e.message,e),{success:!1,message:e.message||"密码校验失败",code:t,error:e}}}async makeRequest(e,t={}){const s=new AbortController,r=setTimeout(()=>{s.abort()},this.options.timeout);try{const i=await fetch(e,{...t,headers:{"Content-Type":"application/json",...this.options.headers,...t.headers},signal:s.signal});if(clearTimeout(r),!i.ok)throw new Error(`HTTP错误: ${i.status} ${i.statusText}`);const o=await i.json();return this.options.debug,o}catch(e){if(clearTimeout(r),"AbortError"===e.name)throw this.handleError(PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR,"请求超时",e),new Error("请求超时");if(e.message.includes("Failed to fetch")||e.message.includes("NetworkError"))throw this.handleError(PasswordValidator.ERROR_TYPES.NETWORK_ERROR,"网络连接失败",e),new Error("网络连接失败");throw e}}clearCache(){this.publicKeyCache=null,this.publicKeyExpiry=null}updateOptions(e){this.options={...this.options,...e},this.clearCache()}getCacheStatus(){return{hasCache:Boolean(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.encryptor=null,this.lastPublicKey=null,this.options.onDestroy&&this.options.onDestroy(),this.options=null}catch(e){}}}e.createPasswordValidator=function(e){return new PasswordValidator(e)},e.default=PasswordValidator,e.validatePassword=function(e,t,s={},r={}){return new PasswordValidator(s).validatePassword(e,t,r)},Object.defineProperty(e,"__esModule",{value:!0})});
1
+ !function(t,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports,require("jsencrypt")):"function"==typeof define&&define.amd?define(["exports","jsencrypt"],r):r((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,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,this.encryptor=null}normalizeUrl(t,r){return t?/^https?:\/\//.test(t)||!r?t:(r.endsWith("/")?r:r+"/")+(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 r=t.data||t.result;if(!r)throw Error("公钥为空");return this.publicKeyCache=r,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,r,s=null){const e={[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]||r||"未知错误";this.options.onError&&this.options.onError({type:t,message:e,originalError:s})}encryptPassword(t,r){try{if(!t)throw Error("密码不能为空");if(t.length<PasswordValidator.CONSTANTS.MIN_PASSWORD_LENGTH)throw Error("密码长度不足");if(!r)throw Error("公钥不能为空");if(void 0===JSEncrypt)throw Error("JSEncrypt库未正确加载,请确保已引入JSEncrypt库");this.encryptor||(this.encryptor=new JSEncrypt),this.lastPublicKey!==r&&(this.encryptor.setPublicKey(r),this.lastPublicKey=r);const s=this.encryptor.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,r,s={}){try{if(!t)throw Error("密码不能为空");if(!r)throw Error("用户名不能为空");const e=await this.getPublicKey(),i=this.encryptPassword(t,e),o=await this.makeRequest(this.options.validateUrl,{method:"POST",body:JSON.stringify({userName:r,password:i,timestamp:Date.now(),...s})});return{success:this.isSuccessResponse(o),data:o.data||o.result,message:o.message||o.msg||"验证完成",code:o.code||o.status,originalResponse:o}}catch(t){let r=PasswordValidator.ERROR_TYPES.VALIDATION_ERROR;return t.message.includes("网络")||t.message.includes("fetch")?r=PasswordValidator.ERROR_TYPES.NETWORK_ERROR:t.message.includes("超时")||t.message.includes("timeout")?r=PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR:t.message.includes("加密")&&(r=PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR),this.handleError(r,t.message,t),{success:!1,message:t.message||"密码校验失败",code:r,error:t}}}async makeRequest(t,r={}){const s=new AbortController,e=setTimeout(()=>{s.abort()},this.options.timeout);try{const i=await fetch(t,{...r,headers:{"Content-Type":"application/json",...this.options.headers,...r.headers},signal:s.signal});if(clearTimeout(e),!i.ok)throw Error(`HTTP错误: ${i.status} ${i.statusText}`);const o=await i.json();return this.options.debug,o}catch(t){if(clearTimeout(e),"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.encryptor=null,this.lastPublicKey=null,this.options.onDestroy&&this.options.onDestroy(),this.options=null}catch(t){}}}t.createPasswordValidator=function(t){return new PasswordValidator(t)},t.default=PasswordValidator,t.validatePassword=function(t,r,s={},e={}){return new PasswordValidator(s).validatePassword(t,r,e)},Object.defineProperty(t,"t",{value:!0})});