From 911e872a4a52ca8610e33de4fc74b2697f58b44f Mon Sep 17 00:00:00 2001 From: TianyangZhang Date: Wed, 25 Mar 2026 16:15:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=89=8D=E7=AB=AF=E8=AE=A4=E8=AF=81?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E9=87=8D=E6=9E=84=20-=20=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E5=92=8C=E5=A4=9A=E7=A7=8D=E7=99=BB=E5=85=A5?= =?UTF-8?q?=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 前端改动: RegisterView.vue: - 改为传统密码注册(邮箱+用户名+手机号+密码) - 移除邮箱验证码逻辑 - 注册成功后自动登入并跳转到首页 LoginView.vue: - 求职者和管理员两个角色选项卡 - 求职者支持两种登入方式: * 邮箱/用户名 + 密码 * 邮箱 + 验证码(快速登入) - 登入方式通过子选项卡切换 - 添加"忘记密码"链接指向密码重置页面 - 管理员仍使用用户名+密码登入 ResetPasswordView.vue (新建): - 两步流程: 1. 输入邮箱 → 获取验证码 → 输入验证码 2. 输入新密码 → 确认密码 → 重置完成 - 验证码倒计时和重新获取 - 密码重置成功后跳转到登入页 API更新 (auth.js): - 修改 register() 使用 axios 而非 client - 新增 resetPassword() - 请求密码重置 - 新增 confirmResetPassword() - 确认密码重置 路由更新 (router/index.js): - 新增 /forgot-password 路由 设计特点: - 统一的多种登入方式UI - 清晰的密码重置流程 - 保留邮箱验证码快速登入选项 Co-Authored-By: Claude Haiku 4.5 --- offer_frontend/src/api/auth.js | 5 +- offer_frontend/src/router/index.js | 1 + offer_frontend/src/views/auth/LoginView.vue | 280 ++++++++++++------ .../src/views/auth/RegisterView.vue | 144 ++++----- .../src/views/auth/ResetPasswordView.vue | 244 +++++++++++++++ 5 files changed, 491 insertions(+), 183 deletions(-) create mode 100644 offer_frontend/src/views/auth/ResetPasswordView.vue diff --git a/offer_frontend/src/api/auth.js b/offer_frontend/src/api/auth.js index a32a438..677bdbc 100644 --- a/offer_frontend/src/api/auth.js +++ b/offer_frontend/src/api/auth.js @@ -3,6 +3,9 @@ import axios from 'axios' export const sendCode = (email) => axios.post('/api/auth/send-code/', { email }) export const loginApi = (data) => axios.post('/api/auth/login/', data) -export const register = (data) => client.post('/auth/register/', data) +export const register = (data) => axios.post('/api/auth/register/', data) export const getMe = () => client.get('/auth/me/') export const updateMe = (data) => client.patch('/auth/me/', data) +export const resetPassword = (email) => axios.post('/api/auth/reset-password/', { email }) +export const confirmResetPassword = (email, code, newPassword) => + axios.post('/api/auth/confirm-reset-password/', { email, code, new_password: newPassword }) diff --git a/offer_frontend/src/router/index.js b/offer_frontend/src/router/index.js index a5ce303..f5fa62e 100644 --- a/offer_frontend/src/router/index.js +++ b/offer_frontend/src/router/index.js @@ -18,6 +18,7 @@ const routes = [ }, { path: '/login', name: 'Login', component: () => import('@/views/auth/LoginView.vue') }, { path: '/register', name: 'Register', component: () => import('@/views/auth/RegisterView.vue') }, + { path: '/forgot-password', name: 'ResetPassword', component: () => import('@/views/auth/ResetPasswordView.vue') }, // 求职者中心 { path: '/seeker', diff --git a/offer_frontend/src/views/auth/LoginView.vue b/offer_frontend/src/views/auth/LoginView.vue index 3afad90..fdbaf2f 100644 --- a/offer_frontend/src/views/auth/LoginView.vue +++ b/offer_frontend/src/views/auth/LoginView.vue @@ -19,71 +19,112 @@ - - - - - - + + - + { // 发送验证码 async function handleSendCode() { - if (!seekerForm.email) { + if (!codeForm.email) { ElMessage.warning('请输入邮箱') return } sendingCode.value = true try { - await sendCode(seekerForm.email) + await sendCode(codeForm.email) ElMessage.success('验证码已发送到您的邮箱') codeSent.value = true - attemptsLeft.value = 5 - isLocked.value = false startCountdown() } catch (err) { - const errorMsg = err.response?.data?.error || - err.response?.data?.email?.[0] || - err.response?.data?.detail || - '发送失败,请检查邮箱是否正确' - console.error('SendCode Error:', err.response?.data) - ElMessage.error(errorMsg) + ElMessage.error(err.response?.data?.email?.[0] || err.response?.data?.error || '发送失败') } finally { sendingCode.value = false } @@ -182,43 +219,63 @@ async function handleSendCode() { // 重新发送验证码 async function handleResendCode() { - seekerForm.code = '' + codeForm.code = '' await handleSendCode() } -// 求职者登入 -async function handleSeekerLogin() { - if (!seekerForm.code || seekerForm.code.length !== 6) { - ElMessage.warning('请输入正确的6位验证码') +// 密码登入 +async function handlePasswordLogin() { + if (!passwordForm.username || !passwordForm.password) { + ElMessage.warning('请输入邮箱/用户名和密码') return } - seekerLoading.value = true + passwordLoading.value = true try { - await auth.login(seekerForm.email, seekerForm.code) + const { data } = await loginApi({ + username: passwordForm.username, + password: passwordForm.password, + }) + localStorage.setItem('access_token', data.access) + localStorage.setItem('refresh_token', data.refresh) + await auth.fetchMe() + ElMessage.success('登录成功') const redirect = route.query.redirect if (redirect) return router.push(redirect) router.push('/seeker/resume') } catch (err) { - const errorMsg = err.response?.data?.non_field_errors?.[0] || - err.response?.data?.code?.[0] || - err.response?.data?.email?.[0] || - err.response?.data?.error || - '登录失败' - - ElMessage.error(errorMsg) - - // 处理重试次数 - if (errorMsg.includes('错误')) { - attemptsLeft.value = Math.max(0, attemptsLeft.value - 1) - if (attemptsLeft.value === 0) { - isLocked.value = true - ElMessage.error('验证次数过多,请10分钟后重试') - } - } + ElMessage.error(err.response?.data?.detail || err.response?.data?.username?.[0] || '登录失败') } finally { - seekerLoading.value = false + passwordLoading.value = false + } +} + +// 验证码登入 +async function handleCodeLogin() { + if (!codeForm.code || codeForm.code.length !== 6) { + ElMessage.warning('请输入正确的6位验证码') + return + } + + codeLoading.value = true + try { + const { data } = await loginApi({ + email: codeForm.email, + code: codeForm.code, + }) + localStorage.setItem('access_token', data.access) + localStorage.setItem('refresh_token', data.refresh) + await auth.fetchMe() + + ElMessage.success('登录成功') + const redirect = route.query.redirect + if (redirect) return router.push(redirect) + router.push('/seeker/resume') + } catch (err) { + ElMessage.error(err.response?.data?.code?.[0] || '登录失败') + } finally { + codeLoading.value = false } } @@ -231,7 +288,10 @@ async function handleAdminLogin() { adminLoading.value = true try { - const { data } = await loginApi({ username: adminForm.username, password: adminForm.password }) + const { data } = await loginApi({ + username: adminForm.username, + password: adminForm.password, + }) localStorage.setItem('access_token', data.access) localStorage.setItem('refresh_token', data.refresh) await auth.fetchMe() @@ -241,7 +301,7 @@ async function handleAdminLogin() { if (redirect) return router.push(redirect) router.push('/admin/jobs') } catch (err) { - ElMessage.error('用户名或密码错误') + ElMessage.error(err.response?.data?.detail || '用户名或密码错误') } finally { adminLoading.value = false } @@ -298,4 +358,40 @@ h2 { font-weight: 600; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); } + +.method-tabs { + display: flex; + gap: 8px; + margin-bottom: 20px; + background: #f5f7fa; + padding: 4px; + border-radius: 4px; +} + +.method-tab { + flex: 1; + padding: 6px 12px; + border: none; + background: transparent; + cursor: pointer; + font-size: 12px; + color: #606266; + border-radius: 3px; + transition: all 0.2s; +} + +.method-tab:hover { + background: rgba(255, 255, 255, 0.5); +} + +.method-tab.active { + background: #fff; + color: #409eff; + font-weight: 600; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); +} + +.login-container { + width: 100%; +} diff --git a/offer_frontend/src/views/auth/RegisterView.vue b/offer_frontend/src/views/auth/RegisterView.vue index 2b1c3d8..20f78c2 100644 --- a/offer_frontend/src/views/auth/RegisterView.vue +++ b/offer_frontend/src/views/auth/RegisterView.vue @@ -3,67 +3,56 @@

