feat(passport): 优化忘记密码页发送验证码体验
- 引入图形验证码弹窗,风格与注册页面保持一致 - 使用 localStorage 记录每个账号发送验证码次数,前三次免图形验证码 - 第四次及以后发送验证码需输入正确图形验证码才能发送 - 失败时自动刷新验证码,关闭弹窗时重置加载状态 - 新增相关国际化文案支持中英文 - 调整登录页邮箱登录相关占位符文案提升用户体验
This commit is contained in:
@@ -45,3 +45,11 @@
|
||||
- `src/api/layout/index.ts`:`resetPassword(data)` → POST /resetPassword(带 X-Tenant-Skip:1);`src/api/layout/model/index.ts` 新增 `ResetPasswordParam`。
|
||||
- 关键改动:`src/utils/request.ts` 请求拦截器在请求头含 `X-Tenant-Skip` 时跳过注入 TenantId/CompanyId/MerchantId/Domain(登录前找回密码场景,避免串租户)。`vite build --outDir dist-verify` 验证通过(真实 `vite build` 因沙箱 safe-delete 守卫删 dist 拦截,非代码问题)。
|
||||
- 后端同址 server.websoft.top/api,与 website-admin 共用一套找回密码接口。
|
||||
|
||||
## 优化:忘记密码页发送验证码前 3 次免图形验证
|
||||
- 修改 `src/views/passport/forget/index.vue`:引入 `getCaptcha`,新增图形验证码弹窗(与 `register2/index.vue` 风格一致)。
|
||||
- 用 `localStorage` 按 `forget_send_count_${activeTab}_${account}` 记录每个邮箱/手机号的发送次数。
|
||||
- 同一个账号前 3 次点击「获取验证码」直接调用 `sendEmailCaptcha`/`sendSmsCaptchaByAdmin`;第 4 次起弹出图形验证码弹窗,输入正确并点击「立即发送」后才真正发送。
|
||||
- 失败时自动刷新图形验证码;弹窗关闭时重置 `codeLoading`。
|
||||
- 新增 i18n 键 `login.forgetPage.captchaTitle`、`captchaPlaceholder`、`sendNow`(zh_CN/en)。
|
||||
- `vue-tsc` 本次新增代码无类型错误;`vite build --outDir dist-verify-captcha` 编译通过。
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -37,6 +37,9 @@ export default {
|
||||
confirmRule: '两次输入的密码不一致',
|
||||
emailRule: '请输入正确的邮箱地址',
|
||||
phoneRule: '请输入正确的手机号',
|
||||
codeRule: '请输入验证码'
|
||||
codeRule: '请输入验证码',
|
||||
captchaTitle: '发送验证码',
|
||||
captchaPlaceholder: '请输入图形验证码',
|
||||
sendNow: '立即发送'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
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(t('login.forgetPage.getCode'));
|
||||
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));
|
||||
}
|
||||
.catch((e: Error) => {
|
||||
message.error(e.message);
|
||||
refreshCaptcha();
|
||||
})
|
||||
.finally(() => {
|
||||
codeLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const startCountdown = () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user