很早之前就想把登录注册页做在一起,开源的一些又比较简陋或者技术栈不太一样,b站上一些很精美的又是原生三件套做的,最近刚好有个项目要做,就先做了一个练练手
其实页面很简单,需要注意的就几点
html<style>
html,
body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden; /* 添加这行来防止滚动 */
}
#app {
height: 100%; /* 确保 app 容器也是全高的 */
}
</style>
html<template>
<div class="login-page">
<div class="content-box">
<div class="form-side">
<div class="header">
<h2 class="title">{{ model ? '欢迎回来' : '创建账号' }}</h2>
<p class="subtitle">{{ model ? '登录您的账号' : '开始您的旅程' }}</p>
</div>
<div class="form-container">
<el-form v-if="model" :model="loginForm" :rules="loginRules" ref="loginFormRef" class="login-form">
<el-form-item prop="username">
<el-input v-model="loginForm.username" placeholder="用户名/学号/邮箱" prefix-icon="User" />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" type="password" placeholder="密码" prefix-icon="Lock" />
</el-form-item>
<el-form-item>
<el-checkbox v-model="loginForm.rememberMe">记住我</el-checkbox>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="login" class="submit-btn">
{{ loading ? '登录中...' : '登录' }}
</el-button>
</el-form-item>
</el-form>
<el-form v-else :model="registerForm" :rules="registerRules" ref="registerFormRef" class="register-form">
<el-form-item prop="studentId">
<el-input v-model="registerForm.studentId" placeholder="学号" prefix-icon="Ticket" />
</el-form-item>
<el-form-item prop="username">
<el-input v-model="registerForm.username" placeholder="用户名" prefix-icon="User" />
</el-form-item>
<el-form-item prop="email">
<el-input v-model="registerForm.email" placeholder="邮箱" prefix-icon="Message" />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="registerForm.password" type="password" placeholder="密码" prefix-icon="Lock" />
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input v-model="registerForm.confirmPassword" type="password" placeholder="确认密码" prefix-icon="Lock" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="register" class="submit-btn">
{{ loading ? '注册中...' : '注册' }}
</el-button>
</el-form-item>
</el-form>
<div class="switch-area">
<div class="switch-button" :class="{ 'right-aligned': model }">
<span class="switch-text">{{ model ? '初来乍到?' : '已有账号?' }}</span>
<el-button link type="primary" @click="handleSwitch" class="switch-link">
{{ model ? '注册账号' : '返回登录' }}
</el-button>
</div>
</div>
</div>
</div>
<div class="image-side">
<div class="overlay"></div>
<img src="@/assets/login-background.jpg" alt="Login Background" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, onMounted, onUnmounted } from 'vue'
import type { FormInstance } from 'element-plus'
const model = ref(true)
const loginFormRef = ref<FormInstance>()
const registerFormRef = ref<FormInstance>()
const handleSwitch = async () => {
// 先重置当前表单
if (model.value) {
loginFormRef.value?.resetFields()
} else {
registerFormRef.value?.resetFields()
}
// 切换模式
model.value = !model.value
// 等待 DOM 更新
await nextTick()
}
const loginForm = ref({
username: '',
password: '',
rememberMe: false
})
const loading = ref(false)
// 增强密码验证规则
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const passwordValidator = (rule: any, value: string, callback: any) => {
if (value === '') {
callback(new Error('请输入密码'))
} else if (value.length < 8) {
callback(new Error('密码长度不能小于8位'))
} else if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
callback(new Error('密码必须包含大小写字母和数字'))
} else {
callback()
}
}
// 确认密码验证
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const confirmPasswordValidator = (rule: any, value: string, callback: any) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== registerForm.value.password) {
callback(new Error('两次输入密码不一致'))
} else {
callback()
}
}
const loginRules = ref({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ validator: passwordValidator, trigger: 'blur' }
]
})
const registerForm = ref({
username: '',
studentId: '',
email: '',
password: '',
confirmPassword: ''
})
const registerRules = ref({
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
studentId: [
{ required: true, message: '请输入学号', trigger: 'blur' },
{ pattern: /^\d+$/, message: '学号必须为数字', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ validator: passwordValidator, trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请确认密码', trigger: 'blur' },
{ validator: confirmPasswordValidator, trigger: 'blur' }
]
})
// 在组件挂载时检查是否有保存的登录信息
onMounted(() => {
const savedCredentials = localStorage.getItem('userCredentials')
if (savedCredentials) {
const { username, rememberMe } = JSON.parse(savedCredentials)
loginForm.value.username = username
loginForm.value.rememberMe = rememberMe
}
})
onUnmounted(() => {
localStorage.removeItem('userCredentials')
})
const login = async () => {
if (!loginFormRef.value) return
try {
loading.value = true
const valid = await loginFormRef.value.validate()
if (valid) {
// 处理登录逻辑...
if (loginForm.value.rememberMe) {
localStorage.setItem('userCredentials', JSON.stringify({
username: loginForm.value.username,
rememberMe: true
}))
} else {
localStorage.removeItem('userCredentials')
}
}
} catch (error) {
console.error('登录验证失败:', error)
} finally {
loading.value = false
}
}
const register = async () => {
if (!registerFormRef.value) return
try {
loading.value = true
const valid = await registerFormRef.value.validate()
if (valid) {
// 处理注册逻辑...
}
} catch (error) {
console.error('注册验证失败:', error)
} finally {
loading.value = false
}
}
</script>
<style scoped lang="scss">
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600&family=Open+Sans&display=swap');
.login-page {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #5a1167 100%);
padding: 20px;
.content-box {
display: flex;
background: white;
width: 800px;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
background-color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(40px);
-webkit-backdrop-filter: blur(10px);
overflow: hidden;
&:hover {
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.2);
}
.form-side {
flex: 1;
padding: 40px;
min-width: 300px;
}
.image-side {
position: relative;
width: 400px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg,
rgba(149, 3, 127, 0.4) 0%,
rgba(0, 0, 0, 0.4) 100%);
z-index: 1;
}
&:hover img {
transform: scale(1.05);
}
}
}
.header {
text-align: center;
margin-bottom: 40px;
.title {
font-size: 28px;
color: #2c3e50;
margin: 0 0 8px;
font-weight: 600;
font-family: "Cormorant Garamond", "Times New Roman", Georgia, serif;
letter-spacing: 0.5px;
}
.subtitle {
font-size: 16px;
color: #7f8c8d;
margin: 0;
font-family: "Poppins", "Helvetica Neue", Arial, sans-serif;
letter-spacing: 0.3px;
}
}
.form-container {
.el-form-item {
margin-bottom: 16px;
}
:deep(.el-input) {
.el-input__wrapper {
padding: 6px;
border-radius: 0;
box-shadow: none !important;
border-bottom: 1px solid #dcdfe6;
background-color: transparent;
transition: all 0.3s ease;
&.is-focus {
border-bottom-color: var(--el-color-primary);
}
}
}
.submit-btn {
width: 100%;
padding: 12px;
font-size: 16px;
border-radius: 8px;
margin-top: 12px;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
:deep(.el-checkbox) {
margin-left: 0;
margin-bottom: 8px;
.el-checkbox__label {
color: #7f8c8d;
}
}
}
.switch-area {
margin-top: 32px;
padding-top: 20px;
border-top: 1px solid #eee;
.switch-button {
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
&.right-aligned {
justify-content: flex-end;
}
.switch-text {
color: #7f8c8d;
font-size: 14px;
}
.switch-link {
font-size: 14px;
font-weight: 500;
&:hover {
text-decoration: underline;
}
}
}
}
}
// 响应式设计
@media (max-width: 900px) {
.login-page {
.content-box {
width: 100%;
max-width: 500px;
margin: 20px;
.image-side {
display: none;
}
}
}
}
@media (max-width: 480px) {
.login-page {
.content-box {
.form-side {
padding: 24px;
}
}
}
}
</style>


本文作者:MapleCity
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!