stock-ai-agent/frontend/login.html
2026-03-28 23:28:54 +08:00

522 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - TradusAI 金融智能体</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>