<template>
|
<div class="login-container">
|
<!-- 动态背景 -->
|
<div class="background-animation">
|
<div class="floating-shapes">
|
<div class="shape shape-1"></div>
|
<div class="shape shape-2"></div>
|
<div class="shape shape-3"></div>
|
<div class="shape shape-4"></div>
|
</div>
|
<div class="network-lines"></div>
|
<div class="gradient-overlay"></div>
|
</div>
|
|
<!-- 主登录区域 -->
|
<div class="login-content">
|
<!-- 左侧品牌展示区 -->
|
<div class="brand-section">
|
<div class="brand-logo-container">
|
<div class="logo-icon">
|
<svg viewBox="0 0 100 100" class="api-icon">
|
<!-- 主API连接图标 -->
|
<path d="M20,30 L50,10 L80,30 L80,70 L50,90 L20,70 Z" fill="rgba(0,212,255,0.1)" stroke="rgba(0,212,255,0.6)" stroke-width="2"/>
|
<!-- 内部连接层 -->
|
<path d="M30,40 L50,25 L70,40 L70,60 L50,75 L30,60 Z" fill="rgba(148,0,211,0.1)" stroke="rgba(148,0,211,0.6)" stroke-width="1.5"/>
|
<!-- 核心数据点 -->
|
<path d="M40,50 L50,40 L60,50 L60,55 L50,60 L40,55 Z" fill="rgba(255,255,255,0.2)" stroke="rgba(255,255,255,0.8)" stroke-width="1"/>
|
<!-- 动态连接线 -->
|
<line x1="35" y1="45" x2="45" y2="52" stroke="#00d4ff" stroke-width="1.5" stroke-dasharray="2,2"/>
|
<line x1="55" y1="45" x2="65" y2="52" stroke="#9400d3" stroke-width="1.5" stroke-dasharray="2,2"/>
|
<!-- 中心数据流点 -->
|
<circle cx="50" cy="50" r="4" fill="#00d4ff">
|
<animate attributeName="r" values="4;6;4" dur="2s" repeatCount="indefinite"/>
|
<animate attributeName="opacity" values="1;0.7;1" dur="2s" repeatCount="indefinite"/>
|
</circle>
|
<!-- 外围数据点 -->
|
<circle cx="35" cy="35" r="2" fill="#00d4ff">
|
<animate attributeName="opacity" values="0.5;1;0.5" dur="1.5s" repeatCount="indefinite"/>
|
</circle>
|
<circle cx="65" cy="35" r="2" fill="#9400d3">
|
<animate attributeName="opacity" values="0.5;1;0.5" dur="1.5s" repeatCount="indefinite" begin="0.5s"/>
|
</circle>
|
<circle cx="35" cy="65" r="2" fill="#9400d3">
|
<animate attributeName="opacity" values="0.5;1;0.5" dur="1.5s" repeatCount="indefinite" begin="1s"/>
|
</circle>
|
<circle cx="65" cy="65" r="2" fill="#00d4ff">
|
<animate attributeName="opacity" values="0.5;1;0.5" dur="1.5s" repeatCount="indefinite" begin="1.5s"/>
|
</circle>
|
</svg>
|
</div>
|
<h1 class="system-name">APITest Pro</h1>
|
<p class="system-desc">智能接口自动化测试平台</p>
|
</div>
|
|
<div class="feature-list">
|
<div class="feature-item">
|
<span class="feature-icon">🚀</span>
|
<span>高效自动化测试</span>
|
</div>
|
<div class="feature-item">
|
<span class="feature-icon">🔗</span>
|
<span>智能接口管理</span>
|
</div>
|
<div class="feature-item">
|
<span class="feature-icon">📊</span>
|
<span>实时数据监控</span>
|
</div>
|
</div>
|
</div>
|
|
<!-- 右侧登录表单 -->
|
<div class="login-form-section">
|
<div class="form-container">
|
<div class="form-header">
|
<h2>欢迎登录</h2>
|
<p>请使用您的账号密码登录系统</p>
|
</div>
|
|
<el-form
|
ref="loginForm"
|
@submit.native.prevent="submitForm"
|
class="login-form"
|
>
|
<div class="input-group">
|
<label class="input-label">用户名</label>
|
<el-input
|
v-model="loginForm.username"
|
placeholder="请输入用户名"
|
prefix-icon="el-icon-user"
|
class="modern-input"
|
:class="{ 'input-error': usernameInvalid }"
|
@blur="validateUserName"
|
/>
|
<transition name="slide-fade">
|
<div v-if="usernameInvalid" class="error-msg">{{ usernameInvalid }}</div>
|
</transition>
|
</div>
|
|
<div class="input-group">
|
<label class="input-label">密码</label>
|
<el-input
|
v-model="loginForm.password"
|
type="password"
|
placeholder="请输入密码"
|
prefix-icon="el-icon-lock"
|
show-password
|
class="modern-input"
|
:class="{ 'input-error': passwordInvalid }"
|
@blur="validatePassword"
|
/>
|
<transition name="slide-fade">
|
<div v-if="passwordInvalid" class="error-msg">{{ passwordInvalid }}</div>
|
</transition>
|
</div>
|
|
<el-button
|
type="primary"
|
class="login-button"
|
:loading="isLoading"
|
@click="submitForm"
|
>
|
{{ isLoading ? '登录中...' : '登录系统' }}
|
</el-button>
|
</el-form>
|
|
<div class="form-footer">
|
<p>还没有账号?<a href="#" class="register-link">联系管理员注册</a></p>
|
<p class="copyright">© 2025 APITest Pro 智能接口自动化平台</p>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script>
|
export default {
|
name: "Login",
|
|
data() {
|
return {
|
isLoading: false,
|
loginForm: {
|
username: "",
|
password: ""
|
},
|
usernameInvalid: "",
|
passwordInvalid: ""
|
};
|
},
|
|
methods: {
|
validateUserName() {
|
if (this.loginForm.username.replace(/(^\s*)/g, "") === "") {
|
this.usernameInvalid = "用户名不能为空";
|
return false;
|
}
|
return true;
|
},
|
validatePassword() {
|
if (this.loginForm.password.replace(/(^\s*)/g, "") === "") {
|
this.passwordInvalid = "密码不能为空";
|
return false;
|
}
|
return true;
|
},
|
handleLoginSuccess(resp) {
|
if (resp.success) {
|
// 显示登录信息提示
|
this.showLoginNotification(resp);
|
|
// 原有路由跳转和存储逻辑
|
this.$router.push({ name: "ProjectList" });
|
this.$store.commit("isLogin", resp.token);
|
this.$store.commit("setUser", resp.user);
|
this.$store.commit("setName", resp.name);
|
this.$store.commit("setId", resp.id);
|
this.$store.commit("setIsSuperuser", resp.is_superuser);
|
this.$store.commit("setRouterName", "ProjectList");
|
this.$store.commit("setShowHosts", resp.show_hosts);
|
|
this.setLocalValue("token", resp.token);
|
this.setLocalValue("user", resp.user);
|
this.setLocalValue("name", resp.name);
|
this.setLocalValue("id", resp.id);
|
this.setLocalValue("is_superuser", resp.is_superuser);
|
this.setLocalValue("routerName", "ProjectList");
|
this.setLocalValue("show_hosts", resp.show_hosts);
|
} else {
|
this.$message.error({
|
message: resp.msg,
|
duration: 2000,
|
center: true
|
});
|
}
|
},
|
|
showLoginNotification(resp) {
|
const currentTime = new Date().toLocaleString();
|
|
this.$notify({
|
title: '登录成功',
|
message: `登录时间: ${currentTime}`,
|
type: 'success',
|
duration: 3000,
|
position: 'top-right'
|
});
|
},
|
submitForm() {
|
if (this.validateUserName() && this.validatePassword()) {
|
this.isLoading = true;
|
this.$api.login(this.loginForm).then(resp => {
|
this.handleLoginSuccess(resp);
|
this.isLoading = false;
|
});
|
}
|
}
|
}
|
};
|
</script>
|
|
<style scoped>
|
.login-container {
|
min-height: 100vh;
|
background: linear-gradient(135deg, #0c0e27 0%, #1a1f3d 50%, #2d1b69 100%);
|
position: relative;
|
overflow: hidden;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
}
|
|
/* 动态背景动画 */
|
.background-animation {
|
position: absolute;
|
width: 100%;
|
height: 100%;
|
top: 0;
|
left: 0;
|
}
|
|
.floating-shapes {
|
position: absolute;
|
width: 100%;
|
height: 100%;
|
}
|
|
.shape {
|
position: absolute;
|
border-radius: 50%;
|
background: linear-gradient(45deg, rgba(0, 212, 255, 0.1), rgba(148, 0, 211, 0.1));
|
animation: float 20s infinite linear;
|
}
|
|
.shape-1 {
|
width: 100px;
|
height: 100px;
|
top: 10%;
|
left: 10%;
|
animation-delay: 0s;
|
}
|
|
.shape-2 {
|
width: 150px;
|
height: 150px;
|
top: 60%;
|
right: 10%;
|
animation-delay: -5s;
|
}
|
|
.shape-3 {
|
width: 80px;
|
height: 80px;
|
bottom: 20%;
|
left: 20%;
|
animation-delay: -10s;
|
}
|
|
.shape-4 {
|
width: 120px;
|
height: 120px;
|
top: 30%;
|
right: 30%;
|
animation-delay: -15s;
|
}
|
|
.network-lines {
|
position: absolute;
|
width: 100%;
|
height: 100%;
|
background-image:
|
linear-gradient(90deg, transparent 24px, rgba(0, 212, 255, 0.03) 25px, rgba(0, 212, 255, 0.03) 26px, transparent 27px, transparent 74px, rgba(148, 0, 211, 0.03) 75px, rgba(148, 0, 211, 0.03) 76px, transparent 77px),
|
linear-gradient(0deg, transparent 24px, rgba(0, 212, 255, 0.03) 25px, rgba(0, 212, 255, 0.03) 26px, transparent 27px, transparent 74px, rgba(148, 0, 211, 0.03) 75px, rgba(148, 0, 211, 0.03) 76px, transparent 77px);
|
background-size: 100px 100px;
|
animation: gridMove 40s linear infinite;
|
}
|
|
.gradient-overlay {
|
position: absolute;
|
width: 100%;
|
height: 100%;
|
background: radial-gradient(circle at 20% 80%, rgba(0, 212, 255, 0.1) 0%, transparent 50%),
|
radial-gradient(circle at 80% 20%, rgba(148, 0, 211, 0.1) 0%, transparent 50%);
|
}
|
|
/* 主内容区域 */
|
.login-content {
|
display: flex;
|
min-height: 100vh;
|
max-width: 1400px;
|
margin: 0 auto;
|
position: relative;
|
z-index: 2;
|
}
|
|
/* 左侧品牌区域 */
|
.brand-section {
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
padding: 0 4rem;
|
color: white;
|
}
|
|
.brand-logo-container {
|
margin-bottom: 3rem;
|
}
|
|
.logo-icon {
|
width: 120px;
|
height: 120px;
|
margin-bottom: 1.5rem;
|
filter: drop-shadow(0 0 20px rgba(0, 212, 255, 0.5));
|
}
|
|
.api-icon {
|
width: 100%;
|
height: 100%;
|
animation: pulse 3s ease-in-out infinite;
|
}
|
|
.system-name {
|
font-size: 3rem;
|
font-weight: 700;
|
margin-bottom: 0.5rem;
|
background: linear-gradient(135deg, #00d4ff 0%, #9400d3 100%);
|
-webkit-background-clip: text;
|
-webkit-text-fill-color: transparent;
|
background-clip: text;
|
}
|
|
.system-desc {
|
font-size: 1.2rem;
|
opacity: 0.8;
|
margin: 0;
|
}
|
|
.feature-list {
|
margin-top: 2rem;
|
}
|
|
.feature-item {
|
display: flex;
|
align-items: center;
|
margin-bottom: 1rem;
|
font-size: 1.1rem;
|
opacity: 0.9;
|
transition: opacity 0.3s ease;
|
}
|
|
.feature-item:hover {
|
opacity: 1;
|
}
|
|
.feature-icon {
|
font-size: 1.5rem;
|
margin-right: 0.8rem;
|
filter: drop-shadow(0 0 10px rgba(0, 212, 255, 0.5));
|
}
|
|
/* 右侧表单区域 */
|
.login-form-section {
|
flex: 0 0 500px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
padding: 2rem;
|
}
|
|
.form-container {
|
background: rgba(255, 255, 255, 0.95);
|
backdrop-filter: blur(20px);
|
border-radius: 20px;
|
padding: 3rem 2.5rem;
|
width: 100%;
|
max-width: 400px;
|
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
}
|
|
.form-header {
|
text-align: center;
|
margin-bottom: 2.5rem;
|
}
|
|
.form-header h2 {
|
font-size: 2rem;
|
font-weight: 600;
|
margin-bottom: 0.5rem;
|
color: #1a1f3d;
|
}
|
|
.form-header p {
|
color: #666;
|
margin: 0;
|
}
|
|
.input-group {
|
margin-bottom: 1.5rem;
|
}
|
|
.input-label {
|
display: block;
|
margin-bottom: 0.5rem;
|
font-weight: 500;
|
color: #333;
|
font-size: 0.9rem;
|
}
|
|
.modern-input {
|
width: 100%;
|
}
|
|
.modern-input >>> .el-input__inner {
|
height: 48px;
|
border-radius: 12px;
|
border: 2px solid #e1e5e9;
|
font-size: 1rem;
|
transition: all 0.3s ease;
|
background: #f8f9fa;
|
}
|
|
.modern-input >>> .el-input__inner:focus {
|
border-color: #00d4ff;
|
box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1);
|
background: white;
|
}
|
|
.modern-input.input-error >>> .el-input__inner {
|
border-color: #ff4757;
|
}
|
|
.login-button {
|
width: 100%;
|
height: 50px;
|
border-radius: 12px;
|
font-size: 1.1rem;
|
font-weight: 600;
|
background: linear-gradient(135deg, #00d4ff 0%, #9400d3 100%);
|
border: none;
|
transition: all 0.3s ease;
|
margin-top: 0.5rem;
|
}
|
|
.login-button:hover {
|
transform: translateY(-2px);
|
box-shadow: 0 10px 25px rgba(0, 212, 255, 0.4);
|
}
|
|
.login-button:active {
|
transform: translateY(0);
|
}
|
|
.error-msg {
|
color: #ff4757;
|
font-size: 0.85rem;
|
margin-top: 0.3rem;
|
animation: slideIn 0.3s ease;
|
}
|
|
.form-footer {
|
margin-top: 2rem;
|
text-align: center;
|
}
|
|
.form-footer p {
|
margin: 0.5rem 0;
|
color: #666;
|
font-size: 0.9rem;
|
}
|
|
.register-link {
|
color: #00d4ff;
|
text-decoration: none;
|
font-weight: 500;
|
}
|
|
.register-link:hover {
|
text-decoration: underline;
|
}
|
|
.copyright {
|
opacity: 0.6;
|
font-size: 0.8rem;
|
}
|
|
/* 动画效果 */
|
@keyframes float {
|
0%, 100% {
|
transform: translateY(0px) rotate(0deg);
|
}
|
33% {
|
transform: translateY(-20px) rotate(120deg);
|
}
|
66% {
|
transform: translateY(10px) rotate(240deg);
|
}
|
}
|
|
@keyframes gridMove {
|
0% {
|
transform: translate(0, 0);
|
}
|
100% {
|
transform: translate(100px, 100px);
|
}
|
}
|
|
@keyframes pulse {
|
0%, 100% {
|
transform: scale(1);
|
opacity: 1;
|
}
|
50% {
|
transform: scale(1.05);
|
opacity: 0.8;
|
}
|
}
|
|
@keyframes slideIn {
|
from {
|
opacity: 0;
|
transform: translateY(-10px);
|
}
|
to {
|
opacity: 1;
|
transform: translateY(0);
|
}
|
}
|
|
.slide-fade-enter-active {
|
transition: all 0.3s ease;
|
}
|
|
.slide-fade-leave-active {
|
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
|
}
|
|
.slide-fade-enter, .slide-fade-leave-to {
|
transform: translateY(-10px);
|
opacity: 0;
|
}
|
|
/* 响应式设计 */
|
@media (max-width: 1024px) {
|
.login-content {
|
flex-direction: column;
|
}
|
|
.brand-section {
|
padding: 2rem;
|
text-align: center;
|
}
|
|
.login-form-section {
|
flex: none;
|
width: 100%;
|
}
|
}
|
|
@media (max-width: 768px) {
|
.form-container {
|
margin: 1rem;
|
padding: 2rem 1.5rem;
|
}
|
|
.system-name {
|
font-size: 2.5rem;
|
}
|
}
|
|
|
.system-name {
|
font-size: 2rem; /* 同步放大系统名称字号 */
|
margin-top: 0.5rem; /* 增加与LOGO的间距 */
|
}
|
|
.system-name {
|
color: #2c3e50;
|
font-size: 1.8rem;
|
font-weight: 600;
|
letter-spacing: 1px;
|
margin: 0;
|
}
|
|
.login-form {
|
margin: 2rem 0;
|
}
|
|
.custom-input {
|
transition: all 0.3s ease;
|
}
|
|
.custom-input:hover {
|
transform: translateY(-2px);
|
}
|
|
.input-error >>> .el-input__inner {
|
border-color: #ff4757;
|
}
|
|
.error-msg {
|
color: #ff4757;
|
font-size: 0.85rem;
|
margin-top: 4px;
|
animation: shake 0.4s ease;
|
}
|
|
@keyframes shake {
|
0%, 100% { transform: translateX(0); }
|
25% { transform: translateX(5px); }
|
75% { transform: translateX(-5px); }
|
}
|
|
.login-button {
|
width: 100%;
|
height: 45px;
|
font-size: 1.1rem;
|
letter-spacing: 2px;
|
border-radius: 8px;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
border: none;
|
transition: all 0.3s ease;
|
}
|
|
.login-button:hover {
|
transform: translateY(-2px);
|
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
|
}
|
</style>
|