2025-09-11 23:13:47 +08:00

1004 lines
25 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-page">
<!-- 背景图片 -->
<div class="background-image">
<img src="/images/loginImage/backImage.png" alt="登录背景" />
</div>
<!-- 左上角logo -->
<div class="top-logo">
<img src="/images/loginImage/logo.png" alt="云岭智教" />
</div>
<!-- 右侧登录区域 -->
<div class="login-area">
<!-- 学员端/教师端切换 -->
<div class="user-type-tabs">
<n-button
:type="activeTab === 'student' ? 'primary' : 'default'"
text
@click="activeTab = 'student'"
class="type-tab"
:class="{ active: activeTab === 'student' }"
>
学员端
</n-button>
<n-button
:type="activeTab === 'teacher' ? 'primary' : 'default'"
text
@click="activeTab = 'teacher'"
class="type-tab"
:class="{ active: activeTab === 'teacher' }"
>
教师端
</n-button>
</div>
<!-- 登录表单 -->
<div class="login-form">
<div class="form-header">
<h2>{{ isRegisterMode ? '账号注册' : '账号密码登录' }}</h2>
</div>
<n-form
ref="formRef"
:model="formData"
:rules="rules"
size="large"
@submit.prevent="handleSubmit"
>
<n-form-item path="studentId">
<template #label>
<div class="student-label-container">
<span class="label-text">{{ getIdLabel() }}</span>
<div v-if="!isRegisterMode" class="input-hint-right">
没有账号<n-button text type="primary" size="small" @click="isRegisterMode = true">立即注册</n-button>
</div>
</div>
</template>
<n-input
v-model:value="formData.studentId"
:placeholder="getIdPlaceholder()"
class="form-input"
/>
</n-form-item>
<!-- 邀请码输入框仅在注册模式显示 -->
<n-form-item v-if="isRegisterMode" path="inviteCode" label="邀请码">
<n-input
v-model:value="formData.inviteCode"
placeholder="请输入邀请码"
class="form-input"
/>
</n-form-item>
<n-form-item path="password" label="密码">
<n-input
v-model:value="formData.password"
placeholder="请输入密码"
type="password"
show-password-on="click"
class="form-input"
/>
</n-form-item>
<!-- 登录模式显示记住我和忘记密码 -->
<div v-if="!isRegisterMode" class="form-options">
<n-checkbox v-model:checked="rememberMe" size="small">
下次自动登录
</n-checkbox>
<n-button text type="primary" size="small">
忘记密码
</n-button>
</div>
<!-- 注册模式显示密码要求提示 -->
<div v-if="isRegisterMode" class="password-hint">
8-12位字符需同时包含数字/字母/符号
</div>
<n-form-item>
<n-button
type="primary"
size="large"
block
:loading="userStore.isLoading"
attr-type="submit"
class="login-btn"
>
{{ isRegisterMode ? '注册' : '登录' }}
</n-button>
</n-form-item>
</n-form>
<div class="form-footer">
<p class="agreement-text">
{{ isRegisterMode ? '登录即代表阅读并同意' : '登录即同意我们的用户协议' }}
<n-button text type="primary" size="small" @click="goToServiceAgreement">服务协议和隐私政策</n-button>
</p>
</div>
</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 { AuthApi } from '@/api'
const router = useRouter()
const message = useMessage()
const userStore = useUserStore()
const formRef = ref<FormInst | null>(null)
const rememberMe = ref(false)
const activeTab = ref('student') // 当前选中的标签页
const isRegisterMode = ref(false) // 注册模式状态
// 表单数据
const formData = reactive({
studentId: '',
password: '',
inviteCode: '' // 添加邀请码字段
})
// 表单验证规则
const rules: FormRules = {
studentId: [
{
required: true,
message: () => {
if (isRegisterMode.value) return '请输入工号'
return activeTab.value === 'teacher' ? '请输入工号' : '请输入学号'
},
trigger: ['input', 'blur']
}
],
inviteCode: [
{
required: true,
message: '请输入邀请码',
trigger: ['input', 'blur']
}
],
password: [
{
required: true,
message: '请输入密码',
trigger: ['input', 'blur']
},
{
min: isRegisterMode.value ? 8 : 3,
max: isRegisterMode.value ? 12 : undefined,
message: isRegisterMode.value ? '密码长度需8-12位字符' : '密码长度不能少于3位',
trigger: ['input', 'blur']
},
{
pattern: isRegisterMode.value ? /^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]).*$/ : undefined,
message: '密码需同时包含数字/字母/符号',
trigger: ['input', 'blur']
}
]
}
// 获取ID标签文字
const getIdLabel = () => {
if (isRegisterMode.value) {
return '工号' // 注册模式统一显示工号
}
return activeTab.value === 'teacher' ? '工号' : '学号'
}
// 获取ID输入框占位符
const getIdPlaceholder = () => {
if (isRegisterMode.value) {
return '请输入您的工号'
}
return activeTab.value === 'teacher' ? '请输入您的工号' : '请输入您的学号'
}
// 处理表单提交
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
// 显示加载状态
userStore.isLoading = true
if (isRegisterMode.value) {
// 注册逻辑
await handleRegister()
} else {
// 登录逻辑
await handleLogin()
}
} catch (error: any) {
console.error('表单提交失败:', error)
if (error.message) {
message.error(error.message)
}
} finally {
userStore.isLoading = false
}
}
// 处理登录
const handleLogin = async () => {
console.log('🚀 开始登录:', { account: formData.studentId, password: '***' })
console.log('🔍 表单密码长度:', formData.password?.length)
// 判断输入的是手机号还是邮箱
const isPhone = /^[0-9]+$/.test(formData.studentId)
const loginParams = {
...(isPhone ? { phone: formData.studentId } : { email: formData.studentId }),
password: formData.password
}
console.log('🔍 准备发送的登录参数:', loginParams)
// 调用登录API
const response = await AuthApi.login(loginParams)
console.log('✅ 登录响应:', response)
if (response.code === 200 || response.code === 0) {
// 先保存token
const token = response.data?.token
if (token) {
userStore.token = token
localStorage.setItem('X-Access-Token', token)
localStorage.setItem('token', token)
console.log('✅ Token已保存:', token)
}
// 如果选择了记住我,设置更长的过期时间
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('⚠️ 获取用户信息失败,使用基本信息')
const basicUser = {
id: 1,
email: isPhone ? '' : formData.studentId,
phone: isPhone ? formData.studentId : '',
username: formData.studentId,
nickname: formData.studentId,
avatar: '',
role: activeTab.value as 'student' | 'teacher',
status: 'active' as const,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
userStore.user = basicUser
localStorage.setItem('user', JSON.stringify(basicUser))
}
} catch (userInfoError) {
// 如果获取用户信息异常,创建基本用户信息
console.warn('⚠️ 获取用户信息异常,使用基本信息:', userInfoError)
const basicUser = {
id: 1,
email: isPhone ? '' : formData.studentId,
phone: isPhone ? formData.studentId : '',
username: formData.studentId,
nickname: formData.studentId,
avatar: '',
role: activeTab.value as 'student' | 'teacher',
status: 'active' as const,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
userStore.user = basicUser
localStorage.setItem('user', JSON.stringify(basicUser))
}
message.success('登录成功!')
console.log('🎉 登录流程完成,用户信息已更新')
// 根据用户类型跳转到不同页面
const redirect = router.currentRoute.value.query.redirect as string
if (activeTab.value === 'teacher') {
console.log('🔄 准备跳转到教师端...')
await router.push(redirect || '/teacher')
} else {
console.log('🔄 准备跳转到首页...')
await router.push(redirect || '/')
}
} else {
console.error('❌ 登录失败,响应码:', response.code)
message.error(response.message || '登录失败')
}
}
// 处理注册
const handleRegister = async () => {
console.log('🚀 开始注册:', { account: formData.studentId, inviteCode: formData.inviteCode })
try {
// 调用注册API
const response = await AuthApi.register({
username: formData.studentId,
email: formData.studentId.includes('@') ? formData.studentId : '',
phone: /^[0-9]+$/.test(formData.studentId) ? formData.studentId : '',
password: formData.password,
confirmPassword: formData.password,
captcha: '', // 暂时不需要验证码
inviteCode: formData.inviteCode
})
console.log('✅ 注册响应:', response)
if (response.code === 200 || response.code === 0) {
message.success('注册成功!请使用您的账号登录')
// 注册成功后切换到登录模式
isRegisterMode.value = false
// 清空邀请码,保留账号和密码方便登录
formData.inviteCode = ''
} else {
message.error(response.message || '注册失败')
}
} catch (error: any) {
console.error('注册失败:', error)
// 处理不同类型的错误
if (error.response?.status === 400) {
message.error('请求参数错误,请检查输入信息')
} else if (error.response?.status === 409) {
message.error('账号已被注册,请使用其他账号')
} else if (error.response?.data?.message) {
message.error(error.response.data.message)
} else if (error.message) {
message.error(error.message)
} else {
message.error('网络错误,请检查网络连接')
}
}
}
// 跳转到服务协议页面
const goToServiceAgreement = () => {
router.push('/service-agreement')
}
</script>
<style scoped>
.login-page {
min-height: 100vh;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 250px; /* 增加右侧边距 */
}
/* 背景图片 - 居中显示,覆盖整个页面 */
.background-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.background-image img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
/* 左上角logo */
.top-logo {
position: absolute;
top: 32px;
left: 48px;
z-index: 10;
}
.top-logo img {
height: 48px;
width: auto;
}
/* 右侧登录区域 */
.login-area {
position: relative;
width: 516px;
min-height: 520px; /* 降低高度 */
z-index: 10;
background: transparent; /* 透明背景 */
display: flex;
flex-direction: column;
justify-content: center;
}
/* 用户类型切换标签 */
.user-type-tabs {
display: flex;
justify-content: center;
gap: 60px; /* 进一步增加按钮间距 */
margin-bottom: 40px;
position: relative;
}
.type-tab {
width: 84px;
height: 40px;
font-family: PingFangSC, PingFang SC;
font-weight: 400; /* 未点击状态字重 */
font-size: 24px;
color: #000000; /* 未点击状态颜色 */
line-height: 40px;
text-align: center;
font-style: normal;
padding: 0;
border: none;
background: transparent;
position: relative;
transition: all 0.3s ease;
cursor: pointer;
}
/* 激活状态样式 */
.type-tab.active {
font-weight: 500; /* 点击状态字重 */
color: #0288D1; /* 点击状态颜色 */
}
/* 激活状态底部横线 */
.type-tab.active::after {
content: '';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 84px; /* 固定宽度84px */
height: 2px; /* 高度2px */
background: #0088D1; /* 使用指定颜色 */
border-radius: 1px;
}
/* 悬停效果 */
.type-tab:hover {
color: #0288D1;
}
/* 登录表单容器 */
.login-form {
background: rgba(255,255,255,0.5);
padding: 40px 35px; /* 减少内边距 */
border-radius: 12px; /* 添加适度的圆角 */
border: 2px solid #FFFFFF;
max-height: 730px; /* 限制最大高度 */
}
/* 表单样式 */
.form-header {
margin-bottom: 32px;
text-align: left;
}
.form-header h2 {
font-size: 20px;
font-weight: 600;
color: #333;
margin: 0;
}
:deep(.n-form-item-label) {
width: auto; /* 改为自动宽度,防止换行 */
min-width: 48px; /* 设置最小宽度 */
height: 32px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 18px; /* 减小字体大小 */
color: #000000;
line-height: 32px;
text-align: left;
font-style: normal;
white-space: nowrap; /* 防止换行 */
}
/* 隐藏必填字段的星号 */
:deep(.n-form-item-label__asterisk) {
display: none;
}
/* 学号标签行样式 */
:deep(.n-input) {
width: 520px;
height: 48x; /* 恢复到64px高度 */
background: #F5F8FB !important; /* 设置背景色 */
border-radius: 2px;
border: none !important; /* 移除边框 */
}
:deep(.n-input:hover) {
border: none !important; /* 悬停时也无边框 */
background: #F5F8FB !important; /* 保持背景色 */
}
:deep(.n-input.n-input--focus) {
border: none !important; /* 聚焦时也无边框 */
box-shadow: none !important; /* 移除聚焦阴影 */
background: #F5F8FB !important; /* 保持背景色 */
}
/* 输入框内部元素样式 */
:deep(.n-input__input-el) {
padding: 12px 16px;
font-size: 14px;
height: 48px; /* 与容器高度一致 */
line-height: 40px; /* 调整行高让文字垂直居中 */
background: transparent !important;
background-color: transparent !important;
}
:deep(.n-input__input) {
background: transparent !important;
background-color: transparent !important;
}
:deep(.n-input-wrapper) {
background: transparent !important;
background-color: transparent !important;
border: none !important;
}
:deep(.n-input__state-border) {
display: none !important; /* 完全隐藏状态边框 */
}
/* 密码输入框特殊处理 */
:deep(.n-input--password) {
border: none !important;
background: #F5F8FB !important;
}
:deep(.n-input--password:hover) {
border: none !important;
background: #F5F8FB !important;
}
:deep(.n-input--password.n-input--focus) {
border: none !important;
box-shadow: none !important;
background: #F5F8FB !important;
}
/* 移除所有可能的边框元素 */
:deep(.n-input__border),
:deep(.n-input__state-border),
:deep(.n-base-suffix__border),
:deep(.n-base-selection__border) {
display: none !important;
border: none !important;
}
.form-input {
margin-bottom: 8px;
}
/* 调整输入框内文字和占位符位置 */
:deep(.n-input .n-input__input-el) {
line-height: 45px !important; /* 使用输入框的高度作为行高 */
padding: 0 16px !important; /* 左右边距 */
height: 45px !important; /* 确保输入框内部高度 */
display: flex !important;
align-items: center !important; /* 垂直居中 */
}
:deep(.n-input .n-input__placeholder) {
line-height: 45px !important; /* 占位符使用相同高度 */
display: flex !important;
align-items: center !important; /* 占位符也垂直居中 */
top: 0 !important; /* 重置顶部位置 */
left: 16px !important; /* 与输入文字对齐 */
}
/* 确保输入框外部容器高度正确 */
:deep(.n-input) {
height: 45px !important; /* 外部容器高度 */
}
:deep(.n-input__input) {
height: 45px !important; /* 输入区域高度 */
min-height: 45px !important;
}
/* 表单项间距调整 */
:deep(.n-form-item) {
margin-bottom: 8px; /* 进一步减少表单项之间的间距 */
}
:deep(.n-form-item-label) {
margin-bottom: 8px; /* 进一步减少标签和输入框之间的间距 */
}
/* 减少表单项内部的默认间距 */
:deep(.n-form-item-blank) {
min-height: auto !important;
padding: 0 !important;
}
/* 减少表单项的整体高度 */
:deep(.n-form-item__feedback-wrapper) {
min-height: auto !important;
margin-top: 4px !important;
}
/* 密码表单项特殊间距 */
:deep(.n-form-item:nth-child(2)) {
margin-top: -6px; /* 减少密码项与上方的距离 */
}
/* 邀请码表单项间距(注册模式下的第二个表单项) */
:deep(.n-form-item:nth-child(2)[path="inviteCode"]) {
margin-top: -6px; /* 减少邀请码与学号的距离 */
}
/* 进一步减少所有表单项的内部间距 */
:deep(.n-form-item-label__text) {
margin-bottom: 0 !important;
}
.input-hint {
height: 28px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 15px;
color: #999999;
line-height: 28px;
font-style: normal;
display: flex;
align-items: center;
justify-content: flex-end;
margin-left: auto; /* 自动推到最右侧 */
flex-shrink: 0; /* 防止被压缩 */
}
/* 学号标签容器 */
.student-label-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
min-width: 400px; /* 确保有足够宽度 */
}
/* 学号标签文字 */
.label-text {
width: 48px;
height: 32px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 18px;
color: #000000;
line-height: 32px;
text-align: left;
font-style: normal;
flex-shrink: 0;
}
/* 右侧注册提示 */
.input-hint-right {
height: 28px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 15px;
color: #999999;
line-height: 28px;
font-style: normal;
display: flex;
align-items: center;
justify-content: flex-end;
flex-shrink: 0;
}
/* 立即注册按钮样式 */
.input-hint-right :deep(.n-button) {
font-size: 15px !important;
padding: 0 !important;
margin-left: 4px;
}
.form-options {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: -24px; /* 进一步向上移动 */
margin-bottom: 20px; /* 与下方登录按钮的间距 */
width: 400px; /* 增加宽度,让两个元素有合适间距 */
height: 28px; /* 固定高度 */
padding: 0; /* 移除内边距 */
box-sizing: border-box; /* 确保宽度包含边框 */
white-space: nowrap; /* 防止整个容器内容换行 */
}
/* 下次自动登录样式 */
:deep(.n-checkbox) {
width: auto; /* 改为自动宽度 */
height: 28px;
white-space: nowrap; /* 防止换行 */
flex-shrink: 0; /* 防止被压缩 */
display: flex;
align-items: center; /* 垂直居中对齐 */
}
/* 勾选框本身的对齐和高度 */
:deep(.n-checkbox .n-checkbox__input) {
align-self: flex-start; /* 改为顶部对齐 */
margin-top: 2px; /* 微调位置让它与文字基线对齐 */
}
/* 勾选框图标的高度调整 */
:deep(.n-checkbox .n-checkbox-box) {
height: 16px !important; /* 调整勾选框图标高度 */
width: 16px !important; /* 调整勾选框图标宽度 */
margin-top: 2px; /* 减少向下偏移,让勾选框往上 */
}
:deep(.n-checkbox .n-checkbox__label) {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px; /* 调整为16px */
color: #999999;
line-height: 28px;
text-align: left;
font-style: normal;
text-transform: none;
white-space: nowrap; /* 防止标签文字换行 */
display: flex;
align-items: center; /* 文字垂直居中 */
}
/* 忘记密码按钮样式 */
.form-options :deep(.n-button) {
width: 91px;
height: 28px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px !important; /* 调整为16px */
color: #0288D1 !important;
line-height: 28px;
text-align: left;
font-style: normal;
text-transform: none;
padding: 0 !important;
margin: 0 !important;
flex-shrink: 0; /* 防止按钮被压缩 */
}
.login-btn {
width: 420px;
height: 56px; /* 调整为56px */
background: #0288D1;
border: none;
border-radius: 6px; /* 添加轻微圆角 */
font-family: AppleSystemUIFont;
font-size: 22px;
color: #FFFFFF;
line-height: 39px;
text-align: center; /* 改为居中对齐 */
font-style: normal;
text-transform: none;
font-weight: 500;
margin-top: 20px;
transition: background-color 0.3s ease;
}
:deep(.login-btn:hover) {
background: #40a9ff;
}
.form-footer {
text-align: left; /* 改为左对齐 */
margin-top: -25px; /* 进一步减少与登录按钮的距离 */
}
.agreement-text {
width: 420px;
height: 28px;
font-family: AppleSystemUIFont;
font-size: 16px; /* 调整为16px */
color: #999999;
line-height: 28px;
text-align: left;
font-style: normal;
text-transform: none;
margin: 0;
}
/* 协议链接按钮样式 */
.agreement-text :deep(.n-button) {
font-size: 16px !important; /* 确保按钮字体也是16px */
padding: 0 !important;
margin-left: 4px;
}
/* 密码提示样式 */
.password-hint {
margin-top: -24px; /* 与form-options相同的位置 */
margin-bottom: 20px;
width: 400px;
height: 28px;
font-family: AppleSystemUIFont;
font-size: 16px;
color: #999999;
line-height: 28px;
text-align: left;
font-style: normal;
text-transform: none;
}
/* 响应式设计 */
@media (max-width: 1400px) {
.login-page {
padding-right: 180px; /* 调整右边距 */
}
.login-area {
width: 500px;
}
}
@media (max-width: 1200px) {
.login-page {
padding-right: 120px; /* 调整右边距 */
}
.login-area {
width: 450px;
}
}
@media (max-width: 1024px) {
.login-page {
justify-content: center;
padding-right: 0;
}
.login-area {
width: 400px;
max-width: 90vw;
}
.background-image {
position: absolute;
width: 100%;
height: 100%;
}
.top-logo {
top: 20px;
left: 24px;
}
.user-type-tabs {
justify-content: center;
gap: 50px; /* 中等屏幕间距 */
}
.type-tab {
font-size: 24px;
width: 70px;
}
}
@media (max-width: 768px) {
.login-page {
justify-content: center;
padding-right: 0;
}
.login-area {
width: 350px;
max-width: 85vw;
}
.login-form {
padding: 36px 24px;
}
.top-logo {
top: 16px;
left: 20px;
}
.top-logo img {
height: 36px;
}
.user-type-tabs {
margin-bottom: 30px;
gap: 40px; /* 小屏幕间距 */
}
.type-tab {
font-size: 20px;
width: 60px;
height: 35px;
line-height: 35px;
}
.type-tab.active::after {
width: 60px; /* 中等屏幕下划线宽度 */
height: 2px;
}
}
@media (max-width: 480px) {
.login-page {
justify-content: center;
padding-right: 0;
}
.login-area {
width: 300px;
max-width: 90vw;
}
.login-form {
padding: 32px 20px;
}
.form-header h2 {
font-size: 18px;
}
.top-logo {
top: 12px;
left: 16px;
}
.type-tab {
font-size: 18px;
width: 55px;
height: 32px;
line-height: 32px;
}
.type-tab.active::after {
width: 50px; /* 小屏幕下划线宽度 */
height: 2px;
}
}
</style>