求职者注册

- - - - - + + - + + + + + + + + - - - - 获取验证码 - + + + - - -
- - - {{ codeCountdown > 0 ? `${codeCountdown}s` : '重新获取' }} - -
+ + + 注册 @@ -82,68 +71,43 @@ import { reactive, ref } from 'vue' import { useRouter } from 'vue-router' import { useAuthStore } from '@/stores/auth' import { ElMessage } from 'element-plus' -import { sendCode } from '@/api/auth' -const form = reactive({ username: '', email: '', phone: '', code: '' }) -const codeSent = ref(false) -const sendingCode = ref(false) -const registering = ref(false) -const codeCountdown = ref(0) +const form = reactive({ + email: '', + username: '', + phone: '', + password: '', + passwordConfirm: '', +}) +const loading = ref(false) const router = useRouter() const auth = useAuthStore() -// 倒计时逻辑 -const startCountdown = () => { - codeCountdown.value = 60 - const interval = setInterval(() => { - codeCountdown.value-- - if (codeCountdown.value <= 0) clearInterval(interval) - }, 1000) -} - -// 发送验证码 -async function handleSendCode() { - if (!form.username) { - ElMessage.warning('请输入用户名') - return - } +async function handleRegister() { + // 验证字段 if (!form.email) { ElMessage.warning('请输入邮箱') return } + if (!form.username) { + ElMessage.warning('请输入用户名') + return + } if (!form.phone) { ElMessage.warning('请输入手机号') return } - - sendingCode.value = true - try { - await sendCode(form.email) - ElMessage.success('验证码已发送到您的邮箱') - codeSent.value = true - startCountdown() - } catch (err) { - ElMessage.error(err.response?.data?.email?.[0] || err.response?.data?.error || '发送失败') - } finally { - sendingCode.value = false + if (!form.password || form.password.length < 6) { + ElMessage.warning('请输入至少6位密码') + return } -} - -// 重新发送验证码 -async function handleResendCode() { - form.code = '' - await handleSendCode() -} - -// 注册 -async function handleRegister() { - if (!form.code || form.code.length !== 6) { - ElMessage.warning('请输入正确的6位验证码') + if (form.password !== form.passwordConfirm) { + ElMessage.warning('两次输入的密码不一致') return } - registering.value = true + loading.value = true try { const response = await fetch('/api/auth/register/', { method: 'POST', @@ -151,16 +115,16 @@ async function handleRegister() { 'Content-Type': 'application/json', }, body: JSON.stringify({ - username: form.username, email: form.email, + username: form.username, phone: form.phone, - code: form.code, + password: form.password, }), }) if (!response.ok) { const data = await response.json() - const errorMsg = data.code?.[0] || data.username?.[0] || data.email?.[0] || data.phone?.[0] || '注册失败' + const errorMsg = data.username?.[0] || data.email?.[0] || data.phone?.[0] || data.password?.[0] || '注册失败' ElMessage.error(errorMsg) return } @@ -177,7 +141,7 @@ async function handleRegister() { } catch (err) { ElMessage.error('注册失败,请稍后重试') } finally { - registering.value = false + loading.value = false } } diff --git a/offer_frontend/src/views/auth/ResetPasswordView.vue b/offer_frontend/src/views/auth/ResetPasswordView.vue new file mode 100644 index 0000000..4d551e5 --- /dev/null +++ b/offer_frontend/src/views/auth/ResetPasswordView.vue @@ -0,0 +1,244 @@ + + + + +