登录页UI及长时间未操作锁定逻辑优化

This commit is contained in:
Cheng Zhou
2026-03-10 10:10:58 +08:00
parent 7936c55f3f
commit 4c98147f3e
12 changed files with 2225 additions and 470 deletions
+224 -441
View File
@@ -1,117 +1,101 @@
<template>
<div class="login-container">
<!-- Vanta.js 网络背景 -->
<div ref="vantaCanvas" id="vanta-canvas" class="vanta-background"></div>
<!-- 登录卡片 -->
<div class="login-wrapper">
<div class="max-w-md w-full card-container">
<div class="card-content">
<!-- 顶部装饰区域 -->
<div class="login-header" id="vanta-header">
<!-- 左上角安全访问 -->
<div class="header-left">
<span class="secure-badge">安全访问</span>
</div>
<!-- 中间CTMS -->
<div class="header-center">
<div class="ctms-logo">CTMS</div>
<div class="header-divider"></div>
</div>
</div>
<!-- 登录表单内容 -->
<div class="login-form-section">
<div class="form-label-section">
<span class="auth-badge">身份验证</span>
<h3 class="form-title">账户登录</h3>
</div>
<el-form
ref="formRef"
:model="form"
:rules="rules"
@keyup.enter="onSubmit"
class="login-form"
<div class="login-wrapper">
<div class="login-bg-shape shape-1"></div>
<div class="login-bg-shape shape-2"></div>
<div class="login-container">
<div class="login-card">
<div class="login-header">
<div class="logo-box">
<svg
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="logo-icon"
>
<!-- 邮箱字段 -->
<div class="form-group">
<label for="email" class="input-label">邮箱</label>
<el-form-item prop="email">
<el-input
id="email"
v-model="form.email"
type="email"
placeholder="your@email.com"
autocomplete="email"
class="custom-input"
/>
</el-form-item>
</div>
<!-- 密码字段 -->
<div class="form-group">
<div class="password-label-row">
<label for="password" class="input-label">密码</label>
<a href="#" class="forgot-link">忘记密码</a>
</div>
<el-form-item prop="password">
<el-input
id="password"
v-model="form.password"
type="password"
placeholder="••••••••"
autocomplete="current-password"
show-password
class="custom-input"
/>
</el-form-item>
</div>
<!-- 记住设备 -->
<div class="remember-row">
<el-checkbox id="remember" v-model="form.remember" class="custom-checkbox">
<span class="remember-text">记住此设备</span>
</el-checkbox>
</div>
<!-- 登录按钮 -->
<div class="button-group">
<el-button
type="primary"
:loading="loading"
@click="onSubmit"
class="login-btn"
>
<svg xmlns="http://www.w3.org/2000/svg" class="btn-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path>
</svg>
登录
</el-button>
</div>
</el-form>
<!-- 底部信息 -->
<div class="footer-section">
<div class="footer-divider"></div>
<p class="footer-text">
还没有账户 <RouterLink to="/register" class="footer-link">立即注册</RouterLink>
</p>
<div class="status-row">
<span class="status-dot"></span>
<span class="status-text">系统状态正常运行</span>
</div>
</div>
<path d="M12 2L2 7l10 5 10-5-10-5z"></path>
<path d="M2 17l10 5 10-5"></path>
<path d="M2 12l10 5 10-5"></path>
</svg>
</div>
<h2 class="sys-title">CTMS 临床试验管理系统</h2>
<p class="sys-subtitle">Log in to your account</p>
</div>
<el-form
ref="formRef"
:model="form"
:rules="rules"
@keyup.enter="onSubmit"
label-position="top"
class="login-form"
>
<el-form-item label="邮箱" prop="email">
<el-input
id="email"
v-model="form.email"
type="email"
placeholder="请输入邮箱"
size="large"
autocomplete="email"
class="login-input"
>
<template #prefix>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
</template>
</el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
id="password"
v-model="form.password"
type="password"
placeholder="请输入密码"
show-password
size="large"
autocomplete="current-password"
class="login-input"
>
<template #prefix>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</template>
</el-input>
</el-form-item>
<div class="remember-row">
<el-checkbox id="remember" v-model="form.remember" class="remember-checkbox">
<span class="remember-text">记住此设备</span>
</el-checkbox>
</div>
<el-button type="primary" :loading="loading" @click="onSubmit" size="large" class="login-btn">
</el-button>
</el-form>
<p class="footer-text">
还没有账户
<RouterLink to="/register" class="footer-link">立即注册</RouterLink>
</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted, onUnmounted } from "vue";
import { reactive, ref, onMounted } from "vue";
import { useRouter } from "vue-router";
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
import { useAuthStore } from "../store/auth";
@@ -124,7 +108,6 @@ const auth = useAuthStore();
const router = useRouter();
const formRef = ref<FormInstance>();
const vantaCanvas = ref<HTMLDivElement>();
const form = reactive({
email: "",
password: "",
@@ -140,9 +123,7 @@ const rules: FormRules<typeof form> = {
};
const loading = ref(false);
let vantaEffect: any = null;
// 初始化 Vanta.js 效果
onMounted(() => {
const logoutReason = consumeLogoutReason();
if (logoutReason === LOGOUT_REASON_TIMEOUT) {
@@ -154,61 +135,8 @@ onMounted(() => {
form.password = cached.password;
form.remember = true;
}
// 动态加载 Three.js 和 Vanta.js
loadVantaEffect();
});
onUnmounted(() => {
if (vantaEffect) {
vantaEffect.destroy();
}
});
// 加载 Vanta.js 网络效果
const loadVantaEffect = () => {
// 检查是否已加载
if ((window as any).VANTA && (window as any).THREE) {
initVanta();
return;
}
// 加载 Three.js
const threeScript = document.createElement('script');
threeScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js';
threeScript.onload = () => {
// 加载 Vanta.js NET 效果
const vantaScript = document.createElement('script');
vantaScript.src = 'https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.net.min.js';
vantaScript.onload = () => {
initVanta();
};
document.head.appendChild(vantaScript);
};
document.head.appendChild(threeScript);
};
const initVanta = () => {
if (vantaCanvas.value && (window as any).VANTA) {
vantaEffect = (window as any).VANTA.NET({
el: vantaCanvas.value,
mouseControls: true,
touchControls: true,
gyroControls: false,
minHeight: 200.00,
minWidth: 200.00,
scale: 1.00,
scaleMobile: 1.00,
color: 0xd1d5db,
backgroundColor: 0x171717,
points: 8,
maxDistance: 20.00,
spacing: 18.00,
showDots: true
});
}
};
const onSubmit = async () => {
if (!formRef.value) return;
const valid = await formRef.value.validate();
@@ -251,364 +179,219 @@ const onSubmit = async () => {
</script>
<style scoped>
/* 导入 Inter 字体 */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
/* 主容器 */
.login-container {
position: fixed;
inset: 0;
background: #0a0a0a;
.login-wrapper {
position: relative;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
font-family: 'Inter', sans-serif;
}
/* Vanta 背景 */
.vanta-background {
position: fixed;
inset: 0;
background-color: #f8fafc;
overflow: hidden;
width: 100%;
height: 100%;
z-index: 0;
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
/* 登录包装器 */
.login-wrapper {
.login-bg-shape {
position: absolute;
border-radius: 50%;
filter: blur(80px);
z-index: 0;
opacity: 0.6;
animation: float 10s ease-in-out infinite alternate;
}
.shape-1 {
width: 400px;
height: 400px;
background: #c0cdd7;
top: -100px;
left: -100px;
}
.shape-2 {
width: 500px;
height: 500px;
background: #dde5ec;
bottom: -150px;
right: -100px;
animation-delay: -5s;
}
@keyframes float {
0% {
transform: translate(0, 0) scale(1);
}
100% {
transform: translate(30px, 50px) scale(1.1);
}
}
.login-container {
position: relative;
z-index: 10;
width: 100%;
max-width: 28rem;
max-width: 440px;
padding: 0 20px;
}
/* 卡片容器 - 带渐变边框效果 */
.card-container {
position: relative;
z-index: 0;
.login-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 20px;
padding: 48px 40px 28px;
box-shadow: 0 20px 40px -15px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(255, 255, 255, 0.6) inset;
border: 1px solid rgba(255, 255, 255, 0.5);
}
.card-container::before {
content: "";
position: absolute;
inset: -1px;
background: linear-gradient(to bottom right, #525252, transparent, #262626);
border-radius: 0.75rem;
z-index: -1;
}
/* 卡片内容 */
.card-content {
border-radius: 0.75rem;
overflow: hidden;
background: #171717;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
/* 登录头部 */
.login-header {
height: 140px;
position: relative;
background: #171717;
overflow: hidden;
text-align: center;
margin-bottom: 36px;
}
/* 左上角 */
.header-left {
position: absolute;
top: 1rem;
left: 1rem;
z-index: 10;
}
/* 中间区域 */
.header-center {
position: absolute;
top: 70%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
display: flex;
flex-direction: column;
.logo-box {
display: inline-flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
background: linear-gradient(135deg, #3f5d75, #24496f);
color: #ffffff;
border-radius: 16px;
margin-bottom: 20px;
box-shadow: 0 8px 20px -6px rgba(63, 93, 117, 0.4);
}
/* CTMS Logo */
.ctms-logo {
font-size: 3.5rem;
font-weight: 800;
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: 0.1em;
margin-bottom: 0.5rem;
text-shadow: 0 0 30px rgba(102, 126, 234, 0.3);
animation: glow 3s ease-in-out infinite;
.sys-title {
font-size: 24px;
font-weight: 700;
color: #1e293b;
margin: 0 0 8px;
letter-spacing: -0.5px;
}
@keyframes glow {
0%, 100% {
filter: drop-shadow(0 0 8px rgba(102, 126, 234, 0.4));
}
50% {
filter: drop-shadow(0 0 16px rgba(102, 126, 234, 0.6));
}
}
.secure-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
background: rgba(38, 38, 38, 0.8);
border-radius: 9999px;
font-size: 0.75rem;
color: #a3a3a3;
}
.header-divider {
height: 0.25rem;
width: 3rem;
background: linear-gradient(90deg, #667eea, #764ba2, #f093fb);
margin-top: 0.5rem;
border-radius: 9999px;
}
/* 登录表单区域 */
.login-form-section {
padding: 1.5rem 1.5rem 1.5rem;
display: flex;
flex-direction: column;
background: #171717;
}
.form-label-section {
margin-bottom: 1.5rem;
}
.auth-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
background: #262626;
border-radius: 9999px;
font-size: 0.75rem;
color: #a3a3a3;
margin-bottom: 0.5rem;
}
.form-title {
font-size: 1.25rem;
font-weight: 600;
color: #e5e5e5;
.sys-subtitle {
font-size: 14px;
color: #64748b;
margin: 0;
}
/* 表单样式 */
.login-form {
margin-bottom: 1.5rem;
.login-form :deep(.el-form-item) {
margin-bottom: 22px;
}
.login-form :deep(.el-form-item) {
margin-bottom: 1rem;
.login-form :deep(.el-form-item__label) {
font-weight: 600;
color: #475569;
font-size: 14px;
padding-bottom: 8px;
line-height: 1;
}
.login-form :deep(.el-form-item__error) {
color: #f87171;
font-size: 0.75rem;
margin-top: 0.25rem;
margin-top: 6px;
}
.form-group {
margin-bottom: 1rem;
.login-input :deep(.el-input__wrapper) {
box-shadow: 0 0 0 1px #e2e8f0;
border-radius: 10px;
padding: 2px 14px;
background-color: #ffffff;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.input-label {
display: block;
color: #d4d4d4;
font-size: 0.75rem;
font-weight: 500;
margin-bottom: 0.25rem;
text-transform: uppercase;
letter-spacing: 0.05em;
.login-input :deep(.el-input__wrapper:hover) {
box-shadow: 0 0 0 1px #cbd5e1;
}
.password-label-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.25rem;
.login-input :deep(.el-input__wrapper.is-focus) {
box-shadow: 0 0 0 2px #3f5d75 !important;
background-color: #ffffff;
}
.forgot-link {
color: #a3a3a3;
font-size: 0.75rem;
text-decoration: none;
transition: color 0.2s;
.login-input :deep(.el-input__prefix) {
color: #94a3b8;
margin-right: 8px;
}
.forgot-link:hover {
color: #d4d4d4;
.login-input :deep(.el-input__inner) {
height: 44px;
font-size: 15px;
color: #1e293b;
}
/* 自定义输入框样式 */
.custom-input :deep(.el-input__wrapper) {
background: #262626;
border: 1px solid #404040;
border-radius: 0.5rem;
padding: 0.5rem 1rem;
box-shadow: none;
transition: all 0.2s;
.login-input :deep(.el-input__inner:-webkit-autofill),
.login-input :deep(.el-input__inner:-webkit-autofill:hover),
.login-input :deep(.el-input__inner:-webkit-autofill:focus) {
-webkit-box-shadow: 0 0 0 100px white inset !important;
-webkit-text-fill-color: #1e293b !important;
transition: background-color 5000s ease-in-out 0s;
}
.custom-input :deep(.el-input__wrapper:hover) {
background: #262626;
border-color: #525252;
}
.custom-input :deep(.el-input__wrapper.is-focus) {
background: #262626;
border-color: #525252;
box-shadow: 0 0 0 2px rgba(82, 82, 82, 0.2);
}
.custom-input :deep(.el-input__inner) {
color: #e5e5e5;
font-size: 0.875rem;
}
.custom-input :deep(.el-input__inner::placeholder) {
color: #737373;
}
.custom-input :deep(.el-icon) {
color: #a3a3a3;
}
/* 记住设备行 */
.remember-row {
display: flex;
align-items: center;
margin-bottom: 1rem;
justify-content: flex-start;
margin: -6px 0 18px;
}
.custom-checkbox :deep(.el-checkbox__inner) {
width: 1rem;
height: 1rem;
background: #262626;
border-color: #404040;
}
.custom-checkbox :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background: #262626;
border-color: #404040;
}
.custom-checkbox :deep(.el-checkbox__inner::after) {
border-color: #e5e5e5;
.remember-checkbox :deep(.el-checkbox__label) {
padding-left: 8px;
}
.remember-text {
color: #a3a3a3;
font-size: 0.75rem;
margin-left: 0.5rem;
}
/* 按钮组 */
.button-group {
display: flex;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.btn-icon {
width: 1rem;
height: 1rem;
margin-right: 0.5rem;
color: #64748b;
font-size: 13px;
}
.login-btn {
width: 100%;
height: 2.75rem;
background: #262626;
height: 48px;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
letter-spacing: 2px;
background: linear-gradient(135deg, #3f5d75, #4a6d87);
border: none;
border-radius: 0.5rem;
color: #e5e5e5;
font-weight: 500;
font-size: 0.875rem;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8px 16px -4px rgba(63, 93, 117, 0.3);
transition: all 0.2s;
cursor: pointer;
}
.login-btn:hover {
background: #404040;
transform: translateY(-1px);
box-shadow: 0 12px 20px -6px rgba(63, 93, 117, 0.4);
background: linear-gradient(135deg, #365067, #3f5d75);
}
/* 底部区域 */
.footer-section {
padding-top: 1rem;
text-align: center;
}
.footer-divider {
height: 1px;
background: linear-gradient(to right, transparent, #525252, transparent);
margin-bottom: 1rem;
.login-btn:active {
transform: translateY(1px);
box-shadow: 0 4px 8px -2px rgba(63, 93, 117, 0.3);
}
.footer-text {
color: #a3a3a3;
font-size: 0.75rem;
margin: 0 0 1rem 0;
text-align: center;
color: #94a3b8;
font-size: 13px;
margin: 18px 0 0;
}
.footer-link {
color: #d4d4d4;
color: #3f5d75;
text-decoration: none;
transition: text-decoration 0.2s;
font-weight: 600;
}
.footer-link:hover {
text-decoration: underline;
color: #365067;
}
.status-row {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.status-dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background: #22c55e;
}
.status-text {
color: #a3a3a3;
font-size: 0.75rem;
}
/* 响应式设计 */
@media (max-width: 640px) {
.login-wrapper {
padding: 0.5rem;
.login-card {
padding: 36px 24px 24px;
border-radius: 18px;
}
.login-header {
height: 120px;
}
.login-form-section {
padding: 1.25rem;
.sys-title {
font-size: 22px;
}
}
</style>