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