347 lines
8.5 KiB
Vue
347 lines
8.5 KiB
Vue
<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.token = token
|
||
localStorage.setItem('X-Access-Token', token)
|
||
localStorage.setItem('token', token)
|
||
localStorage.setItem('refreshToken', refreshToken)
|
||
|
||
// 如果选择了记住我,设置更长的过期时间
|
||
if (rememberMe.value) {
|
||
localStorage.setItem('rememberMe', 'true')
|
||
}
|
||
|
||
try {
|
||
// 登录成功后立即调用用户信息接口获取完整的用户信息
|
||
console.log('🔍 登录成功,正在获取用户信息...')
|
||
const userInfoResponse = await AuthApi.getUserInfo()
|
||
|
||
if (userInfoResponse.success && userInfoResponse.result) {
|
||
// 将后端用户信息转换为前端格式
|
||
const convertedUser = AuthApi.convertBackendUserToUser(userInfoResponse.result)
|
||
|
||
console.log('🔍 转换后的用户信息:', convertedUser)
|
||
console.log('🔍 用户真实姓名:', convertedUser.profile?.realName)
|
||
console.log('🔍 用户头像:', convertedUser.avatar)
|
||
|
||
// 保存转换后的用户信息
|
||
userStore.user = convertedUser
|
||
localStorage.setItem('user', JSON.stringify(convertedUser))
|
||
|
||
console.log('✅ 用户信息获取成功并保存到store:', userStore.user)
|
||
} else {
|
||
// 如果获取用户信息失败,使用登录接口返回的基本用户信息
|
||
console.warn('⚠️ 获取用户信息失败,使用登录返回的基本信息')
|
||
userStore.user = user
|
||
localStorage.setItem('user', JSON.stringify(user))
|
||
}
|
||
} catch (userInfoError) {
|
||
// 如果获取用户信息失败,使用登录接口返回的基本用户信息
|
||
console.warn('⚠️ 获取用户信息异常,使用登录返回的基本信息:', userInfoError)
|
||
userStore.user = user
|
||
localStorage.setItem('user', JSON.stringify(user))
|
||
}
|
||
|
||
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>
|