feat(passport): 优化忘记密码页发送验证码体验

- 引入图形验证码弹窗,风格与注册页面保持一致
- 使用 localStorage 记录每个账号发送验证码次数,前三次免图形验证码
- 第四次及以后发送验证码需输入正确图形验证码才能发送
- 失败时自动刷新验证码,关闭弹窗时重置加载状态
- 新增相关国际化文案支持中英文
- 调整登录页邮箱登录相关占位符文案提升用户体验
This commit is contained in:
2026-07-24 22:14:29 +08:00
parent d6b56d99ce
commit f9bf3c6df3
5 changed files with 146 additions and 18 deletions

View File

@@ -37,6 +37,9 @@ export default {
confirmRule: 'The two passwords do not match',
emailRule: 'Please enter a valid email address',
phoneRule: 'Please enter a valid phone number',
codeRule: 'Please enter the code'
codeRule: 'Please enter the code',
captchaTitle: 'Send Code',
captchaPlaceholder: 'Please enter the captcha',
sendNow: 'Send Now'
}
};

View File

@@ -37,6 +37,9 @@ export default {
confirmRule: '两次输入的密码不一致',
emailRule: '请输入正确的邮箱地址',
phoneRule: '请输入正确的手机号',
codeRule: '请输入验证码'
codeRule: '请输入验证码',
captchaTitle: '发送验证码',
captchaPlaceholder: '请输入图形验证码',
sendNow: '立即发送'
}
};

View File

@@ -143,6 +143,39 @@
<a class="login-forget" @click="goLogin">{{ t('login.forgetPage.backLogin') }}</a>
</div>
</a-form>
<!-- 图形验证码弹窗 -->
<a-modal
:width="340"
:footer="null"
:title="t('login.forgetPage.captchaTitle')"
v-model:visible="captchaVisible"
@cancel="codeLoading = false"
>
<div class="login-input-group" style="margin-bottom: 16px">
<a-input
v-model:value="imgCode"
:maxlength="5"
size="large"
:placeholder="t('login.forgetPage.captchaPlaceholder')"
allow-clear
@pressEnter="() => doSendCode(imgCode)"
/>
<a-button class="login-captcha">
<img alt="" :src="captchaImage" @click="refreshCaptcha" />
</a-button>
</div>
<a-button
block
size="large"
type="primary"
:loading="codeLoading"
@click="() => doSendCode(imgCode)"
>
{{ t('login.forgetPage.sendNow') }}
</a-button>
</a-modal>
<div class="login-copyright">
<a-space>
<span>© {{ new Date().getFullYear() }}</span>
@@ -166,7 +199,8 @@
import { FormInstance } from 'ant-design-vue/es/form';
import {
sendSmsCaptchaByAdmin,
sendEmailCaptcha
sendEmailCaptcha,
getCaptcha
} from '@/api/passport/login';
import { resetPassword } from '@/api/layout';
import useFormData from '@/utils/use-form-data';
@@ -219,6 +253,54 @@
const countdownTime = ref(0);
let countdownTimer: number | null = null;
// 图形验证码弹窗
const captchaVisible = ref(false);
const captchaImage = ref('');
const captchaText = ref('');
const imgCode = ref('');
const codeLoading = ref(false);
// 同一个账号前 3 次发送不需要图形验证码
const FREE_SEND_TIMES = 3;
// 根据当前找回方式和账号生成发送次数缓存 key
const getSendCountKey = () => {
const account = activeTab.value === 'email' ? form.email : form.phone;
return `forget_send_count_${activeTab.value}_${account}`;
};
// 读取当前账号已发送次数
const getSendCount = () => {
const key = getSendCountKey();
if (!key) return 0;
const count = localStorage.getItem(key);
return count ? Number(count) || 0 : 0;
};
// 增加发送次数
const increaseSendCount = () => {
const key = getSendCountKey();
if (!key) return;
localStorage.setItem(key, String(getSendCount() + 1));
};
// 刷新图形验证码
const refreshCaptcha = () => {
getCaptcha()
.then((data) => {
captchaImage.value = data.base64;
captchaText.value = data.text;
})
.catch((e: Error) => message.error(e.message));
};
// 打开图形验证码弹窗
const openCaptchaModal = () => {
imgCode.value = '';
refreshCaptcha();
captchaVisible.value = true;
};
// 密码强度至少8位且包含字母和数字
const passwordRules = [
{
@@ -287,24 +369,56 @@
message.error(t('login.forgetPage.emailRule'));
return;
}
sendEmailCaptcha({ email: form.email }, { skipTenant: true })
.then(() => {
message.success(t('login.forgetPage.sentEmail'));
startCountdown();
})
.catch((e: Error) => message.error(e.message));
} else {
if (!form.phone || !/^1\d{10}$/.test(form.phone)) {
message.error(t('login.forgetPage.phoneRule'));
return;
}
sendSmsCaptchaByAdmin({ phone: form.phone }, { skipTenant: true })
.then(() => {
message.success(t('login.forgetPage.getCode'));
startCountdown();
})
.catch((e: Error) => message.error(e.message));
}
if (getSendCount() >= FREE_SEND_TIMES) {
openCaptchaModal();
} else {
doSendCode();
}
};
// 真正调用接口发送验证码
const doSendCode = (captchaCode?: string) => {
if (captchaCode !== undefined) {
if (!captchaCode) {
message.error('请输入图形验证码');
return;
}
if (captchaText.value !== captchaCode.toLowerCase()) {
message.error('图形验证码不正确');
return;
}
}
codeLoading.value = true;
const sendPromise =
activeTab.value === 'email'
? sendEmailCaptcha({ email: form.email }, { skipTenant: true })
: sendSmsCaptchaByAdmin({ phone: form.phone }, { skipTenant: true });
sendPromise
.then(() => {
message.success(
activeTab.value === 'email'
? t('login.forgetPage.sentEmail')
: t('login.forgetPage.getCode')
);
captchaVisible.value = false;
increaseSendCount();
startCountdown();
})
.catch((e: Error) => {
message.error(e.message);
refreshCaptcha();
})
.finally(() => {
codeLoading.value = false;
});
};
const startCountdown = () => {

View File

@@ -36,7 +36,7 @@
class="title-btn"
:class="loginType === 'account' ? 'active' : ''"
@click="onLoginType('account')"
>账号登录</h4
>邮箱登录</h4
>
<a-divider type="vertical" style="height: 20px" />
<h4
@@ -85,7 +85,7 @@
allow-clear
size="large"
v-model:value="form.account"
placeholder="邮箱 / 用户ID"
placeholder="请输入邮箱"
>
<template #prefix>
<UserOutlined />
@@ -96,7 +96,7 @@
<a-input-password
size="large"
v-model:value="form.password"
placeholder="登录密码"
placeholder="请输入登录密码"
@pressEnter="submit"
>
<template #prefix>