feat: 前端认证系统重构 - 密码注册和多种登入方式
前端改动: 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 <noreply@anthropic.com>
This commit is contained in:
parent
8a5ed86421
commit
911e872a4a
|
|
@ -3,6 +3,9 @@ import axios from 'axios'
|
||||||
|
|
||||||
export const sendCode = (email) => axios.post('/api/auth/send-code/', { email })
|
export const sendCode = (email) => axios.post('/api/auth/send-code/', { email })
|
||||||
export const loginApi = (data) => axios.post('/api/auth/login/', data)
|
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 getMe = () => client.get('/auth/me/')
|
||||||
export const updateMe = (data) => client.patch('/auth/me/', data)
|
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 })
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ const routes = [
|
||||||
},
|
},
|
||||||
{ path: '/login', name: 'Login', component: () => import('@/views/auth/LoginView.vue') },
|
{ path: '/login', name: 'Login', component: () => import('@/views/auth/LoginView.vue') },
|
||||||
{ path: '/register', name: 'Register', component: () => import('@/views/auth/RegisterView.vue') },
|
{ path: '/register', name: 'Register', component: () => import('@/views/auth/RegisterView.vue') },
|
||||||
|
{ path: '/forgot-password', name: 'ResetPassword', component: () => import('@/views/auth/ResetPasswordView.vue') },
|
||||||
// 求职者中心
|
// 求职者中心
|
||||||
{
|
{
|
||||||
path: '/seeker',
|
path: '/seeker',
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,63 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 求职者登入(邮箱验证码) -->
|
<!-- 求职者登入 -->
|
||||||
<el-form v-if="role === 'seeker'" :model="seekerForm" @submit.prevent="handleSeekerLogin">
|
<div v-if="role === 'seeker'" class="login-container">
|
||||||
|
<!-- 登入方式选择 -->
|
||||||
|
<div class="method-tabs">
|
||||||
|
<button
|
||||||
|
:class="['method-tab', { active: seekerMethod === 'password' }]"
|
||||||
|
@click="seekerMethod = 'password'"
|
||||||
|
>
|
||||||
|
邮箱/用户名登入
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="['method-tab', { active: seekerMethod === 'code' }]"
|
||||||
|
@click="seekerMethod = 'code'"
|
||||||
|
>
|
||||||
|
验证码登入
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 密码登入 -->
|
||||||
|
<el-form v-if="seekerMethod === 'password'" :model="passwordForm" @submit.prevent="handlePasswordLogin">
|
||||||
|
<el-form-item>
|
||||||
|
<el-input
|
||||||
|
v-model="passwordForm.username"
|
||||||
|
placeholder="邮箱或用户名"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-input
|
||||||
|
v-model="passwordForm.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="密码"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
native-type="submit"
|
||||||
|
:loading="passwordLoading"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</el-button>
|
||||||
|
<div style="text-align: right; margin-top: 8px;">
|
||||||
|
<router-link to="/forgot-password" style="color: #909399; font-size: 12px; text-decoration: none;">
|
||||||
|
忘记密码?
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 验证码登入 -->
|
||||||
|
<el-form v-if="seekerMethod === 'code'" @submit.prevent="handleCodeLogin">
|
||||||
<!-- 邮箱输入 -->
|
<!-- 邮箱输入 -->
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="seekerForm.email"
|
v-model="codeForm.email"
|
||||||
placeholder="请输入邮箱"
|
|
||||||
type="email"
|
type="email"
|
||||||
|
placeholder="请输入邮箱"
|
||||||
:disabled="codeSent"
|
:disabled="codeSent"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
@ -47,7 +96,7 @@
|
||||||
<el-form-item v-if="codeSent">
|
<el-form-item v-if="codeSent">
|
||||||
<div style="display: flex; gap: 8px;">
|
<div style="display: flex; gap: 8px;">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="seekerForm.code"
|
v-model="codeForm.code"
|
||||||
placeholder="请输入验证码(6位)"
|
placeholder="请输入验证码(6位)"
|
||||||
maxlength="6"
|
maxlength="6"
|
||||||
style="flex: 1"
|
style="flex: 1"
|
||||||
|
|
@ -62,28 +111,20 @@
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 错误提示 -->
|
|
||||||
<div v-if="codeSent && attemptsLeft < 5" style="margin-bottom: 12px; text-align: center; color: #f56c6c; font-size: 12px;">
|
|
||||||
还有 {{ attemptsLeft }} 次重试机会
|
|
||||||
</div>
|
|
||||||
<div v-if="isLocked" style="margin-bottom: 12px; text-align: center; color: #f56c6c; font-size: 12px;">
|
|
||||||
验证次数过多,请10分钟后重试
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 登录按钮 -->
|
<!-- 登录按钮 -->
|
||||||
<el-button
|
<el-button
|
||||||
v-if="codeSent"
|
v-if="codeSent"
|
||||||
type="primary"
|
type="primary"
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
:loading="seekerLoading"
|
:loading="codeLoading"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:disabled="isLocked"
|
|
||||||
>
|
>
|
||||||
登录
|
登录
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 管理员登入(用户名密码) -->
|
<!-- 管理员登入 -->
|
||||||
<el-form v-if="role === 'admin'" :model="adminForm" @submit.prevent="handleAdminLogin">
|
<el-form v-if="role === 'admin'" :model="adminForm" @submit.prevent="handleAdminLogin">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-input
|
<el-input
|
||||||
|
|
@ -126,17 +167,20 @@ import { ElMessage } from 'element-plus'
|
||||||
import { sendCode, loginApi } from '@/api/auth'
|
import { sendCode, loginApi } from '@/api/auth'
|
||||||
|
|
||||||
const role = ref('seeker')
|
const role = ref('seeker')
|
||||||
|
const seekerMethod = ref('password')
|
||||||
|
|
||||||
// 求职者表单
|
// 密码登入
|
||||||
const seekerForm = reactive({ email: '', code: '' })
|
const passwordForm = reactive({ username: '', password: '' })
|
||||||
|
const passwordLoading = ref(false)
|
||||||
|
|
||||||
|
// 验证码登入
|
||||||
|
const codeForm = reactive({ email: '', code: '' })
|
||||||
const codeSent = ref(false)
|
const codeSent = ref(false)
|
||||||
const sendingCode = ref(false)
|
const sendingCode = ref(false)
|
||||||
const seekerLoading = ref(false)
|
const codeLoading = ref(false)
|
||||||
const codeCountdown = ref(0)
|
const codeCountdown = ref(0)
|
||||||
const attemptsLeft = ref(5)
|
|
||||||
const isLocked = ref(false)
|
|
||||||
|
|
||||||
// 管理员表单
|
// 管理员登入
|
||||||
const adminForm = reactive({ username: '', password: '' })
|
const adminForm = reactive({ username: '', password: '' })
|
||||||
const adminLoading = ref(false)
|
const adminLoading = ref(false)
|
||||||
|
|
||||||
|
|
@ -155,26 +199,19 @@ const startCountdown = () => {
|
||||||
|
|
||||||
// 发送验证码
|
// 发送验证码
|
||||||
async function handleSendCode() {
|
async function handleSendCode() {
|
||||||
if (!seekerForm.email) {
|
if (!codeForm.email) {
|
||||||
ElMessage.warning('请输入邮箱')
|
ElMessage.warning('请输入邮箱')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sendingCode.value = true
|
sendingCode.value = true
|
||||||
try {
|
try {
|
||||||
await sendCode(seekerForm.email)
|
await sendCode(codeForm.email)
|
||||||
ElMessage.success('验证码已发送到您的邮箱')
|
ElMessage.success('验证码已发送到您的邮箱')
|
||||||
codeSent.value = true
|
codeSent.value = true
|
||||||
attemptsLeft.value = 5
|
|
||||||
isLocked.value = false
|
|
||||||
startCountdown()
|
startCountdown()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMsg = err.response?.data?.error ||
|
ElMessage.error(err.response?.data?.email?.[0] || err.response?.data?.error || '发送失败')
|
||||||
err.response?.data?.email?.[0] ||
|
|
||||||
err.response?.data?.detail ||
|
|
||||||
'发送失败,请检查邮箱是否正确'
|
|
||||||
console.error('SendCode Error:', err.response?.data)
|
|
||||||
ElMessage.error(errorMsg)
|
|
||||||
} finally {
|
} finally {
|
||||||
sendingCode.value = false
|
sendingCode.value = false
|
||||||
}
|
}
|
||||||
|
|
@ -182,43 +219,63 @@ async function handleSendCode() {
|
||||||
|
|
||||||
// 重新发送验证码
|
// 重新发送验证码
|
||||||
async function handleResendCode() {
|
async function handleResendCode() {
|
||||||
seekerForm.code = ''
|
codeForm.code = ''
|
||||||
await handleSendCode()
|
await handleSendCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 求职者登入
|
// 密码登入
|
||||||
async function handleSeekerLogin() {
|
async function handlePasswordLogin() {
|
||||||
if (!seekerForm.code || seekerForm.code.length !== 6) {
|
if (!passwordForm.username || !passwordForm.password) {
|
||||||
ElMessage.warning('请输入正确的6位验证码')
|
ElMessage.warning('请输入邮箱/用户名和密码')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
seekerLoading.value = true
|
passwordLoading.value = true
|
||||||
try {
|
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('登录成功')
|
ElMessage.success('登录成功')
|
||||||
const redirect = route.query.redirect
|
const redirect = route.query.redirect
|
||||||
if (redirect) return router.push(redirect)
|
if (redirect) return router.push(redirect)
|
||||||
router.push('/seeker/resume')
|
router.push('/seeker/resume')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMsg = err.response?.data?.non_field_errors?.[0] ||
|
ElMessage.error(err.response?.data?.detail || err.response?.data?.username?.[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分钟后重试')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
} 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
|
adminLoading.value = true
|
||||||
try {
|
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('access_token', data.access)
|
||||||
localStorage.setItem('refresh_token', data.refresh)
|
localStorage.setItem('refresh_token', data.refresh)
|
||||||
await auth.fetchMe()
|
await auth.fetchMe()
|
||||||
|
|
@ -241,7 +301,7 @@ async function handleAdminLogin() {
|
||||||
if (redirect) return router.push(redirect)
|
if (redirect) return router.push(redirect)
|
||||||
router.push('/admin/jobs')
|
router.push('/admin/jobs')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ElMessage.error('用户名或密码错误')
|
ElMessage.error(err.response?.data?.detail || '用户名或密码错误')
|
||||||
} finally {
|
} finally {
|
||||||
adminLoading.value = false
|
adminLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
@ -298,4 +358,40 @@ h2 {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
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%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -3,67 +3,56 @@
|
||||||
<el-card class="auth-card">
|
<el-card class="auth-card">
|
||||||
<h2>求职者注册</h2>
|
<h2>求职者注册</h2>
|
||||||
<el-form :model="form" @submit.prevent="handleRegister">
|
<el-form :model="form" @submit.prevent="handleRegister">
|
||||||
<!-- 用户名、邮箱、手机号 -->
|
<!-- 邮箱 -->
|
||||||
<el-form-item v-if="!codeSent">
|
<el-form-item>
|
||||||
<el-input
|
|
||||||
v-model="form.username"
|
|
||||||
placeholder="请输入用户名"
|
|
||||||
:disabled="codeSent"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="!codeSent">
|
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.email"
|
v-model="form.email"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="请输入邮箱"
|
placeholder="请输入邮箱"
|
||||||
:disabled="codeSent"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-if="!codeSent">
|
|
||||||
|
<!-- 用户名 -->
|
||||||
|
<el-form-item>
|
||||||
|
<el-input
|
||||||
|
v-model="form.username"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 手机号 -->
|
||||||
|
<el-form-item>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.phone"
|
v-model="form.phone"
|
||||||
placeholder="请输入手机号"
|
placeholder="请输入手机号"
|
||||||
:disabled="codeSent"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 获取验证码按钮 -->
|
<!-- 密码 -->
|
||||||
<el-form-item v-if="!codeSent">
|
<el-form-item>
|
||||||
<el-button
|
|
||||||
type="info"
|
|
||||||
@click="handleSendCode"
|
|
||||||
:loading="sendingCode"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
获取验证码
|
|
||||||
</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 验证码输入 -->
|
|
||||||
<el-form-item v-if="codeSent">
|
|
||||||
<div style="display: flex; gap: 8px;">
|
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.code"
|
v-model="form.password"
|
||||||
placeholder="请输入验证码(6位)"
|
type="password"
|
||||||
maxlength="6"
|
placeholder="请输入密码(至少6位)"
|
||||||
style="flex: 1"
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 确认密码 -->
|
||||||
|
<el-form-item>
|
||||||
|
<el-input
|
||||||
|
v-model="form.passwordConfirm"
|
||||||
|
type="password"
|
||||||
|
placeholder="请确认密码"
|
||||||
|
show-password
|
||||||
/>
|
/>
|
||||||
<el-button
|
|
||||||
type="info"
|
|
||||||
@click="handleResendCode"
|
|
||||||
:disabled="codeCountdown > 0"
|
|
||||||
>
|
|
||||||
{{ codeCountdown > 0 ? `${codeCountdown}s` : '重新获取' }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 注册按钮 -->
|
<!-- 注册按钮 -->
|
||||||
<el-button
|
<el-button
|
||||||
v-if="codeSent"
|
|
||||||
type="primary"
|
type="primary"
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
:loading="registering"
|
:loading="loading"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
注册
|
注册
|
||||||
|
|
@ -82,68 +71,43 @@ import { reactive, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { sendCode } from '@/api/auth'
|
|
||||||
|
|
||||||
const form = reactive({ username: '', email: '', phone: '', code: '' })
|
const form = reactive({
|
||||||
const codeSent = ref(false)
|
email: '',
|
||||||
const sendingCode = ref(false)
|
username: '',
|
||||||
const registering = ref(false)
|
phone: '',
|
||||||
const codeCountdown = ref(0)
|
password: '',
|
||||||
|
passwordConfirm: '',
|
||||||
|
})
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
|
||||||
// 倒计时逻辑
|
async function handleRegister() {
|
||||||
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
|
|
||||||
}
|
|
||||||
if (!form.email) {
|
if (!form.email) {
|
||||||
ElMessage.warning('请输入邮箱')
|
ElMessage.warning('请输入邮箱')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!form.username) {
|
||||||
|
ElMessage.warning('请输入用户名')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!form.phone) {
|
if (!form.phone) {
|
||||||
ElMessage.warning('请输入手机号')
|
ElMessage.warning('请输入手机号')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!form.password || form.password.length < 6) {
|
||||||
sendingCode.value = true
|
ElMessage.warning('请输入至少6位密码')
|
||||||
try {
|
return
|
||||||
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.passwordConfirm) {
|
||||||
|
ElMessage.warning('两次输入的密码不一致')
|
||||||
// 重新发送验证码
|
|
||||||
async function handleResendCode() {
|
|
||||||
form.code = ''
|
|
||||||
await handleSendCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册
|
|
||||||
async function handleRegister() {
|
|
||||||
if (!form.code || form.code.length !== 6) {
|
|
||||||
ElMessage.warning('请输入正确的6位验证码')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
registering.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/auth/register/', {
|
const response = await fetch('/api/auth/register/', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -151,16 +115,16 @@ async function handleRegister() {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
username: form.username,
|
|
||||||
email: form.email,
|
email: form.email,
|
||||||
|
username: form.username,
|
||||||
phone: form.phone,
|
phone: form.phone,
|
||||||
code: form.code,
|
password: form.password,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const data = await response.json()
|
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)
|
ElMessage.error(errorMsg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -177,7 +141,7 @@ async function handleRegister() {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ElMessage.error('注册失败,请稍后重试')
|
ElMessage.error('注册失败,请稍后重试')
|
||||||
} finally {
|
} finally {
|
||||||
registering.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,244 @@
|
||||||
|
<template>
|
||||||
|
<div class="auth-page">
|
||||||
|
<el-card class="auth-card">
|
||||||
|
<h2>重置密码</h2>
|
||||||
|
|
||||||
|
<el-form @submit.prevent="handleResetPassword">
|
||||||
|
<!-- 第一步:邮箱验证 -->
|
||||||
|
<div v-if="step === 1">
|
||||||
|
<el-form-item>
|
||||||
|
<el-input
|
||||||
|
v-model="form.email"
|
||||||
|
type="email"
|
||||||
|
placeholder="请输入邮箱"
|
||||||
|
:disabled="codeSent"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="!codeSent">
|
||||||
|
<el-button
|
||||||
|
type="info"
|
||||||
|
@click="handleSendCode"
|
||||||
|
:loading="sendingCode"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
获取验证码
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="codeSent">
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
|
<el-input
|
||||||
|
v-model="form.code"
|
||||||
|
placeholder="请输入验证码(6位)"
|
||||||
|
maxlength="6"
|
||||||
|
style="flex: 1"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="info"
|
||||||
|
@click="handleResendCode"
|
||||||
|
:disabled="codeCountdown > 0"
|
||||||
|
>
|
||||||
|
{{ codeCountdown > 0 ? `${codeCountdown}s` : '重新获取' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
v-if="codeSent"
|
||||||
|
type="primary"
|
||||||
|
@click="goToStep2"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
下一步
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第二步:重置密码 -->
|
||||||
|
<div v-if="step === 2">
|
||||||
|
<div style="margin-bottom: 16px; padding: 8px 12px; background: #f0f9ff; border-radius: 4px; color: #606266; font-size: 12px;">
|
||||||
|
邮箱:<span style="font-weight: 600;">{{ form.email }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-input
|
||||||
|
v-model="form.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入新密码(至少6位)"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-input
|
||||||
|
v-model="form.passwordConfirm"
|
||||||
|
type="password"
|
||||||
|
placeholder="请确认新密码"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
native-type="submit"
|
||||||
|
:loading="resetLoading"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
重置密码
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
@click="goBackToStep1"
|
||||||
|
style="width: 100%; margin-top: 8px;"
|
||||||
|
>
|
||||||
|
上一步
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div style="margin-top: 12px; text-align: center; font-size: 12px;">
|
||||||
|
<router-link to="/login" style="color: #409eff; text-decoration: none;">返回登录</router-link>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const step = ref(1)
|
||||||
|
const form = reactive({
|
||||||
|
email: '',
|
||||||
|
code: '',
|
||||||
|
password: '',
|
||||||
|
passwordConfirm: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const codeSent = ref(false)
|
||||||
|
const sendingCode = ref(false)
|
||||||
|
const resetLoading = ref(false)
|
||||||
|
const codeCountdown = ref(0)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 倒计时
|
||||||
|
const startCountdown = () => {
|
||||||
|
codeCountdown.value = 60
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
codeCountdown.value--
|
||||||
|
if (codeCountdown.value <= 0) clearInterval(interval)
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送验证码
|
||||||
|
async function handleSendCode() {
|
||||||
|
if (!form.email) {
|
||||||
|
ElMessage.warning('请输入邮箱')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sendingCode.value = true
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/reset-password/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email: form.email }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
ElMessage.error(data.email?.[0] || '发送失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success('验证码已发送到您的邮箱')
|
||||||
|
codeSent.value = true
|
||||||
|
startCountdown()
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error('发送失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
sendingCode.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新发送验证码
|
||||||
|
async function handleResendCode() {
|
||||||
|
form.code = ''
|
||||||
|
await handleSendCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下一步
|
||||||
|
function goToStep2() {
|
||||||
|
if (!form.code || form.code.length !== 6) {
|
||||||
|
ElMessage.warning('请输入正确的6位验证码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
step.value = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上一步
|
||||||
|
function goBackToStep1() {
|
||||||
|
step.value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置密码
|
||||||
|
async function handleResetPassword() {
|
||||||
|
if (!form.password || form.password.length < 6) {
|
||||||
|
ElMessage.warning('请输入至少6位密码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (form.password !== form.passwordConfirm) {
|
||||||
|
ElMessage.warning('两次输入的密码不一致')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resetLoading.value = true
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/confirm-reset-password/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: form.email,
|
||||||
|
code: form.code,
|
||||||
|
new_password: form.password,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
const errorMsg = data.code?.[0] || data.new_password?.[0] || '重置失败'
|
||||||
|
ElMessage.error(errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success('密码重置成功,请使用新密码登入')
|
||||||
|
router.push('/login')
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error('密码重置失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
resetLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.auth-page {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 60vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
width: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue