Files
happy-life-star/web-new/src/views/auth/Login.vue
T

296 lines
7.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="login-form">
<!-- 标题 -->
<div class="text-center mb-6">
<h2 class="text-2xl font-bold text-gray-900 mb-2">欢迎回来</h2>
<p class="text-gray-600">登录您的账户继续使用</p>
</div>
<!-- 登录表单 -->
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
size="large"
@submit.prevent="handleLogin"
>
<!-- 用户名 -->
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名/邮箱/手机号"
clearable
:prefix-icon="User"
@keyup.enter="handleLogin"
/>
</el-form-item>
<!-- 密码 -->
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
show-password
clearable
:prefix-icon="Lock"
@keyup.enter="handleLogin"
/>
</el-form-item>
<!-- 验证码 -->
<el-form-item v-if="showCaptcha" prop="captcha">
<div class="flex space-x-2">
<el-input
v-model="loginForm.captcha"
placeholder="请输入验证码"
clearable
class="flex-1"
@keyup.enter="handleLogin"
/>
<div
class="w-24 h-10 bg-gray-100 rounded cursor-pointer flex items-center justify-center"
@click="refreshCaptcha"
>
<img
v-if="captchaImage"
:src="captchaImage"
alt="验证码"
class="w-full h-full object-contain"
/>
<span v-else class="text-xs text-gray-500">点击刷新</span>
</div>
</div>
</el-form-item>
<!-- 记住我和忘记密码 -->
<div class="flex items-center justify-between mb-6">
<el-checkbox v-model="loginForm.rememberMe">
记住我
</el-checkbox>
<el-link type="primary" @click="showForgotPassword">
忘记密码
</el-link>
</div>
<!-- 登录按钮 -->
<el-form-item>
<el-button
type="primary"
size="large"
class="w-full"
:loading="isLoggingIn"
@click="handleLogin"
>
{{ isLoggingIn ? '登录中...' : '登录' }}
</el-button>
</el-form-item>
</el-form>
<!-- 第三方登录 -->
<div class="mt-6">
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-white text-gray-500">或使用以下方式登录</span>
</div>
</div>
<div class="mt-6 grid grid-cols-3 gap-3">
<el-button
v-for="provider in oauthProviders"
:key="provider.name"
class="oauth-button"
@click="handleOAuthLogin(provider.name)"
>
<el-icon :size="20">
<component :is="provider.icon" />
</el-icon>
</el-button>
</div>
</div>
<!-- 注册链接 -->
<div class="text-center mt-6">
<span class="text-gray-600">还没有账户</span>
<router-link to="/auth/register" class="text-blue-600 hover:text-blue-500 ml-1">
立即注册
</router-link>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { User, Lock, ChatDotRound, Share, Link } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import { useAuthStore } from '@/stores/auth'
import { authApi } from '@/api/auth'
import type { LoginRequest } from '@/types/api'
import { validateUsername, validatePassword } from '@/utils/validation'
// 状态管理
const authStore = useAuthStore()
// 响应式数据
const loginFormRef = ref<FormInstance>()
const isLoggingIn = computed(() => authStore.isLoggingIn)
const showCaptcha = ref(false)
const captchaImage = ref('')
const captchaId = ref('')
// 登录表单
const loginForm = reactive<LoginRequest>({
username: '',
password: '',
captcha: '',
captchaId: '',
rememberMe: false
})
// 表单验证规则
const loginRules: FormRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 50, message: '用户名长度在 3 到 50 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
],
captcha: [
{
required: true,
message: '请输入验证码',
trigger: 'blur',
validator: (rule: any, value: string, callback: any) => {
if (showCaptcha.value && !value) {
callback(new Error('请输入验证码'))
} else {
callback()
}
}
}
]
}
// 第三方登录提供商
const oauthProviders = [
{ name: 'wechat', icon: ChatDotRound, title: '微信' },
{ name: 'qq', icon: Share, title: 'QQ' },
{ name: 'github', icon: Link, title: 'GitHub' }
]
// 方法
const handleLogin = async () => {
if (!loginFormRef.value) return
try {
await loginFormRef.value.validate()
// 设置验证码ID
if (showCaptcha.value) {
loginForm.captchaId = captchaId.value
}
await authStore.login(loginForm)
} catch (error: any) {
console.error('登录失败:', error)
// 如果是验证码错误,刷新验证码
if (error.message?.includes('验证码')) {
await refreshCaptcha()
}
// 连续登录失败后显示验证码
if (!showCaptcha.value && error.code === 'LOGIN_FAILED_TOO_MANY') {
showCaptcha.value = true
await refreshCaptcha()
}
}
}
const refreshCaptcha = async () => {
try {
const response = await authApi.getCaptcha()
captchaImage.value = response.captchaImage
captchaId.value = response.captchaId
loginForm.captcha = ''
} catch (error) {
console.error('获取验证码失败:', error)
ElMessage.error('获取验证码失败,请稍后重试')
}
}
const handleOAuthLogin = async (provider: string) => {
try {
ElMessage.info(`${provider} 登录功能开发中...`)
// 这里实现第三方登录逻辑
// 1. 跳转到第三方授权页面
// 2. 获取授权码
// 3. 调用后端接口完成登录
} catch (error) {
console.error('第三方登录失败:', error)
ElMessage.error('第三方登录失败,请稍后重试')
}
}
const showForgotPassword = () => {
ElMessage.info('忘记密码功能开发中...')
}
// 生命周期
onMounted(() => {
// 如果已登录,跳转到首页
if (authStore.isLoggedIn) {
router.push('/home')
return
}
// 检查是否需要显示验证码
const loginAttempts = localStorage.getItem('login_attempts')
if (loginAttempts && parseInt(loginAttempts) >= 3) {
showCaptcha.value = true
refreshCaptcha()
}
})
// 监听登录失败次数
let loginFailCount = 0
watch(() => authStore.isLoggingIn, (isLogging) => {
if (!isLogging && !authStore.isLoggedIn) {
loginFailCount++
localStorage.setItem('login_attempts', loginFailCount.toString())
// 失败3次后显示验证码
if (loginFailCount >= 3 && !showCaptcha.value) {
showCaptcha.value = true
refreshCaptcha()
}
}
})
</script>
<style scoped>
.oauth-button {
@apply w-full h-12 border border-gray-300 rounded-lg hover:border-gray-400 transition-colors;
}
.oauth-button:hover {
@apply bg-gray-50;
}
:deep(.el-input__inner) {
@apply h-12;
}
:deep(.el-button--large) {
@apply h-12;
}
</style>