otp-code-component
Version:
Google Authenticator 验证组件
385 lines (384 loc) • 14.6 kB
JavaScript
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