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/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 拦截,非代码问题)。
|
- 关键改动:`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 共用一套找回密码接口。
|
- 后端同址 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',
|
confirmRule: 'The two passwords do not match',
|
||||||
emailRule: 'Please enter a valid email address',
|
emailRule: 'Please enter a valid email address',
|
||||||
phoneRule: 'Please enter a valid phone number',
|
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: '两次输入的密码不一致',
|
confirmRule: '两次输入的密码不一致',
|
||||||
emailRule: '请输入正确的邮箱地址',
|
emailRule: '请输入正确的邮箱地址',
|
||||||
phoneRule: '请输入正确的手机号',
|
phoneRule: '请输入正确的手机号',
|
||||||
codeRule: '请输入验证码'
|
codeRule: '请输入验证码',
|
||||||
|
captchaTitle: '发送验证码',
|
||||||
|
captchaPlaceholder: '请输入图形验证码',
|
||||||
|
sendNow: '立即发送'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -143,6 +143,39 @@
|
|||||||
<a class="login-forget" @click="goLogin">{{ t('login.forgetPage.backLogin') }}</a>
|
<a class="login-forget" @click="goLogin">{{ t('login.forgetPage.backLogin') }}</a>
|
||||||
</div>
|
</div>
|
||||||
</a-form>
|
</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">
|
<div class="login-copyright">
|
||||||
<a-space>
|
<a-space>
|
||||||
<span>© {{ new Date().getFullYear() }}</span>
|
<span>© {{ new Date().getFullYear() }}</span>
|
||||||
@@ -166,7 +199,8 @@
|
|||||||
import { FormInstance } from 'ant-design-vue/es/form';
|
import { FormInstance } from 'ant-design-vue/es/form';
|
||||||
import {
|
import {
|
||||||
sendSmsCaptchaByAdmin,
|
sendSmsCaptchaByAdmin,
|
||||||
sendEmailCaptcha
|
sendEmailCaptcha,
|
||||||
|
getCaptcha
|
||||||
} from '@/api/passport/login';
|
} from '@/api/passport/login';
|
||||||
import { resetPassword } from '@/api/layout';
|
import { resetPassword } from '@/api/layout';
|
||||||
import useFormData from '@/utils/use-form-data';
|
import useFormData from '@/utils/use-form-data';
|
||||||
@@ -219,6 +253,54 @@
|
|||||||
const countdownTime = ref(0);
|
const countdownTime = ref(0);
|
||||||
let countdownTimer: number | null = null;
|
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位,且包含字母和数字
|
// 密码强度:至少8位,且包含字母和数字
|
||||||
const passwordRules = [
|
const passwordRules = [
|
||||||
{
|
{
|
||||||
@@ -287,24 +369,56 @@
|
|||||||
message.error(t('login.forgetPage.emailRule'));
|
message.error(t('login.forgetPage.emailRule'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sendEmailCaptcha({ email: form.email }, { skipTenant: true })
|
|
||||||
.then(() => {
|
|
||||||
message.success(t('login.forgetPage.sentEmail'));
|
|
||||||
startCountdown();
|
|
||||||
})
|
|
||||||
.catch((e: Error) => message.error(e.message));
|
|
||||||
} else {
|
} else {
|
||||||
if (!form.phone || !/^1\d{10}$/.test(form.phone)) {
|
if (!form.phone || !/^1\d{10}$/.test(form.phone)) {
|
||||||
message.error(t('login.forgetPage.phoneRule'));
|
message.error(t('login.forgetPage.phoneRule'));
|
||||||
return;
|
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(() => {
|
.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();
|
startCountdown();
|
||||||
})
|
})
|
||||||
.catch((e: Error) => message.error(e.message));
|
.catch((e: Error) => {
|
||||||
}
|
message.error(e.message);
|
||||||
|
refreshCaptcha();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
codeLoading.value = false;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const startCountdown = () => {
|
const startCountdown = () => {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
class="title-btn"
|
class="title-btn"
|
||||||
:class="loginType === 'account' ? 'active' : ''"
|
:class="loginType === 'account' ? 'active' : ''"
|
||||||
@click="onLoginType('account')"
|
@click="onLoginType('account')"
|
||||||
>账号登录</h4
|
>邮箱登录</h4
|
||||||
>
|
>
|
||||||
<a-divider type="vertical" style="height: 20px" />
|
<a-divider type="vertical" style="height: 20px" />
|
||||||
<h4
|
<h4
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
allow-clear
|
allow-clear
|
||||||
size="large"
|
size="large"
|
||||||
v-model:value="form.account"
|
v-model:value="form.account"
|
||||||
placeholder="邮箱 / 用户ID"
|
placeholder="请输入邮箱"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<UserOutlined />
|
<UserOutlined />
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
<a-input-password
|
<a-input-password
|
||||||
size="large"
|
size="large"
|
||||||
v-model:value="form.password"
|
v-model:value="form.password"
|
||||||
placeholder="登录密码"
|
placeholder="请输入登录密码"
|
||||||
@pressEnter="submit"
|
@pressEnter="submit"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
|
|||||||
Reference in New Issue
Block a user