319 lines
7.1 KiB
Vue
Raw Normal View History

2025-08-10 22:42:56 +08:00
<template>
<div class="login-page">
<div class="login-container">
<div class="login-form">
<div class="form-header">
<h1>登录</h1>
<p>欢迎回到在线学习平台</p>
</div>
<n-form
ref="formRef"
:model="formData"
:rules="rules"
size="large"
@submit.prevent="handleSubmit"
>
<n-form-item path="email" label="邮箱">
<n-input
v-model:value="formData.email"
placeholder="请输入邮箱地址"
type="email"
>
<template #prefix>
<n-icon>
<MailOutline />
</n-icon>
</template>
</n-input>
</n-form-item>
<n-form-item path="password" label="密码">
<n-input
v-model:value="formData.password"
placeholder="请输入密码"
type="password"
show-password-on="mousedown"
>
<template #prefix>
<n-icon>
<LockClosedOutline />
</n-icon>
</template>
</n-input>
</n-form-item>
<n-form-item>
<div class="form-options">
<n-checkbox v-model:checked="rememberMe">
记住我
</n-checkbox>
<n-button text type="primary">
忘记密码
</n-button>
</div>
</n-form-item>
<n-form-item>
<n-button
type="primary"
size="large"
block
:loading="userStore.isLoading"
attr-type="submit"
>
登录
</n-button>
</n-form-item>
</n-form>
<div class="form-footer">
<p>
还没有账号
<n-button text type="primary" @click="$router.push('/register')">
立即注册
</n-button>
</p>
</div>
<!-- 社交登录 -->
<div class="social-login">
<n-divider>或使用以下方式登录</n-divider>
<n-space justify="center">
<n-button circle size="large">
<n-icon size="20">
<LogoGithub />
</n-icon>
</n-button>
<n-button circle size="large">
<n-icon size="20">
<LogoGoogle />
</n-icon>
</n-button>
<n-button circle size="large">
<n-icon size="20">
<LogoWechat />
</n-icon>
</n-button>
</n-space>
</div>
</div>
<!-- 侧边图片 -->
<div class="login-image">
<PlaceholderImage
:width="600"
:height="800"
text="登录背景图"
icon="🎨"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { useMessage, type FormInst, type FormRules } from 'naive-ui'
import { useUserStore } from '@/stores/user'
import PlaceholderImage from '@/components/common/PlaceholderImage.vue'
import {
MailOutline,
LockClosedOutline,
LogoGithub,
LogoGoogle,
LogoWechat
} from '@vicons/ionicons5'
import { AuthApi } from '@/api'
const router = useRouter()
const message = useMessage()
const userStore = useUserStore()
const formRef = ref<FormInst | null>(null)
const rememberMe = ref(false)
// 表单数据
const formData = reactive({
email: '',
password: ''
})
// 表单验证规则
const rules: FormRules = {
email: [
{
required: true,
message: '请输入邮箱地址',
trigger: ['input', 'blur']
},
{
type: 'email',
message: '请输入有效的邮箱地址',
trigger: ['input', 'blur']
}
],
password: [
{
required: true,
message: '请输入密码',
trigger: ['input', 'blur']
},
{
min: 3,
message: '密码长度不能少于3位',
trigger: ['input', 'blur']
}
]
}
// 处理表单提交
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
// 显示加载状态
userStore.isLoading = true
// 调用登录API
const response = await AuthApi.login({
email: formData.email,
password: formData.password
})
if (response.code === 200) {
const { user, token, refreshToken } = response.data
// 保存用户信息和token到store
userStore.user = user
userStore.token = token
// 保存到本地存储
localStorage.setItem('token', token)
localStorage.setItem('refreshToken', refreshToken)
localStorage.setItem('user', JSON.stringify(user))
// 如果选择了记住我,设置更长的过期时间
if (rememberMe.value) {
localStorage.setItem('rememberMe', 'true')
}
message.success('登录成功!')
// 登录成功后跳转到首页或之前的页面
const redirect = router.currentRoute.value.query.redirect as string
router.push(redirect || '/')
} else {
message.error(response.message || '登录失败')
}
} catch (error: any) {
console.error('登录失败:', error)
// 处理不同类型的错误
if (error.response?.status === 401) {
message.error('邮箱或密码错误')
} else if (error.response?.status === 429) {
message.error('登录尝试过于频繁,请稍后再试')
} else if (error.response?.data?.message) {
message.error(error.response.data.message)
} else {
message.error('网络错误,请检查网络连接')
}
} finally {
userStore.isLoading = false
}
}
</script>
<style scoped>
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
.login-container {
display: grid;
grid-template-columns: 1fr 1fr;
max-width: 1000px;
width: 100%;
background: white;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
.login-form {
padding: 60px 40px;
display: flex;
flex-direction: column;
justify-content: center;
}
.form-header {
text-align: center;
margin-bottom: 40px;
}
.form-header h1 {
font-size: 2rem;
color: #333;
margin-bottom: 8px;
}
.form-header p {
color: #666;
font-size: 1rem;
}
.form-options {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.form-footer {
text-align: center;
margin-top: 24px;
}
.social-login {
margin-top: 32px;
}
.login-image {
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
}
.login-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
@media (max-width: 768px) {
.login-container {
grid-template-columns: 1fr;
max-width: 400px;
}
.login-image {
display: none;
}
.login-form {
padding: 40px 24px;
}
}
</style>