feat: 登入页面支持求职者和管理员两种角色
前端改动: - LoginView.vue 添加角色选择(求职者/管理员) - 求职者使用邮箱验证码登入 - 管理员使用用户名密码登入 - 两种方式在同一页面,通过角色选项卡切换 后端改动: - CustomTokenObtainPairView 改为支持两种登入方式 - 若提供 email+code 则使用邮箱验证码登入 - 若提供 username+password 则使用用户名密码登入 设计: - 求职者可自助注册和邮箱验证码登入 - 管理员由 superadmin 创建,使用用户名密码登入 - 两种登入都返回同样的 JWT token Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
99220b6daf
commit
12697c5750
|
|
@ -50,11 +50,25 @@ class SendCodeView(APIView):
|
|||
|
||||
|
||||
class CustomTokenObtainPairView(TokenObtainPairView):
|
||||
"""自定义邮箱验证码登入视图"""
|
||||
serializer_class = LoginSerializer
|
||||
"""自定义登入视图,支持邮箱验证码和用户名密码两种方式"""
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
# 判断是否是邮箱验证码登入(有 email 和 code)还是用户名密码登入(有 username 和 password)
|
||||
if 'email' in request.data and 'code' in request.data:
|
||||
# 邮箱验证码登入
|
||||
return self._login_with_code(request)
|
||||
elif 'username' in request.data and 'password' in request.data:
|
||||
# 用户名密码登入
|
||||
return self._login_with_password(request)
|
||||
else:
|
||||
return Response(
|
||||
{'error': '请提供正确的登入方式'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
def _login_with_code(self, request):
|
||||
"""邮箱验证码登入"""
|
||||
serializer = LoginSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
user = serializer.validated_data['user']
|
||||
|
|
@ -64,17 +78,19 @@ class CustomTokenObtainPairView(TokenObtainPairView):
|
|||
vc.mark_as_verified()
|
||||
|
||||
# 生成 JWT token
|
||||
refresh = self.get_token(user)
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
refresh = RefreshToken.for_user(user)
|
||||
return Response({
|
||||
'refresh': str(refresh),
|
||||
'access': str(refresh.access_token),
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
@classmethod
|
||||
def get_token(cls, user):
|
||||
"""使用 TokenObtainPairSerializer 生成 token"""
|
||||
token = super().get_token(user)
|
||||
return token
|
||||
def _login_with_password(self, request):
|
||||
"""用户名密码登入"""
|
||||
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||
serializer = TokenObtainPairSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return Response(serializer.validated_data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class RegisterView(APIView):
|
||||
|
|
|
|||
|
|
@ -1,12 +1,30 @@
|
|||
<template>
|
||||
<div class="auth-page">
|
||||
<el-card class="auth-card">
|
||||
<h2>邮箱登录</h2>
|
||||
<el-form :model="form" @submit.prevent="handleLogin">
|
||||
<h2>登录</h2>
|
||||
|
||||
<!-- 角色选择 -->
|
||||
<div class="role-tabs">
|
||||
<button
|
||||
:class="['role-tab', { active: role === 'seeker' }]"
|
||||
@click="role = 'seeker'"
|
||||
>
|
||||
求职者
|
||||
</button>
|
||||
<button
|
||||
:class="['role-tab', { active: role === 'admin' }]"
|
||||
@click="role = 'admin'"
|
||||
>
|
||||
管理员
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 求职者登入(邮箱验证码) -->
|
||||
<el-form v-if="role === 'seeker'" :model="seekerForm" @submit.prevent="handleSeekerLogin">
|
||||
<!-- 邮箱输入 -->
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="form.email"
|
||||
v-model="seekerForm.email"
|
||||
placeholder="请输入邮箱"
|
||||
type="email"
|
||||
:disabled="codeSent"
|
||||
|
|
@ -29,7 +47,7 @@
|
|||
<el-form-item v-if="codeSent">
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<el-input
|
||||
v-model="form.code"
|
||||
v-model="seekerForm.code"
|
||||
placeholder="请输入验证码(6位)"
|
||||
maxlength="6"
|
||||
style="flex: 1"
|
||||
|
|
@ -44,7 +62,7 @@
|
|||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 错误提示和重试次数 -->
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="codeSent && attemptsLeft < 5" style="margin-bottom: 12px; text-align: center; color: #f56c6c; font-size: 12px;">
|
||||
还有 {{ attemptsLeft }} 次重试机会
|
||||
</div>
|
||||
|
|
@ -57,7 +75,7 @@
|
|||
v-if="codeSent"
|
||||
type="primary"
|
||||
native-type="submit"
|
||||
:loading="logging"
|
||||
:loading="seekerLoading"
|
||||
style="width: 100%"
|
||||
:disabled="isLocked"
|
||||
>
|
||||
|
|
@ -65,8 +83,36 @@
|
|||
</el-button>
|
||||
</el-form>
|
||||
|
||||
<div style="margin-top: 12px; text-align: center; font-size: 12px; color: #999;">
|
||||
需要帮助?<a href="#" style="color: #409eff; text-decoration: none;">联系支持</a>
|
||||
<!-- 管理员登入(用户名密码) -->
|
||||
<el-form v-if="role === 'admin'" :model="adminForm" @submit.prevent="handleAdminLogin">
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="adminForm.username"
|
||||
placeholder="用户名"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="adminForm.password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
native-type="submit"
|
||||
:loading="adminLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
登录
|
||||
</el-button>
|
||||
</el-form>
|
||||
|
||||
<div style="margin-top: 12px; text-align: center; font-size: 12px;">
|
||||
<span v-if="role === 'seeker'">
|
||||
没有账号?<router-link to="/register" style="color: #409eff; text-decoration: none;">立即注册</router-link>
|
||||
</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
|
@ -79,15 +125,21 @@ import { useAuthStore } from '@/stores/auth'
|
|||
import { ElMessage } from 'element-plus'
|
||||
import { sendCode, loginApi } from '@/api/auth'
|
||||
|
||||
const form = reactive({ email: '', code: '' })
|
||||
const loading = ref(false)
|
||||
const role = ref('seeker')
|
||||
|
||||
// 求职者表单
|
||||
const seekerForm = reactive({ email: '', code: '' })
|
||||
const codeSent = ref(false)
|
||||
const sendingCode = ref(false)
|
||||
const logging = ref(false)
|
||||
const seekerLoading = ref(false)
|
||||
const codeCountdown = ref(0)
|
||||
const attemptsLeft = ref(5)
|
||||
const isLocked = ref(false)
|
||||
|
||||
// 管理员表单
|
||||
const adminForm = reactive({ username: '', password: '' })
|
||||
const adminLoading = ref(false)
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const auth = useAuthStore()
|
||||
|
|
@ -103,14 +155,14 @@ const startCountdown = () => {
|
|||
|
||||
// 发送验证码
|
||||
async function handleSendCode() {
|
||||
if (!form.email) {
|
||||
if (!seekerForm.email) {
|
||||
ElMessage.warning('请输入邮箱')
|
||||
return
|
||||
}
|
||||
|
||||
sendingCode.value = true
|
||||
try {
|
||||
await sendCode(form.email)
|
||||
await sendCode(seekerForm.email)
|
||||
ElMessage.success('验证码已发送到您的邮箱')
|
||||
codeSent.value = true
|
||||
attemptsLeft.value = 5
|
||||
|
|
@ -125,25 +177,24 @@ async function handleSendCode() {
|
|||
|
||||
// 重新发送验证码
|
||||
async function handleResendCode() {
|
||||
form.code = ''
|
||||
seekerForm.code = ''
|
||||
await handleSendCode()
|
||||
}
|
||||
|
||||
// 登录
|
||||
async function handleLogin() {
|
||||
if (!form.code || form.code.length !== 6) {
|
||||
// 求职者登入
|
||||
async function handleSeekerLogin() {
|
||||
if (!seekerForm.code || seekerForm.code.length !== 6) {
|
||||
ElMessage.warning('请输入正确的6位验证码')
|
||||
return
|
||||
}
|
||||
|
||||
logging.value = true
|
||||
seekerLoading.value = true
|
||||
try {
|
||||
await auth.login(form.email, form.code)
|
||||
await auth.login(seekerForm.email, seekerForm.code)
|
||||
ElMessage.success('登录成功')
|
||||
const redirect = route.query.redirect
|
||||
if (redirect) return router.push(redirect)
|
||||
if (auth.isSeeker) router.push('/seeker/resume')
|
||||
else router.push('/admin/jobs')
|
||||
router.push('/seeker/resume')
|
||||
} catch (err) {
|
||||
const errorMsg = err.response?.data?.non_field_errors?.[0] ||
|
||||
err.response?.data?.code?.[0] ||
|
||||
|
|
@ -162,7 +213,32 @@ async function handleLogin() {
|
|||
}
|
||||
}
|
||||
} finally {
|
||||
logging.value = false
|
||||
seekerLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 管理员登入
|
||||
async function handleAdminLogin() {
|
||||
if (!adminForm.username || !adminForm.password) {
|
||||
ElMessage.warning('请输入用户名和密码')
|
||||
return
|
||||
}
|
||||
|
||||
adminLoading.value = true
|
||||
try {
|
||||
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()
|
||||
|
||||
ElMessage.success('登录成功')
|
||||
const redirect = route.query.redirect
|
||||
if (redirect) return router.push(redirect)
|
||||
router.push('/admin/jobs')
|
||||
} catch (err) {
|
||||
ElMessage.error('用户名或密码错误')
|
||||
} finally {
|
||||
adminLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -181,8 +257,40 @@ async function handleLogin() {
|
|||
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.role-tabs {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
background: #f5f7fa;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.role-tab {
|
||||
flex: 1;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.role-tab:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.role-tab.active {
|
||||
background: #fff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue