diff --git a/.workbuddy/memory/2026-07-24.md b/.workbuddy/memory/2026-07-24.md index ffc0585..09b4ad8 100644 --- a/.workbuddy/memory/2026-07-24.md +++ b/.workbuddy/memory/2026-07-24.md @@ -35,3 +35,13 @@ - 分类管理页 `/shop/shopGoodsCategory` 本身**支持**多级:模型有 parentId/children,编辑弹窗用 a-tree-select(toTreeData) 不限层级,列表 computeDepth 递归缩进。 - 但商品端选分类的级联组件 `src/components/SelectGoodsCategory/index.vue` 的 formatData/filterData 只映射 2~3 级;shopGoodsEdit 的 `chooseGoodsCategory` 写死 `value[1].value`/`value[0].label`(仅 2 级),3 级及以上会选错/选不中。 - 后端 `/shop/shop-goods-category` 是否限层级需查后端仓库确认(本仓库看不到)。 + +## 新增:忘记密码(找回密码)功能 +- 参考项目:`/Users/gxwebsoft/VUE/website-admin/app/pages/login.vue`(已验证可用)。 +- 新增页面 `src/views/passport/forget/index.vue`:复用登录页视觉风格,支持「邮箱找回 / 手机找回」两种 Tab,含验证码 60s 倒计时、密码强度校验(≥8 位且含字母+数字)、两次密码一致性校验。 +- 路由:在 `src/router/routes.ts` 启用 `/forget`(登录页「忘记密码」链接 `push('/forget')` 原本就指向它,路由此前被注释)。 +- 新增 API: + - `src/api/passport/login/index.ts`:`sendEmailCaptcha(data,{skipTenant})` → POST /sendEmailCaptcha;`sendSmsCaptchaByAdmin(data,{skipTenant})` → POST /sendSmsCaptchaByAdmin。 + - `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 共用一套找回密码接口。 diff --git a/src/api/layout/index.ts b/src/api/layout/index.ts index c6e22b7..fba80e7 100644 --- a/src/api/layout/index.ts +++ b/src/api/layout/index.ts @@ -1,7 +1,7 @@ import request from '@/utils/request'; import type { ApiResult } from '@/api'; import type { User } from '@/api/system/user/model'; -import type { UpdatePasswordParam, NoticeResult } from './model'; +import type { UpdatePasswordParam, ResetPasswordParam, NoticeResult } from './model'; import { SERVER_API_URL } from '@/config/setting'; import { Company } from '@/api/system/company/model'; import { CmsWebsite } from '@/api/cms/cmsWebsite/model'; @@ -108,6 +108,24 @@ export async function updatePassword( return Promise.reject(new Error(res.data.message)); } +/** + * 找回/重置密码(未登录场景,登录页"忘记密码"入口) + * 后端对应 POST /resetPassword,支持手机或邮箱两种方式 + */ +export async function resetPassword( + data: ResetPasswordParam +): Promise { + const res = await request.post>( + SERVER_API_URL + '/resetPassword', + data, + { headers: { 'X-Tenant-Skip': '1' } } + ); + if (res.data.code === 0) { + return res.data.message ?? '密码重置成功'; + } + return Promise.reject(new Error(res.data.message)); +} + /** * 创建完整网站并初始化 */ diff --git a/src/api/layout/model/index.ts b/src/api/layout/model/index.ts index 317b25d..ff1b585 100644 --- a/src/api/layout/model/index.ts +++ b/src/api/layout/model/index.ts @@ -24,6 +24,23 @@ export interface UpdatePasswordParam { oldPassword: string; } +/** + * 找回/重置密码参数 + * 手机找回时传 phone + smsCode,邮箱找回时传 email + emailCode + */ +export interface ResetPasswordParam { + /** 手机找回时必填 */ + phone?: string; + smsCode?: string; + /** 邮箱找回时必填 */ + email?: string; + emailCode?: string; + /** 新密码 */ + newPassword: string; + /** 确认新密码 */ + confirmPassword: string; +} + /** * 通知数据格式 */ diff --git a/src/api/passport/login/index.ts b/src/api/passport/login/index.ts index 3b80301..e8ea358 100644 --- a/src/api/passport/login/index.ts +++ b/src/api/passport/login/index.ts @@ -139,6 +139,46 @@ export async function sendSmsCaptcha(data: LoginParam) { return Promise.reject(new Error(res.data.message)); } +/** + * 发送短信验证码(后台管理员,跨租户:只要该手机号是超管或管理员即发送) + * 后端对应 POST /sendSmsCaptchaByAdmin,参数为 { phone } + * @param options.skipTenant 为 true 时不携带 tenantId 请求头(登录前无法确定租户时使用) + */ +export async function sendSmsCaptchaByAdmin( + data: LoginParam, + options?: { skipTenant?: boolean } +) { + const res = await request.post>( + SERVER_API_URL + '/sendSmsCaptchaByAdmin', + data, + options?.skipTenant ? { headers: { 'X-Tenant-Skip': '1' } } : undefined + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +/** + * 发送邮箱验证码(找回密码等登录前场景使用) + * 后端对应 POST /sendEmailCaptcha,参数为 { email } + * @param options.skipTenant 为 true 时不携带 tenantId 请求头(登录前无法确定租户时使用) + */ +export async function sendEmailCaptcha( + data: LoginParam, + options?: { skipTenant?: boolean } +) { + const res = await request.post>( + SERVER_API_URL + '/sendEmailCaptcha', + data, + options?.skipTenant ? { headers: { 'X-Tenant-Skip': '1' } } : undefined + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + /** * 登录 */ diff --git a/src/i18n/lang/en/login.ts b/src/i18n/lang/en/login.ts index e29569a..5baf0ff 100644 --- a/src/i18n/lang/en/login.ts +++ b/src/i18n/lang/en/login.ts @@ -10,5 +10,33 @@ export default { loading: 'loading', oldPassword: 'Old', newPassword: 'New', - confirm: 'Confirm' + confirm: 'Confirm', + forgetPage: { + title: 'Forgot Password', + subTitle: 'Enter your account info to reset the password', + tabEmail: 'By Email', + tabPhone: 'By Phone', + email: 'Email', + phone: 'Phone', + emailCode: 'Email Code', + code: 'SMS Code', + getCode: 'Get Code', + sentEmail: 'Verification code sent, please check your email', + sentPhone: 'Verification code sent', + newPassword: 'New Password', + confirmPassword: 'Confirm Password', + reset: 'Reset Password', + backLogin: 'Back to Login', + success: 'Password reset successfully, please login with the new password', + emailPlaceholder: 'Please enter your registered email', + phonePlaceholder: 'Please enter your registered phone', + codePlaceholder: 'Please enter the code', + passwordPlaceholder: 'At least 8 chars, must contain letters and numbers', + confirmPlaceholder: 'Please enter the password again', + newPasswordRule: 'Password must be at least 8 chars with letters and numbers', + 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' + } }; diff --git a/src/i18n/lang/zh_CN/login.ts b/src/i18n/lang/zh_CN/login.ts index 08a41b5..5c7cb11 100644 --- a/src/i18n/lang/zh_CN/login.ts +++ b/src/i18n/lang/zh_CN/login.ts @@ -10,5 +10,33 @@ export default { loading: '登录中', oldPassword: '旧密码', newPassword: '新密码', - confirm: '确认密码' + confirm: '确认密码', + forgetPage: { + title: '忘记密码', + subTitle: '请输入账号信息以重置密码', + tabEmail: '邮箱找回', + tabPhone: '手机找回', + email: '邮箱', + phone: '手机号', + emailCode: '邮箱验证码', + code: '短信验证码', + getCode: '获取验证码', + sentEmail: '验证码已发送,请查收邮箱', + sentPhone: '验证码已发送', + newPassword: '新密码', + confirmPassword: '确认新密码', + reset: '重置密码', + backLogin: '返回登录', + success: '密码重置成功,请使用新密码登录', + emailPlaceholder: '请输入注册邮箱', + phonePlaceholder: '请输入注册手机号', + codePlaceholder: '请输入验证码', + passwordPlaceholder: '至少 8 位,需包含字母和数字', + confirmPlaceholder: '请再次输入新密码', + newPasswordRule: '新密码至少 8 位,需包含字母和数字', + confirmRule: '两次输入的密码不一致', + emailRule: '请输入正确的邮箱地址', + phoneRule: '请输入正确的手机号', + codeRule: '请输入验证码' + } }; diff --git a/src/router/routes.ts b/src/router/routes.ts index f0f3412..036efa9 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -50,11 +50,11 @@ export const routes = [ component: () => import('@/views/passport/merchant/success.vue'), meta: { title: '申请提交成功' } }, - // { - // path: '/forget', - // component: () => import('@/views/passport/forget/index.vue'), - // meta: { title: '忘记密码' } - // }, + { + path: '/forget', + component: () => import('@/views/passport/forget/index.vue'), + meta: { title: '忘记密码' } + }, // { // path: '/wx-work-login', // component: () => import('@/views/passport/wx-work/index.vue'), diff --git a/src/utils/request.ts b/src/utils/request.ts index ffddaf2..a493a1d 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -47,6 +47,11 @@ service.interceptors.request.use( } // 获取租户ID if (config.headers) { + // 登录前场景(找回密码等)由调用方显式设置 X-Tenant-Skip, + // 此时不做任何租户/企业/商户/域名头注入,避免串租户 + if (config.headers['X-Tenant-Skip']) { + return config; + } // 附加企业ID const companyId = localStorage.getItem('CompanyId'); if (companyId) { diff --git a/src/views/passport/forget/index.vue b/src/views/passport/forget/index.vue new file mode 100644 index 0000000..ce480e0 --- /dev/null +++ b/src/views/passport/forget/index.vue @@ -0,0 +1,507 @@ + + + + +