From f9bf3c6df3d3d645722c6fdf7fe0ca9405a7665e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com>
Date: Fri, 24 Jul 2026 22:14:29 +0800
Subject: [PATCH] =?UTF-8?q?feat(passport):=20=E4=BC=98=E5=8C=96=E5=BF=98?=
=?UTF-8?q?=E8=AE=B0=E5=AF=86=E7=A0=81=E9=A1=B5=E5=8F=91=E9=80=81=E9=AA=8C?=
=?UTF-8?q?=E8=AF=81=E7=A0=81=E4=BD=93=E9=AA=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 引入图形验证码弹窗,风格与注册页面保持一致
- 使用 localStorage 记录每个账号发送验证码次数,前三次免图形验证码
- 第四次及以后发送验证码需输入正确图形验证码才能发送
- 失败时自动刷新验证码,关闭弹窗时重置加载状态
- 新增相关国际化文案支持中英文
- 调整登录页邮箱登录相关占位符文案提升用户体验
---
.workbuddy/memory/2026-07-24.md | 8 ++
src/i18n/lang/en/login.ts | 5 +-
src/i18n/lang/zh_CN/login.ts | 5 +-
src/views/passport/forget/index.vue | 140 +++++++++++++++++++++++++---
src/views/passport/login/index.vue | 6 +-
5 files changed, 146 insertions(+), 18 deletions(-)
diff --git a/.workbuddy/memory/2026-07-24.md b/.workbuddy/memory/2026-07-24.md
index 09b4ad8..62c3181 100644
--- a/.workbuddy/memory/2026-07-24.md
+++ b/.workbuddy/memory/2026-07-24.md
@@ -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` 编译通过。
diff --git a/src/i18n/lang/en/login.ts b/src/i18n/lang/en/login.ts
index 5baf0ff..0d8e768 100644
--- a/src/i18n/lang/en/login.ts
+++ b/src/i18n/lang/en/login.ts
@@ -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'
}
};
diff --git a/src/i18n/lang/zh_CN/login.ts b/src/i18n/lang/zh_CN/login.ts
index 5c7cb11..8b8dc1b 100644
--- a/src/i18n/lang/zh_CN/login.ts
+++ b/src/i18n/lang/zh_CN/login.ts
@@ -37,6 +37,9 @@ export default {
confirmRule: '两次输入的密码不一致',
emailRule: '请输入正确的邮箱地址',
phoneRule: '请输入正确的手机号',
- codeRule: '请输入验证码'
+ codeRule: '请输入验证码',
+ captchaTitle: '发送验证码',
+ captchaPlaceholder: '请输入图形验证码',
+ sendNow: '立即发送'
}
};
diff --git a/src/views/passport/forget/index.vue b/src/views/passport/forget/index.vue
index ce480e0..91c8735 100644
--- a/src/views/passport/forget/index.vue
+++ b/src/views/passport/forget/index.vue
@@ -143,6 +143,39 @@
{{ t('login.forgetPage.backLogin') }}
+
+
+
+
+ doSendCode(imgCode)"
+ >
+ {{ t('login.forgetPage.sendNow') }}
+
+
+
© {{ new Date().getFullYear() }}
@@ -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 = () => {
diff --git a/src/views/passport/login/index.vue b/src/views/passport/login/index.vue
index 957dac3..7223084 100644
--- a/src/views/passport/login/index.vue
+++ b/src/views/passport/login/index.vue
@@ -36,7 +36,7 @@
class="title-btn"
:class="loginType === 'account' ? 'active' : ''"
@click="onLoginType('account')"
- >账号登录邮箱登录
@@ -96,7 +96,7 @@