You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

797 lines
19 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<script setup lang="ts">
import { ref, computed, reactive, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { ElMessage, ElButton, ElInput, ElForm, ElFormItem, ElIcon, ElDivider, ElCard } from 'element-plus';
import { User, Lock, View, Hide, UserFilled, Message } from '@element-plus/icons-vue';
import { useUserStore } from '../stores';
const router = useRouter();
const route = useRoute();
const userStore = useUserStore();
// 表单状态
const isLogin = ref(true);
const showPassword = ref(false);
const loading = ref(false);
// 登录表单
const loginForm = reactive({
username: '',
password: ''
});
// 注册表单
const registerForm = reactive({
username: '',
email: '',
password: '',
confirmPassword: '',
nickname: ''
});
// 表单验证规则
const loginRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名长度为3-20个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
]
};
const registerRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名长度为3-20个字符', trigger: 'blur' },
{ pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
{ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '请输入正确的邮箱格式', trigger: 'blur' }
],
nickname: [
{ required: true, message: '请输入昵称', trigger: 'blur' },
{ min: 2, max: 10, message: '昵称长度为2-10个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{
validator: (rule: any, value: string, callback: Function) => {
if (value !== registerForm.password) {
callback(new Error('两次输入的密码不一致'));
} else {
callback();
}
},
trigger: 'blur'
}
]
};
// 表单引用
const loginFormRef = ref();
const registerFormRef = ref();
// 计算属性
const formTitle = computed(() => isLogin.value ? '欢迎回来' : '加入我们');
const submitText = computed(() => isLogin.value ? '登录' : '注册');
const switchText = computed(() => isLogin.value ? '还没有账号?点击注册' : '已有账号?点击登录');
// 切换登录/注册模式
const toggleMode = () => {
isLogin.value = !isLogin.value;
// 清空表单
if (loginFormRef.value) loginFormRef.value.resetFields();
if (registerFormRef.value) registerFormRef.value.resetFields();
};
// 切换密码显示
const togglePasswordVisibility = () => {
showPassword.value = !showPassword.value;
};
// 处理登录
const handleLogin = async () => {
if (!loginFormRef.value) return;
const valid = await loginFormRef.value.validate().catch(() => false);
if (!valid) return;
loading.value = true;
try {
await userStore.login(loginForm.username, loginForm.password);
ElMessage.success('登录成功!');
// 重定向到原来要访问的页面或默认页面
const redirect = route.query.redirect as string;
router.push(redirect || '/home');
} catch (error: any) {
ElMessage.error(error?.message || '登录失败,请重试');
} finally {
loading.value = false;
}
};
// 处理注册
const handleRegister = async () => {
if (!registerFormRef.value) return;
const valid = await registerFormRef.value.validate().catch(() => false);
if (!valid) return;
loading.value = true;
try {
// 由于当前register方法只接受email、password、code三个参数先使用email作为用户名
await userStore.register(
registerForm.email,
registerForm.password,
'' // 验证码参数,暂时留空
);
ElMessage.success('注册成功!');
// 注册成功后自动切换到登录模式
isLogin.value = true;
loginForm.username = registerForm.username;
} catch (error: any) {
ElMessage.error(error?.message || '注册失败,请重试');
} finally {
loading.value = false;
}
};
// 处理表单提交
const handleSubmit = () => {
if (isLogin.value) {
handleLogin();
} else {
handleRegister();
}
};
// 快速登录演示
const quickLogin = () => {
loginForm.username = 'demo';
loginForm.password = '123456';
ElMessage.info('已填入演示账号,点击登录即可体验');
};
onMounted(() => {
// 如果已经登录,直接跳转
if (userStore.isLoggedIn) {
router.push('/home');
}
});
</script>
<template>
<div class="login-page">
<!-- 背景装饰 -->
<div class="bg-decoration">
<div class="decoration-circle decoration-circle--1"></div>
<div class="decoration-circle decoration-circle--2"></div>
<div class="decoration-circle decoration-circle--3"></div>
<div class="decoration-grid"></div>
</div>
<!-- 主内容 -->
<div class="login-container">
<!-- 左侧信息区 -->
<div class="info-section">
<div class="info-content">
<div class="logo">
<div class="logo-icon">
<span class="logo-text">UL</span>
</div>
<h1 class="brand-name">UniLife</h1>
</div>
<div class="info-text">
<h2 class="info-title">连接校园分享生活</h2>
<p class="info-description">
加入UniLife社区与同学们一起交流学习经验分享校园生活共同成长进步
</p>
</div>
<div class="features">
<div class="feature-item">
<div class="feature-icon">
<el-icon><UserFilled /></el-icon>
</div>
<div class="feature-text">
<h3>学术交流</h3>
<p>与同学讨论学术问题分享学习心得</p>
</div>
</div>
<div class="feature-item">
<div class="feature-icon">
<el-icon><Message /></el-icon>
</div>
<div class="feature-text">
<h3>校园动态</h3>
<p>获取最新的校园资讯和活动信息</p>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧表单区 -->
<div class="form-section">
<el-card class="form-card" shadow="hover">
<!-- 表单头部 -->
<div class="form-header">
<h2 class="form-title">{{ formTitle }}</h2>
<p class="form-subtitle">
{{ isLogin ? '登录您的账号继续使用' : '创建新账号开始您的校园之旅' }}
</p>
</div>
<!-- 登录表单 -->
<el-form
v-if="isLogin"
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
class="auth-form"
size="large"
@submit.prevent="handleSubmit"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名"
:prefix-icon="User"
clearable
class="form-input"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
:type="showPassword ? 'text' : 'password'"
placeholder="请输入密码"
:prefix-icon="Lock"
clearable
class="form-input"
@keyup.enter="handleSubmit"
>
<template #suffix>
<el-icon @click="togglePasswordVisibility" class="password-toggle">
<View v-if="showPassword" />
<Hide v-else />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-button
type="primary"
:loading="loading"
@click="handleSubmit"
class="submit-btn"
size="large"
>
{{ loading ? '登录中...' : '登录' }}
</el-button>
</el-form>
<!-- 注册表单 -->
<el-form
v-else
ref="registerFormRef"
:model="registerForm"
:rules="registerRules"
class="auth-form"
size="large"
@submit.prevent="handleSubmit"
>
<el-form-item prop="username">
<el-input
v-model="registerForm.username"
placeholder="请输入用户名"
:prefix-icon="User"
clearable
class="form-input"
/>
</el-form-item>
<el-form-item prop="nickname">
<el-input
v-model="registerForm.nickname"
placeholder="请输入昵称"
:prefix-icon="UserFilled"
clearable
class="form-input"
/>
</el-form-item>
<el-form-item prop="email">
<el-input
v-model="registerForm.email"
placeholder="请输入邮箱地址"
:prefix-icon="Message"
clearable
class="form-input"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="registerForm.password"
:type="showPassword ? 'text' : 'password'"
placeholder="请输入密码"
:prefix-icon="Lock"
clearable
class="form-input"
>
<template #suffix>
<el-icon @click="togglePasswordVisibility" class="password-toggle">
<View v-if="showPassword" />
<Hide v-else />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input
v-model="registerForm.confirmPassword"
:type="showPassword ? 'text' : 'password'"
placeholder="请再次输入密码"
:prefix-icon="Lock"
clearable
class="form-input"
@keyup.enter="handleSubmit"
/>
</el-form-item>
<el-button
type="primary"
:loading="loading"
@click="handleSubmit"
class="submit-btn"
size="large"
>
{{ loading ? '注册中...' : '注册' }}
</el-button>
</el-form>
<!-- 分割线 -->
<el-divider class="form-divider">
<span class="divider-text">或者</span>
</el-divider>
<!-- 快速登录/切换模式 -->
<div class="form-footer">
<el-button
v-if="isLogin"
@click="quickLogin"
class="quick-login-btn"
size="large"
plain
>
演示账号快速登录
</el-button>
<el-button
@click="toggleMode"
class="switch-mode-btn"
type="primary"
link
size="large"
>
{{ switchText }}
</el-button>
</div>
</el-card>
</div>
</div>
</div>
</template>
<style scoped>
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--primary-50) 0%, var(--primary-100) 100%);
position: relative;
overflow: hidden;
}
/* 背景装饰 */
.bg-decoration {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
}
.decoration-circle {
position: absolute;
background: linear-gradient(135deg, var(--primary-300), var(--primary-200));
border-radius: var(--radius-full);
opacity: 0.3;
}
.decoration-circle--1 {
width: 200px;
height: 200px;
top: 10%;
left: 10%;
animation: float 8s ease-in-out infinite;
}
.decoration-circle--2 {
width: 150px;
height: 150px;
bottom: 15%;
right: 15%;
animation: float 6s ease-in-out infinite reverse;
}
.decoration-circle--3 {
width: 100px;
height: 100px;
top: 60%;
left: 5%;
animation: float 10s ease-in-out infinite;
}
.decoration-grid {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
linear-gradient(rgba(139, 77, 255, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(139, 77, 255, 0.05) 1px, transparent 1px);
background-size: 50px 50px;
}
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-30px) rotate(180deg); }
}
/* 主容器 */
.login-container {
display: flex;
max-width: 1000px;
width: 100%;
margin: 0 auto;
padding: var(--space-6);
gap: var(--space-8);
position: relative;
z-index: 2;
}
/* 信息区域 */
.info-section {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(20px);
border-radius: var(--radius-2xl);
margin-right: var(--space-4);
box-shadow: var(--shadow-lg);
}
.info-content {
max-width: 400px;
color: var(--text-primary);
padding: var(--space-8);
}
.logo {
display: flex;
align-items: center;
gap: var(--space-4);
margin-bottom: var(--space-8);
}
.logo-icon {
width: 64px;
height: 64px;
border-radius: var(--radius-xl);
background: linear-gradient(135deg, var(--primary-600), var(--primary-400));
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--shadow-xl);
}
.logo-text {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: white;
}
.brand-name {
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
margin: 0;
background: linear-gradient(135deg, var(--primary-600), var(--primary-400));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.info-text {
margin-bottom: var(--space-8);
}
.info-title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
margin: 0 0 var(--space-4) 0;
color: var(--text-primary);
}
.info-description {
font-size: var(--font-size-lg);
line-height: var(--line-height-relaxed);
margin: 0;
color: var(--text-secondary);
}
.features {
display: flex;
flex-direction: column;
gap: var(--space-6);
}
.feature-item {
display: flex;
align-items: center;
gap: var(--space-4);
padding: var(--space-4);
border-radius: var(--radius-lg);
background: var(--primary-50);
transition: all var(--duration-200) var(--ease-out);
}
.feature-item:hover {
background: var(--primary-100);
transform: translateX(4px);
}
.feature-icon {
width: 48px;
height: 48px;
border-radius: var(--radius-lg);
background: linear-gradient(135deg, var(--primary-600), var(--primary-400));
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-xl);
color: white;
box-shadow: var(--shadow-sm);
}
.feature-text h3 {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
margin: 0 0 var(--space-1) 0;
color: var(--text-primary);
}
.feature-text p {
font-size: var(--font-size-sm);
margin: 0;
color: var(--text-secondary);
}
/* 表单区域 */
.form-section {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.form-card {
width: 100%;
max-width: 400px;
border-radius: var(--radius-2xl);
border: none;
box-shadow: var(--shadow-2xl);
overflow: hidden;
}
.form-card :deep(.el-card__body) {
padding: var(--space-8);
}
.form-header {
text-align: center;
margin-bottom: var(--space-8);
}
.form-title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--text-primary);
margin: 0 0 var(--space-2) 0;
}
.form-subtitle {
color: var(--text-secondary);
margin: 0;
font-size: var(--font-size-sm);
}
/* 表单样式 */
.auth-form {
margin-bottom: var(--space-6);
}
.form-input :deep(.el-input__wrapper) {
border-radius: var(--radius-lg);
padding: var(--space-3) var(--space-4);
box-shadow: var(--shadow-sm);
border: 1px solid var(--border-light);
transition: all var(--duration-150) var(--ease-out);
}
.form-input :deep(.el-input__wrapper:hover) {
box-shadow: var(--shadow-md);
border-color: var(--primary-300);
}
.form-input :deep(.el-input__wrapper.is-focus) {
box-shadow: 0 0 0 3px rgba(139, 77, 255, 0.1), var(--shadow-md);
border-color: var(--primary-500);
}
.password-toggle {
cursor: pointer;
color: var(--text-light);
transition: color var(--duration-150) var(--ease-out);
}
.password-toggle:hover {
color: var(--primary-500);
}
.submit-btn {
width: 100%;
height: 48px;
border-radius: var(--radius-lg);
font-weight: var(--font-weight-medium);
margin-top: var(--space-4);
box-shadow: var(--shadow-primary);
transition: all var(--duration-150) var(--ease-out);
}
.submit-btn:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-primary-lg);
}
/* 分割线 */
.form-divider {
margin: var(--space-6) 0;
}
.divider-text {
color: var(--text-light);
font-size: var(--font-size-sm);
padding: 0 var(--space-3);
background: var(--bg-elevated);
}
/* 表单底部 */
.form-footer {
display: flex;
flex-direction: column;
gap: var(--space-3);
align-items: center;
}
.quick-login-btn {
width: 100%;
height: 44px;
border-radius: var(--radius-lg);
border: 1px solid var(--border-color);
color: var(--text-secondary);
transition: all var(--duration-150) var(--ease-out);
}
.quick-login-btn:hover {
border-color: var(--primary-300);
color: var(--primary-600);
background: var(--primary-50);
}
.switch-mode-btn {
font-size: var(--font-size-sm);
transition: all var(--duration-150) var(--ease-out);
}
/* 响应式设计 */
@media (max-width: 768px) {
.login-container {
flex-direction: column;
padding: var(--space-4);
gap: var(--space-6);
}
.info-section {
margin-right: 0;
margin-bottom: var(--space-4);
}
.info-content {
padding: var(--space-6);
text-align: center;
}
.logo {
justify-content: center;
}
.features {
gap: var(--space-4);
}
.feature-item {
padding: var(--space-3);
}
.form-card {
max-width: none;
}
.form-card :deep(.el-card__body) {
padding: var(--space-6);
}
}
@media (max-width: 480px) {
.login-page {
padding: var(--space-4);
}
.info-content {
padding: var(--space-4);
}
.features {
flex-direction: column;
gap: var(--space-3);
}
.feature-item {
flex-direction: column;
text-align: center;
gap: var(--space-2);
}
.form-card :deep(.el-card__body) {
padding: var(--space-4);
}
.form-header {
margin-bottom: var(--space-6);
}
.form-title {
font-size: var(--font-size-xl);
}
}
</style>