UNPKG

otp-code-component

Version:

Google Authenticator 验证组件

385 lines (384 loc) 14.6 kB
import { h } from "@stencil/core"; export class GoogleAuth { 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 is() { return "google-auth"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["google-auth.scss"] }; } static get styleUrls() { return { "$": ["google-auth.css"] }; } static get properties() { return { "getQrcodeUrl": { "type": "string", "attribute": "get-qrcode-url", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "" }, "getter": false, "setter": false, "reflect": false }, "verifyCodeUrl": { "type": "string", "attribute": "verify-code-url", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "" }, "getter": false, "setter": false, "reflect": false }, "accessToken": { "type": "string", "attribute": "access-token", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "" }, "getter": false, "setter": false, "reflect": false }, "type": { "type": "string", "attribute": "type", "mutable": false, "complexType": { "original": "AuthType", "resolved": "\"google\" | \"phone\"", "references": { "AuthType": { "location": "import", "path": "./types", "id": "src/components/google-auth/types.ts::AuthType" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "" }, "getter": false, "setter": false, "reflect": false, "defaultValue": "'google'" }, "phone": { "type": "string", "attribute": "phone", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "" }, "getter": false, "setter": false, "reflect": false } }; } static get states() { return { "qrCodeUrl": {}, "status": {}, "errorMessage": {}, "codeInputs": {}, "showQRCode": {}, "countdown": {} }; } static get events() { return [{ "method": "authSuccess", "name": "authSuccess", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "" }, "complexType": { "original": "AuthSuccessResponse", "resolved": "AuthSuccessResponse", "references": { "AuthSuccessResponse": { "location": "import", "path": "./types", "id": "src/components/google-auth/types.ts::AuthSuccessResponse" } } } }, { "method": "authError", "name": "authError", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "" }, "complexType": { "original": "AuthErrorResponse", "resolved": "AuthErrorResponse", "references": { "AuthErrorResponse": { "location": "import", "path": "./types", "id": "src/components/google-auth/types.ts::AuthErrorResponse" } } } }, { "method": "authBound", "name": "authBound", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "" }, "complexType": { "original": "void", "resolved": "void", "references": {} } }]; } } //# sourceMappingURL=google-auth.js.map