UNPKG

otp-code-component

Version:

Google Authenticator 验证组件

239 lines (235 loc) 12.8 kB
import { p as proxyCustomElement, H, c as createEvent, h } from './p-Ckl2LJRn.js'; export { g as getAssetPath, r as render, s as setAssetPath, a as setNonce, b as setPlatformOptions } from './p-Ckl2LJRn.js'; const googleAuthCss = ".google-auth-container{display:flex;flex-direction:column;align-items:center;gap:24px;padding:20px;text-align:center;max-width:400px;margin:0 auto}.google-auth-container .auth-header{margin-bottom:24px}.google-auth-container .auth-header .lock-icon{width:48px;height:48px;margin:0 auto;color:#1677ff}.google-auth-container .auth-header .lock-icon svg{width:100%;height:100%}.google-auth-container .auth-header h2{color:#333;font-size:16px;font-weight:normal;margin:0}.google-auth-container .verification-section{display:flex;flex-direction:column;align-items:center;width:100%;gap:16px}.google-auth-container .verification-section p{margin-bottom:24px;color:#333;font-size:14px}.google-auth-container .verification-section .get-qr-button,.google-auth-container .verification-section .get-code-button{padding:8px 16px;background-color:#1677ff;color:white;border:none;border-radius:4px;cursor:pointer;font-size:14px;transition:all 0.3s}.google-auth-container .verification-section .get-qr-button:hover,.google-auth-container .verification-section .get-code-button:hover{background-color:#4096ff}.google-auth-container .verification-section .get-qr-button:disabled,.google-auth-container .verification-section .get-code-button:disabled{background-color:#d9d9d9;cursor:not-allowed}.google-auth-container .verification-section .qr-code{margin-top:24px}.google-auth-container .verification-section .qr-code img{max-width:200px;border:1px solid #d9d9d9;border-radius:4px;padding:8px}.google-auth-container .verification-section .code-input-group{display:flex;gap:8px;justify-content:center;margin-bottom:24px}.google-auth-container .verification-section .code-input-group input{width:40px;height:40px;text-align:center;border:1px solid #d9d9d9;border-radius:4px;font-size:18px;outline:none}.google-auth-container .verification-section .code-input-group input:focus{border-color:#1677ff;box-shadow:0 0 0 2px rgba(22, 119, 255, 0.2)}.google-auth-container .verification-section .code-input-group input:disabled{background-color:#f5f5f5;cursor:not-allowed}.google-auth-container .trust-device{display:flex;align-items:center;gap:8px;color:#666;font-size:14px;margin-top:16px}.google-auth-container .trust-device input[type=checkbox]{margin:0}.google-auth-container .mfa-help{color:#4285f4;text-decoration:none;font-size:14px;margin-top:16px}.google-auth-container .mfa-help:hover{text-decoration:underline}.google-auth-container .error-message{color:#ff4d4f;margin-top:16px;font-size:14px}.google-auth-container .success-message{color:#52c41a;margin-top:16px;font-size:14px}.qr-section{text-align:center}.qr-section h3{color:#333;font-size:16px;font-weight:normal;margin-bottom:20px}.qr-section img{max-width:200px;height:auto;margin-top:10px;border:1px solid #ddd;padding:10px;border-radius:4px}"; const GoogleAuth = /*@__PURE__*/ proxyCustomElement(class GoogleAuth extends H { constructor() { super(); this.__registerHost(); this.__attachShadow(); this.authSuccess = createEvent(this, "authSuccess"); this.authError = createEvent(this, "authError"); this.authBound = createEvent(this, "authBound"); } getQrcodeUrl; verifyCodeUrl; accessToken; type = 'google'; phone; qrCodeUrl; status = 'initial'; errorMessage = ''; codeInputs = ['', '', '', '', '', '']; showQRCode = false; countdown = 0; countdownTimer; authSuccess; authError; authBound; inputRefs = []; componentWillLoad() { if (this.type === 'phone' && this.phone) { this.fetchQRCode(); } } disconnectedCallback() { if (this.countdownTimer) { window.clearInterval(this.countdownTimer); } } startCountdown() { this.countdown = 60; this.countdownTimer = window.setInterval(() => { if (this.countdown > 0) { this.countdown--; } else { window.clearInterval(this.countdownTimer); } }, 1000); } async fetchQRCode() { try { const response = await fetch(`${this.getQrcodeUrl}`, { method: 'POST', headers: { Authorization: `Bearer ${this.accessToken}`, 'Content-Type': 'application/json', }, }); if (!response.ok) throw new Error(this.type === 'google' ? '获取二维码失败' : '获取验证码失败'); const data = (await response.json()); if (data.code !== 200) { throw new Error(data.message || (this.type === 'google' ? '获取二维码失败' : '获取验证码失败')); } if (this.type === 'google') { this.qrCodeUrl = data.data.qr_img; this.showQRCode = true; this.authBound.emit(); } else { this.startCountdown(); } } catch (error) { this.errorMessage = error.message; this.authError.emit({ code: 400, message: error.message, }); } } async verifyCode() { const code = this.codeInputs.join(''); if (!code || code.length !== 6) { this.errorMessage = '请输入6位验证码'; return; } this.status = 'verifying'; try { const response = await fetch(`${this.verifyCodeUrl}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.accessToken}`, }, body: JSON.stringify({ code, ...(this.type === 'phone' ? { phone: this.phone } : {}), }), }); if (!response.ok) throw new Error('验证失败'); const data = await response.json(); if (data.code !== 200) { throw new Error(data.message || '验证失败'); } if (data.data.is_pass) { this.status = 'success'; this.authSuccess.emit({ code: data.code, message: data.message, data: { is_pass: data.data.is_pass } }); } else { this.status = 'error'; this.errorMessage = '验证失败'; this.authError.emit({ code: 400, message: '验证失败', }); } } catch (error) { this.status = 'error'; this.errorMessage = error.message; this.authError.emit({ code: 400, message: error.message, }); } } handleInput(index, event) { const input = event.target; const value = input.value; // 如果输入的不是数字,清空输入框 if (!/^\d*$/.test(value)) { input.value = ''; return; } // 只取最后一个字符 const lastChar = value.slice(-1); input.value = lastChar; // 更新当前输入框的值 this.codeInputs = [...this.codeInputs.slice(0, index), lastChar, ...this.codeInputs.slice(index + 1)]; // 自动聚焦下一个输入框 if (lastChar && index < 5) { this.inputRefs[index + 1]?.focus(); } // 当所有输入框都填写完成时自动验证 if (this.codeInputs.every(v => v) && this.codeInputs.join('').length === 6) { this.verifyCode(); } } handleKeyDown(index, event) { // 阻止所有按键,只允许以下情况: // 1. 数字键 // 2. 退格键 // 3. Delete 键 // 4. 方向键 // 5. Tab 键 const allowedKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; if (!allowedKeys.includes(event.key)) { event.preventDefault(); return; } // 处理退格键 if (event.key === 'Backspace') { if (!this.codeInputs[index] && index > 0) { // 当前输入框为空且按下删除键时,聚焦到前一个输入框并清空其值 this.inputRefs[index - 1]?.focus(); this.codeInputs = [...this.codeInputs.slice(0, index - 1), '', ...this.codeInputs.slice(index)]; } else { // 清空当前输入框 this.codeInputs = [...this.codeInputs.slice(0, index), '', ...this.codeInputs.slice(index + 1)]; } } } handlePaste = (event) => { event.preventDefault(); const pastedText = event.clipboardData.getData('text'); const numbers = pastedText.replace(/\D/g, '').slice(0, 6).split(''); this.codeInputs = [...numbers, ...Array(6 - numbers.length).fill('')]; // 聚焦到最后一个填充的输入框的下一个 const nextEmptyIndex = numbers.length; if (nextEmptyIndex < 6) { this.inputRefs[nextEmptyIndex]?.focus(); } // 如果粘贴的内容正好是6位数字,触发验证 if (numbers.length === 6) { this.verifyCode(); } }; render() { const maskPhone = (phone) => { return phone?.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'); }; return (h("div", { class: "google-auth-container" }, h("div", { class: "auth-header" }, h("div", { class: "lock-icon" }, h("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "currentColor" }, h("path", { d: "M12 1C8.676 1 6 3.676 6 7v2H4v14h16V9h-2V7c0-3.324-2.676-6-6-6zm0 2c2.276 0 4 1.724 4 4v2H8V7c0-2.276 1.724-4 4-4z" })))), h("div", { class: "verification-section" }, this.type === 'google' ? (h("p", null, "\u5F53\u524D\u8EAB\u4EFD\u5DF2\u5F00\u542F\u767B\u5F55\u4FDD\u62A4\uFF0C\u8BF7\u8F93\u5165Google Authenticator\u4E0A\u7684\u5B89\u5168\u7801")) : (h("p", null, "\u5F53\u524D\u8EAB\u4EFD\u5DF2\u5F00\u542F\u767B\u5F55\u4FDD\u62A4\uFF0C\u8BF7\u8F93\u5165\u53D1\u9001\u81F3 ", maskPhone(this.phone), " \u7684\u9A8C\u8BC1\u7801")), h("div", { class: "code-input-group" }, Array(6) .fill(null) .map((_, index) => (h("input", { key: index, type: "text", inputMode: "numeric", pattern: "[0-9]*", maxLength: 1, value: this.codeInputs[index], onInput: e => this.handleInput(index, e), onKeyDown: e => this.handleKeyDown(index, e), onPaste: this.handlePaste, disabled: this.status === 'verifying' || this.status === 'success', ref: el => (this.inputRefs[index] = el) })))), this.type === 'google' && !this.showQRCode && (h("button", { class: "get-qr-button", onClick: () => this.fetchQRCode() }, "\u83B7\u53D6\u4E8C\u7EF4\u7801")), this.type === 'phone' && (h("button", { class: "get-code-button", onClick: () => this.fetchQRCode(), disabled: this.countdown > 0 }, this.countdown > 0 ? `重新获取(${this.countdown}s)` : '获取验证码')), this.type === 'google' && this.showQRCode && this.qrCodeUrl && (h("div", { class: "qr-code" }, h("img", { src: this.qrCodeUrl, alt: "Google Authenticator QR Code" })))), this.errorMessage && h("div", { class: "error-message" }, this.errorMessage), this.status === 'success' && h("div", { class: "success-message" }, "\u9A8C\u8BC1\u6210\u529F\uFF01"))); } static get style() { return googleAuthCss; } }, [1, "google-auth", { "getQrcodeUrl": [1, "get-qrcode-url"], "verifyCodeUrl": [1, "verify-code-url"], "accessToken": [1, "access-token"], "type": [1], "phone": [1], "qrCodeUrl": [32], "status": [32], "errorMessage": [32], "codeInputs": [32], "showQRCode": [32], "countdown": [32] }]); function defineCustomElement() { if (typeof customElements === "undefined") { return; } const components = ["google-auth"]; components.forEach(tagName => { switch (tagName) { case "google-auth": if (!customElements.get(tagName)) { customElements.define(tagName, GoogleAuth); } break; } }); } defineCustomElement(); export { GoogleAuth, defineCustomElement as d }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map