Files
schoolNews/schoolNewsWeb/src/views/public/login/Login.vue
2025-11-24 17:41:28 +08:00

856 lines
20 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-container">
<!-- 左侧励志区域 -->
<div class="login-left">
<div class="left-content">
<div class="quote-text">
<span class="quote-mark"></span>
<div class="quote-content">
<p>不负时代韶华</p>
<p>争做时代新人</p>
</div>
</div>
</div>
</div>
<!-- 右侧登录表单区域 -->
<div class="login-right">
<div class="login-form-container">
<!-- Logo和标题区域 -->
<div class="login-header">
<div class="logo-section">
<div class="logo">
<img src="@/assets/imgs/logo-icon.svg" alt="Logo" />
</div>
<h1 class="platform-title">红色思政学习平台</h1>
</div>
<h2 class="login-title">{{ loginModeTitle }}</h2>
</div>
<!-- 登录方式切换 -->
<div class="login-mode-tabs">
<div
:class="['tab-item', { active: loginMode === 'password' }]"
@click="switchLoginMode('password')"
>
密码登录
</div>
<div
:class="['tab-item', { active: loginMode === 'captcha' }]"
@click="switchLoginMode('captcha')"
>
验证码登录
</div>
</div>
<!-- 登录表单 -->
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
class="login-form"
@submit.prevent="handleLogin"
>
<!-- 密码登录模式 -->
<template v-if="loginMode === 'password'">
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入学号、手机号、邮箱"
class="form-input"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
show-password
class="form-input"
/>
</el-form-item>
</template>
<!-- 验证码登录模式 -->
<template v-else>
<!-- 登录方式选择 -->
<div class="captcha-type-tabs">
<div
:class="['captcha-tab-item', { active: captchaType === 'phone' }]"
@click="switchCaptchaType('phone')"
>
手机号
</div>
<div
:class="['captcha-tab-item', { active: captchaType === 'email' }]"
@click="switchCaptchaType('email')"
>
邮箱
</div>
</div>
<!-- 手机号验证码登录 -->
<template v-if="captchaType === 'phone'">
<el-form-item prop="phone">
<el-input
v-model="loginForm.phone"
placeholder="请输入手机号"
class="form-input"
maxlength="11"
/>
</el-form-item>
<el-form-item prop="captcha">
<div class="captcha-input-container">
<el-input
v-model="loginForm.captcha"
placeholder="请输入验证码"
class="form-input captcha-input"
maxlength="6"
/>
<el-button
class="send-captcha-btn"
:disabled="smsCountdown > 0"
@click="handleSendSmsCode"
>
{{ smsCountdown > 0 ? `${smsCountdown}秒后重试` : '获取验证码' }}
</el-button>
</div>
</el-form-item>
</template>
<!-- 邮箱验证码登录 -->
<template v-else>
<el-form-item prop="email">
<el-input
v-model="loginForm.email"
placeholder="请输入邮箱"
class="form-input"
/>
</el-form-item>
<el-form-item prop="captcha">
<div class="captcha-input-container">
<el-input
v-model="loginForm.captcha"
placeholder="请输入验证码"
class="form-input captcha-input"
maxlength="6"
/>
<el-button
class="send-captcha-btn"
:disabled="emailCountdown > 0"
@click="handleSendEmailCode"
>
{{ emailCountdown > 0 ? `${emailCountdown}秒后重试` : '获取验证码' }}
</el-button>
</div>
</el-form-item>
</template>
</template>
<el-form-item>
<div class="login-options">
<div class="remember-me">
<el-checkbox v-model="loginForm.rememberMe">
自动登录
</el-checkbox>
</div>
<el-link type="primary" @click="goToForgotPassword" class="forgot-password">
忘记密码
</el-link>
</div>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:loading="loginLoading"
@click="handleLogin"
class="login-button"
>
登录
</el-button>
</el-form-item>
<el-form-item prop="agree">
<p class="agreement-text">
<el-checkbox v-model="loginForm.agree">登录即为同意<span class="agreement-link" style="color: red">红色思政智能体平台</span></el-checkbox>
</p>
</el-form-item>
</el-form>
</div>
<!-- 底部信息 -->
<div class="login-footer">
<p class="register-link">
没有账号<span class="register-link-text" @click="goToRegister">现在就注册</span>
</p>
<p class="copyright">
Copyright ©红色思政智能体平台
</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useStore } from 'vuex';
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import type { LoginParam } from '@/types';
import { LoginType } from '@/types/enums';
import { authApi } from '@/apis/system/auth';
// 响应式引用
const loginFormRef = ref<FormInstance>();
const loginLoading = ref(false);
const showCaptcha = ref(false);
const captchaImage = ref('');
// 登录模式password-密码登录captcha-验证码登录
const loginMode = ref<'password' | 'captcha'>('password');
// 验证码类型phone-手机号email-邮箱
const captchaType = ref<'phone' | 'email'>('phone');
// 倒计时
const smsCountdown = ref(0);
const emailCountdown = ref(0);
// Composition API
const router = useRouter();
const route = useRoute();
const store = useStore();
// 表单数据
const loginForm = reactive<LoginParam>({
loginType: undefined,
username: '',
phone: '',
email: '',
password: '',
captcha: '',
captchaId: '',
rememberMe: false,
agree: true
});
// 计算属性:登录模式标题
const loginModeTitle = computed(() => {
return loginMode.value === 'password' ? '密码登录' : '验证码登录';
});
// 表单验证规则
const loginRules: FormRules = {
username: [
{
required: loginMode.value === 'password',
message: '请输入用户名、手机号或邮箱',
trigger: 'blur'
}
],
phone: [
{
required: loginMode.value === 'captcha' && captchaType.value === 'phone',
message: '请输入手机号',
trigger: 'blur'
},
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号',
trigger: 'blur'
}
],
email: [
{
required: loginMode.value === 'captcha' && captchaType.value === 'email',
message: '请输入邮箱',
trigger: 'blur'
},
{
type: 'email',
message: '请输入正确的邮箱格式',
trigger: 'blur'
}
],
password: [
{
required: loginMode.value === 'password',
message: '请输入密码',
trigger: 'blur'
},
{
min: 6,
message: '密码至少6个字符',
trigger: 'blur'
}
],
captcha: [
{
required: loginMode.value === 'captcha',
message: '请输入验证码',
trigger: 'blur'
},
{
len: 6,
message: '验证码为6位',
trigger: 'blur'
}
],
agree: [
{
validator: (rule: any, value: boolean, callback: any) => {
if (!value) {
callback(new Error('请同意《红色思政智能体平台》用户协议'));
} else {
callback();
}
},
trigger: 'change'
}
]
};
// 切换登录模式
const switchLoginMode = (mode: 'password' | 'captcha') => {
if (loginMode.value === mode) return;
loginMode.value = mode;
// 清空表单数据和验证
loginFormRef.value?.clearValidate();
loginForm.username = '';
loginForm.phone = '';
loginForm.email = '';
loginForm.password = '';
loginForm.captcha = '';
loginForm.captchaId = '';
};
// 切换验证码类型
const switchCaptchaType = (type: 'phone' | 'email') => {
if (captchaType.value === type) return;
captchaType.value = type;
// 清空相关表单数据和验证
loginFormRef.value?.clearValidate();
loginForm.phone = '';
loginForm.email = '';
loginForm.captcha = '';
loginForm.captchaId = '';
};
// 发送短信验证码
const handleSendSmsCode = async () => {
// 验证手机号
if (!loginForm.phone || loginForm.phone.trim() === '') {
ElMessage.warning('请输入手机号');
return;
}
const phonePattern = /^1[3-9]\d{9}$/;
if (!phonePattern.test(loginForm.phone)) {
ElMessage.warning('请输入正确的手机号');
return;
}
try {
const result = await authApi.sendSmsCode(loginForm.phone);
if (result.code === 200 && result.data) {
// 保存sessionId
loginForm.captchaId = result.data.sessionId;
ElMessage.success(result.data.message || '验证码已发送');
// 开始倒计时
smsCountdown.value = 60;
const timer = setInterval(() => {
smsCountdown.value--;
if (smsCountdown.value <= 0) {
clearInterval(timer);
}
}, 1000);
} else {
ElMessage.error(result.message || '发送验证码失败');
}
} catch (error: any) {
console.error('发送验证码失败:', error);
ElMessage.error(error.message || '发送验证码失败');
}
};
// 发送邮箱验证码
const handleSendEmailCode = async () => {
// 验证邮箱
if (!loginForm.email || loginForm.email.trim() === '') {
ElMessage.warning('请输入邮箱');
return;
}
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(loginForm.email)) {
ElMessage.warning('请输入正确的邮箱格式');
return;
}
try {
const result = await authApi.sendEmailCode(loginForm.email);
if (result.code === 200 && result.data) {
// 保存sessionId
loginForm.captchaId = result.data.sessionId;
ElMessage.success(result.data.message || '验证码已发送到邮箱');
// 开始倒计时
emailCountdown.value = 60;
const timer = setInterval(() => {
emailCountdown.value--;
if (emailCountdown.value <= 0) {
clearInterval(timer);
}
}, 1000);
} else {
ElMessage.error(result.message || '发送验证码失败');
}
} catch (error: any) {
console.error('发送验证码失败:', error);
ElMessage.error(error.message || '发送验证码失败');
}
};
// 方法
const handleLogin = async () => {
if (!loginFormRef.value) return;
// 检查是否同意用户协议
if (!loginForm.agree) {
ElMessage.warning('请先同意《红色思政智能体平台》用户协议');
return;
}
try {
const valid = await loginFormRef.value.validate();
if (!valid) return;
loginLoading.value = true;
// 根据登录模式设置loginType
if (loginMode.value === 'password') {
loginForm.loginType = LoginType.PASSWORD;
} else {
loginForm.loginType = captchaType.value === 'phone' ? LoginType.PHONE : LoginType.EMAIL;
}
// 调用store中的登录action
const result = await store.dispatch('auth/login', loginForm);
ElMessage.success('登录成功!');
// 优先使用 query 中的 redirect其次使用返回的 redirectUrl最后使用默认首页
const redirectPath = (route.query.redirect as string) || result.redirectUrl || '/home';
router.push(redirectPath);
} catch (error: any) {
console.error('登录失败:', error);
ElMessage.error(error.message || '登录失败,请检查用户名和密码');
// 登录失败后显示验证码
showCaptcha.value = true;
// refreshCaptcha();
} finally {
loginLoading.value = false;
}
};
const refreshCaptcha = async () => {
try {
const result = await authApi.getCaptcha();
if (result.data) {
captchaImage.value = result.data.captchaImage;
loginForm.captchaId = result.data.captchaId;
}
} catch (error) {
console.error('获取验证码失败:', error);
ElMessage.error('获取验证码失败');
}
};
function goToRegister() {
router.push('/register');
}
function goToForgotPassword() {
router.push('/forgot-password');
}
// 组件挂载时检查是否需要显示验证码
onMounted(() => {
// 可以根据需要决定是否默认显示验证码
// refreshCaptcha();
});
</script>
<style lang="scss" scoped>
.login-container {
display: flex;
width: 100%;
height: 80%;
max-width: 1142px;
margin: auto auto;
box-shadow: 0px 4px 30px 0px rgba(176, 196, 225, 0.25);
border-radius: 30px;
overflow: hidden;
justify-content: center;
align-items: center;
}
.login-left {
height: 100%;
flex: 1;
display: flex;
/* min-height: 671px; */
padding: 113px 120px;
border-radius: 30px 0 0 30px;
overflow: hidden;
background: url(@/assets/imgs/login-bg.png);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
.left-content {
width: 100%;
max-width: 350px;
}
.quote-text {
color: #FFF2D3;
.quote-mark {
font-family: 'Arial Black', sans-serif;
font-weight: 900;
font-size: 71.096px;
line-height: 0.74;
display: block;
margin-left: 1.48px;
}
.quote-content {
margin-top: 46.66px;
p {
font-family: 'Taipei Sans TC Beta', 'PingFang SC', sans-serif;
font-weight: 700;
font-size: 50px;
line-height: 1.42;
margin: 0;
}
}
}
}
.login-right {
flex: 1;
background: #FFFFFF;
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
border-radius: 0 30px 30px 0;
padding: 40px 0;
}
.login-form-container {
width: 287px;
padding: 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.login-header {
margin-bottom: 24px;
.logo-section {
display: flex;
align-items: center;
gap: 11px;
margin-bottom: 16px;
.logo {
width: 36px;
height: 36px;
background: #C62828;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
padding: 3px;
img {
width: 30px;
height: 30px;
}
}
.platform-title {
font-family: 'Taipei Sans TC Beta', sans-serif;
font-weight: 700;
font-size: 26px;
line-height: 1.31;
color: #141F38;
margin: 0;
}
}
.login-title {
font-family: 'PingFang SC', sans-serif;
font-weight: 600;
font-size: 16px;
line-height: 1.5;
color: #141F38;
margin: 0;
}
}
.login-mode-tabs {
display: flex;
gap: 16px;
margin-bottom: 24px;
border-bottom: 1px solid #E8E8E8;
.tab-item {
padding: 8px 16px;
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.3s;
&:hover {
color: #C62828;
}
&.active {
color: #C62828;
border-bottom-color: #C62828;
font-weight: 600;
}
}
}
.captcha-type-tabs {
display: flex;
gap: 12px;
margin-bottom: 16px;
.captcha-tab-item {
flex: 1;
padding: 8px;
text-align: center;
font-size: 14px;
color: rgba(0, 0, 0, 0.65);
background: #F2F3F5;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: #E8E8E8;
}
&.active {
color: #FFFFFF;
background: #C62828;
font-weight: 500;
}
}
}
.login-form {
.form-input {
width: 100%;
height: 48px;
background: #F2F3F5;
border: none;
border-radius: 12px;
font-size: 14px;
color: rgba(0, 0, 0, 0.3);
&::placeholder {
color: rgba(0, 0, 0, 0.3);
}
&:focus {
outline: none;
background: #FFFFFF;
border: 1px solid #C62828;
}
}
.captcha-input-container {
display: flex;
gap: 8px;
width: 100%;
.captcha-input {
flex: 1;
}
.send-captcha-btn {
height: 48px;
padding: 0 16px;
background: #C62828;
border: none;
border-radius: 12px;
color: #FFFFFF;
font-size: 13px;
white-space: nowrap;
&:hover:not(:disabled) {
background: #B71C1C;
}
&:disabled {
background: #D9D9D9;
color: rgba(0, 0, 0, 0.25);
cursor: not-allowed;
}
}
}
.login-options {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 16px;
.remember-me {
display: flex;
align-items: center;
gap: 4px;
.el-checkbox {
font-size: 12px;
color: rgba(0, 0, 0, 0.3);
.el-checkbox__label {
font-size: 12px;
line-height: 1.67;
}
}
}
.forgot-password {
font-size: 12px;
color: rgba(0, 0, 0, 0.3);
text-decoration: none;
&:hover {
color: #C62828;
}
}
}
.login-button {
width: 100%;
height: 46px;
background: #C62828;
border: none;
border-radius: 12px;
color: #FFFFFF;
font-family: 'PingFang SC', sans-serif;
font-weight: 500;
font-size: 16px;
line-height: 1.4;
margin-bottom: 8px;
&:hover {
background: #B71C1C;
}
}
.agreement-text {
font-family: 'PingFang SC', sans-serif;
font-weight: 400;
font-size: 10px;
line-height: 1.8;
color: rgba(0, 0, 0, 0.3);
text-align: center;
margin: 0;
}
}
.login-footer {
width: 100%;
.register-link {
font-family: 'PingFang SC', sans-serif;
font-weight: 600;
font-size: 12px;
line-height: 1.83;
color: #141F38;
text-align: center;
margin: 0 0 16px 0;
.register-link-text {
cursor: pointer;
color: #ff0000;
&:hover {
color: #C62828;
}
}
}
.copyright {
font-family: 'PingFang SC', sans-serif;
font-size: 12px;
line-height: 2;
color: #D9D9D9;
text-align: center;
padding-bottom: 0;
}
}
// 响应式设计
@media (max-width: 768px) {
.login-container {
flex-direction: column;
border-radius: 0;
min-height: 100vh;
}
.login-left {
min-height: 300px;
padding: 40px;
.left-content {
max-width: 100%;
}
.quote-text {
.quote-mark {
font-size: 50px;
}
.quote-content p {
font-size: 36px;
}
}
}
.login-right {
min-height: auto;
padding: 20px;
}
.login-form-container {
width: 100%;
max-width: 400px;
}
}
</style>