522 lines
16 KiB
HTML
522 lines
16 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>登录 - Tradus|AI 金融智能体</title>
|
||
|
||
<!-- Fonts -->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700&family=DM+Serif+Display&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||
|
||
<link rel="stylesheet" href="/static/css/style.css">
|
||
<style>
|
||
/* ========================================
|
||
LOGIN PAGE - ADVANCED STYLING
|
||
======================================== */
|
||
|
||
/* === ATMOSPHERIC BACKGROUND === */
|
||
body {
|
||
background: var(--bg-primary);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
body::before {
|
||
content: '';
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-image:
|
||
radial-gradient(circle at 20% 30%, rgba(102, 126, 234, 0.18) 0%, transparent 50%),
|
||
radial-gradient(circle at 80% 70%, rgba(118, 75, 162, 0.15) 0%, transparent 50%),
|
||
radial-gradient(circle at 50% 100%, rgba(0, 240, 255, 0.1) 0%, transparent 40%);
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
animation: loginBackgroundPulse 8s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes loginBackgroundPulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.7; }
|
||
}
|
||
|
||
#app {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.login-page {
|
||
min-height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: transparent;
|
||
padding: 20px;
|
||
}
|
||
|
||
/* === GLASSMORPHISM LOGIN CONTAINER === */
|
||
.login-container {
|
||
width: 100%;
|
||
max-width: 480px;
|
||
padding: 56px 48px;
|
||
background: rgba(26, 31, 58, 0.95);
|
||
backdrop-filter: blur(20px);
|
||
-webkit-backdrop-filter: blur(20px);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 20px;
|
||
box-shadow:
|
||
0 16px 64px rgba(0, 0, 0, 0.6),
|
||
0 0 40px rgba(0, 240, 255, 0.15),
|
||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||
animation: loginSlideIn 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.login-container::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 3px;
|
||
background: linear-gradient(90deg, #667EEA 0%, #764BA2 50%, #00F0FF 100%);
|
||
opacity: 0.8;
|
||
}
|
||
|
||
@keyframes loginSlideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(40px) scale(0.95);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0) scale(1);
|
||
}
|
||
}
|
||
|
||
/* === HEADER === */
|
||
.login-header {
|
||
text-align: center;
|
||
margin-bottom: 48px;
|
||
}
|
||
|
||
.login-logo {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 16px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.login-logo svg {
|
||
color: var(--accent);
|
||
filter: drop-shadow(0 0 12px rgba(0, 240, 255, 0.6));
|
||
animation: logoFloat 4s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes logoFloat {
|
||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
||
50% { transform: translateY(-8px) rotate(5deg); }
|
||
}
|
||
|
||
.login-title {
|
||
font-size: 32px;
|
||
font-weight: 300;
|
||
font-family: 'DM Serif Display', serif;
|
||
background: linear-gradient(135deg, #667EEA 0%, #764BA2 50%, #00F0FF 100%);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
letter-spacing: 1px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
/* === FORM === */
|
||
.login-form {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24px;
|
||
}
|
||
|
||
.form-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.form-label {
|
||
font-size: 13px;
|
||
color: var(--text-secondary);
|
||
letter-spacing: 0.5px;
|
||
font-weight: 500;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.form-input {
|
||
width: 100%;
|
||
padding: 14px 18px;
|
||
background: rgba(10, 14, 39, 0.6);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 12px;
|
||
color: var(--text-primary);
|
||
font-size: 15px;
|
||
font-family: 'DM Sans', sans-serif;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.form-input:focus {
|
||
outline: none;
|
||
background: rgba(10, 14, 39, 0.9);
|
||
border-color: rgba(0, 240, 255, 0.5);
|
||
box-shadow: 0 0 24px rgba(0, 240, 255, 0.25);
|
||
}
|
||
|
||
.form-input::placeholder {
|
||
color: var(--text-tertiary);
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.code-input-group {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.code-input-group .form-input {
|
||
flex: 1;
|
||
}
|
||
|
||
.send-code-btn {
|
||
padding: 14px 24px;
|
||
background: rgba(26, 31, 58, 0.6);
|
||
border: 1px solid rgba(0, 240, 255, 0.3);
|
||
border-radius: 12px;
|
||
color: var(--accent);
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
transition: all 0.3s ease;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.send-code-btn::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
width: 0;
|
||
height: 0;
|
||
border-radius: 50%;
|
||
background: rgba(0, 240, 255, 0.3);
|
||
transform: translate(-50%, -50%);
|
||
transition: width 0.4s ease, height 0.4s ease;
|
||
}
|
||
|
||
.send-code-btn:hover:not(:disabled) {
|
||
background: rgba(0, 240, 255, 0.15);
|
||
border-color: rgba(0, 240, 255, 0.6);
|
||
box-shadow: 0 0 20px rgba(0, 240, 255, 0.3);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.send-code-btn:active:not(:disabled)::before {
|
||
width: 200%;
|
||
height: 200%;
|
||
}
|
||
|
||
.send-code-btn:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.login-btn {
|
||
width: 100%;
|
||
padding: 16px;
|
||
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%);
|
||
border: none;
|
||
border-radius: 12px;
|
||
color: var(--bg-primary);
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
box-shadow: 0 4px 20px rgba(0, 240, 255, 0.4);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.login-btn::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: -100%;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.3) 50%, transparent 100%);
|
||
transition: left 0.5s ease;
|
||
}
|
||
|
||
.login-btn:hover:not(:disabled) {
|
||
box-shadow: 0 6px 32px rgba(0, 240, 255, 0.6);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.login-btn:hover:not(:disabled)::before {
|
||
left: 100%;
|
||
}
|
||
|
||
.login-btn:active:not(:disabled) {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.login-btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.error-message {
|
||
padding: 14px 18px;
|
||
background: rgba(255, 0, 64, 0.15);
|
||
border: 1px solid rgba(255, 0, 64, 0.4);
|
||
border-radius: 12px;
|
||
color: #ff0040;
|
||
font-size: 13px;
|
||
text-align: center;
|
||
box-shadow: 0 0 20px rgba(255, 0, 64, 0.2);
|
||
animation: errorShake 0.4s ease-out;
|
||
}
|
||
|
||
@keyframes errorShake {
|
||
0%, 100% { transform: translateX(0); }
|
||
25% { transform: translateX(-8px); }
|
||
75% { transform: translateX(8px); }
|
||
}
|
||
|
||
.login-footer {
|
||
margin-top: 32px;
|
||
text-align: center;
|
||
font-size: 12px;
|
||
color: var(--text-tertiary);
|
||
opacity: 0.7;
|
||
}
|
||
|
||
/* === RESPONSIVE === */
|
||
@media (max-width: 768px) {
|
||
.login-container {
|
||
padding: 40px 32px;
|
||
}
|
||
|
||
.login-title {
|
||
font-size: 26px;
|
||
}
|
||
|
||
.code-input-group {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.send-code-btn {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.login-container {
|
||
padding: 32px 24px;
|
||
}
|
||
|
||
.login-title {
|
||
font-size: 22px;
|
||
}
|
||
|
||
.form-input {
|
||
padding: 12px 16px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.login-btn {
|
||
padding: 14px;
|
||
font-size: 15px;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="app" class="login-page">
|
||
<div class="login-container">
|
||
<div class="login-header">
|
||
<div class="login-logo">
|
||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none">
|
||
<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z" fill="currentColor"/>
|
||
</svg>
|
||
</div>
|
||
<h1 class="login-title">Tradus AI</h1>
|
||
</div>
|
||
|
||
<form class="login-form" @submit.prevent="handleLogin">
|
||
<div class="form-group">
|
||
<label class="form-label">手机号</label>
|
||
<input
|
||
type="tel"
|
||
class="form-input"
|
||
v-model="phone"
|
||
placeholder="请输入手机号"
|
||
maxlength="11"
|
||
required
|
||
>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">验证码</label>
|
||
<div class="code-input-group">
|
||
<input
|
||
type="text"
|
||
class="form-input"
|
||
v-model="code"
|
||
placeholder="请输入验证码"
|
||
maxlength="6"
|
||
required
|
||
>
|
||
<button
|
||
type="button"
|
||
class="send-code-btn"
|
||
@click="sendCode"
|
||
:disabled="countdown > 0 || !isPhoneValid"
|
||
>
|
||
{{ countdown > 0 ? `${countdown}秒后重试` : '发送验证码' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="errorMessage" class="error-message">
|
||
{{ errorMessage }}
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
class="login-btn"
|
||
:disabled="loading || !isFormValid"
|
||
>
|
||
{{ loading ? '登录中...' : '登录' }}
|
||
</button>
|
||
</form>
|
||
|
||
<div class="login-footer">
|
||
登录即表示同意服务条款和隐私政策
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
|
||
<script>
|
||
const { createApp } = Vue;
|
||
|
||
createApp({
|
||
data() {
|
||
return {
|
||
phone: '',
|
||
code: '',
|
||
countdown: 0,
|
||
loading: false,
|
||
errorMessage: ''
|
||
};
|
||
},
|
||
computed: {
|
||
isPhoneValid() {
|
||
return /^1[3-9]\d{9}$/.test(this.phone);
|
||
},
|
||
isFormValid() {
|
||
return this.isPhoneValid && this.code.length === 6;
|
||
}
|
||
},
|
||
methods: {
|
||
async sendCode() {
|
||
if (!this.isPhoneValid) {
|
||
this.errorMessage = '请输入正确的手机号';
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/auth/send-code', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
phone: this.phone
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
this.errorMessage = '';
|
||
this.countdown = 60;
|
||
this.startCountdown();
|
||
alert('验证码已发送');
|
||
} else {
|
||
this.errorMessage = data.message || '发送失败';
|
||
}
|
||
} catch (error) {
|
||
console.error('发送验证码失败:', error);
|
||
this.errorMessage = '发送失败,请稍后重试';
|
||
}
|
||
},
|
||
|
||
startCountdown() {
|
||
const timer = setInterval(() => {
|
||
this.countdown--;
|
||
if (this.countdown <= 0) {
|
||
clearInterval(timer);
|
||
}
|
||
}, 1000);
|
||
},
|
||
|
||
async handleLogin() {
|
||
if (!this.isFormValid) {
|
||
return;
|
||
}
|
||
|
||
this.loading = true;
|
||
this.errorMessage = '';
|
||
|
||
try {
|
||
const response = await fetch('/api/auth/login', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
phone: this.phone,
|
||
code: this.code
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success && data.token) {
|
||
// 保存token
|
||
localStorage.setItem('token', data.token);
|
||
// 跳转到主页
|
||
window.location.href = '/app';
|
||
} else {
|
||
this.errorMessage = data.message || '登录失败';
|
||
}
|
||
} catch (error) {
|
||
console.error('登录失败:', error);
|
||
this.errorMessage = '登录失败,请稍后重试';
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
}
|
||
}
|
||
}).mount('#app');
|
||
</script>
|
||
</body>
|
||
</html>
|