优化登录验证码锁定交互
This commit is contained in:
@@ -3,6 +3,8 @@ import { describe, it } from "node:test";
|
|||||||
import {
|
import {
|
||||||
buildCaptchaImageUrl,
|
buildCaptchaImageUrl,
|
||||||
buildSendMobileCodeRequest,
|
buildSendMobileCodeRequest,
|
||||||
|
isImageCaptchaLocked,
|
||||||
|
resolveImageCaptchaInteractionState,
|
||||||
resolveLoginErrorMessage,
|
resolveLoginErrorMessage,
|
||||||
} from "./loginSms.ts";
|
} from "./loginSms.ts";
|
||||||
|
|
||||||
@@ -63,4 +65,46 @@ describe("login sms api helpers", () => {
|
|||||||
"网络错误",
|
"网络错误",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("locks image captcha while sms countdown is active", () => {
|
||||||
|
assert.equal(isImageCaptchaLocked(60), true);
|
||||||
|
assert.equal(isImageCaptchaLocked(1), true);
|
||||||
|
assert.equal(isImageCaptchaLocked(0), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves captcha field and refresh button interaction state", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
resolveImageCaptchaInteractionState({
|
||||||
|
smsSending: false,
|
||||||
|
smsCountdown: 60,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
disabled: true,
|
||||||
|
clearable: false,
|
||||||
|
refreshDisabled: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
resolveImageCaptchaInteractionState({
|
||||||
|
smsSending: false,
|
||||||
|
smsCountdown: 0,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
clearable: true,
|
||||||
|
refreshDisabled: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
resolveImageCaptchaInteractionState({
|
||||||
|
smsSending: true,
|
||||||
|
smsCountdown: 0,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
disabled: true,
|
||||||
|
clearable: false,
|
||||||
|
refreshDisabled: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,17 @@ export interface CaptchaImageUrlOptions {
|
|||||||
cacheBust?: number | string;
|
cacheBust?: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImageCaptchaInteractionParams {
|
||||||
|
smsSending: boolean;
|
||||||
|
smsCountdown: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImageCaptchaInteractionState {
|
||||||
|
disabled: boolean;
|
||||||
|
clearable: boolean;
|
||||||
|
refreshDisabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
function trimEndSlash(value: string): string {
|
function trimEndSlash(value: string): string {
|
||||||
return value.replace(/\/+$/, "");
|
return value.replace(/\/+$/, "");
|
||||||
}
|
}
|
||||||
@@ -39,6 +50,23 @@ export function createCaptchaRandomStr(): string {
|
|||||||
return `${Date.now()}${Math.random().toString(36).slice(2, 10)}`;
|
return `${Date.now()}${Math.random().toString(36).slice(2, 10)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isImageCaptchaLocked(smsCountdown: number): boolean {
|
||||||
|
return smsCountdown > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveImageCaptchaInteractionState({
|
||||||
|
smsSending,
|
||||||
|
smsCountdown,
|
||||||
|
}: ImageCaptchaInteractionParams): ImageCaptchaInteractionState {
|
||||||
|
const disabled = smsSending || isImageCaptchaLocked(smsCountdown);
|
||||||
|
|
||||||
|
return {
|
||||||
|
disabled,
|
||||||
|
clearable: !disabled,
|
||||||
|
refreshDisabled: disabled,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function buildCaptchaImageUrl(
|
export function buildCaptchaImageUrl(
|
||||||
randomStr: string,
|
randomStr: string,
|
||||||
options: CaptchaImageUrlOptions = {},
|
options: CaptchaImageUrlOptions = {},
|
||||||
|
|||||||
@@ -16,11 +16,14 @@
|
|||||||
<van-field v-model="phone" type="tel" clearable :label="t('common.login.fields.phone')"
|
<van-field v-model="phone" type="tel" clearable :label="t('common.login.fields.phone')"
|
||||||
:placeholder="t('common.login.placeholders.phone')" autocomplete="tel" />
|
:placeholder="t('common.login.placeholders.phone')" autocomplete="tel" />
|
||||||
|
|
||||||
<van-field v-model="imageCode" class="login-code-field" type="digit" clearable maxlength="6"
|
<van-field v-model="imageCode" class="login-code-field" type="digit"
|
||||||
|
:clearable="imageCaptchaInteraction.clearable"
|
||||||
|
:disabled="imageCaptchaInteraction.disabled" maxlength="6"
|
||||||
:label="t('common.login.fields.imageCode')" :placeholder="t('common.login.placeholders.imageCode')"
|
:label="t('common.login.fields.imageCode')" :placeholder="t('common.login.placeholders.imageCode')"
|
||||||
autocomplete="off">
|
autocomplete="off">
|
||||||
<template #button>
|
<template #button>
|
||||||
<button class="captcha-image-button" type="button" :aria-label="t('common.login.actions.refreshImageCode')"
|
<button class="captcha-image-button" type="button" :aria-label="t('common.login.actions.refreshImageCode')"
|
||||||
|
:disabled="imageCaptchaInteraction.refreshDisabled"
|
||||||
@click="refreshCaptchaImage">
|
@click="refreshCaptchaImage">
|
||||||
<img v-if="captchaImageUrl" :src="captchaImageUrl" alt="" loading="lazy" />
|
<img v-if="captchaImageUrl" :src="captchaImageUrl" alt="" loading="lazy" />
|
||||||
<RefreshCw v-else :size="18" />
|
<RefreshCw v-else :size="18" />
|
||||||
@@ -97,6 +100,7 @@ import { oauthToken, sendCode } from "@/api/login";
|
|||||||
import {
|
import {
|
||||||
buildCaptchaImageUrl,
|
buildCaptchaImageUrl,
|
||||||
createCaptchaRandomStr,
|
createCaptchaRandomStr,
|
||||||
|
resolveImageCaptchaInteractionState,
|
||||||
resolveLoginErrorMessage,
|
resolveLoginErrorMessage,
|
||||||
} from "@/api/loginSms";
|
} from "@/api/loginSms";
|
||||||
import { NOTICE_EVENT_LOGIN_SUCCESS } from "@/constants/constant";
|
import { NOTICE_EVENT_LOGIN_SUCCESS } from "@/constants/constant";
|
||||||
@@ -213,6 +217,13 @@ const canSendSmsCode = computed(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const imageCaptchaInteraction = computed(() => (
|
||||||
|
resolveImageCaptchaInteractionState({
|
||||||
|
smsSending: smsSending.value,
|
||||||
|
smsCountdown: smsCountdown.value,
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
const sendCodeText = computed(() => {
|
const sendCodeText = computed(() => {
|
||||||
if (smsCountdown.value <= 0) {
|
if (smsCountdown.value <= 0) {
|
||||||
return t("common.login.actions.sendCode");
|
return t("common.login.actions.sendCode");
|
||||||
@@ -229,12 +240,20 @@ function handleSelectCountry(item: { name: string; iso2: string; dialCode: strin
|
|||||||
countrySearch.value = "";
|
countrySearch.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshCaptchaImage() {
|
function resetCaptchaImage() {
|
||||||
imageRandomStr.value = createCaptchaRandomStr();
|
imageRandomStr.value = createCaptchaRandomStr();
|
||||||
captchaImageUrl.value = buildCaptchaImageUrl(imageRandomStr.value);
|
captchaImageUrl.value = buildCaptchaImageUrl(imageRandomStr.value);
|
||||||
imageCode.value = "";
|
imageCode.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshCaptchaImage() {
|
||||||
|
if (imageCaptchaInteraction.value.refreshDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetCaptchaImage();
|
||||||
|
}
|
||||||
|
|
||||||
function clearSmsCountdownTimer() {
|
function clearSmsCountdownTimer() {
|
||||||
if (smsCountdownTimer === null) {
|
if (smsCountdownTimer === null) {
|
||||||
return;
|
return;
|
||||||
@@ -281,9 +300,9 @@ async function handleSendCode() {
|
|||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showToast(resolveLoginErrorMessage(e, t("common.errors.network")));
|
showToast(resolveLoginErrorMessage(e, t("common.errors.network")));
|
||||||
|
resetCaptchaImage();
|
||||||
} finally {
|
} finally {
|
||||||
smsSending.value = false;
|
smsSending.value = false;
|
||||||
refreshCaptchaImage();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,6 +503,11 @@ onUnmounted(() => {
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.captcha-image-button:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.country-search.van-search) {
|
:deep(.country-search.van-search) {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user