- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换) - 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制) - 增量同步:支持 -incr/-since 参数按存档日期增量入库 - 策略回测:净值曲线/10指标/基准对比,含基本面过滤 - 用户认证:用户名密码 + 邮箱验证码双登录方式 - 前端:三Tab SPA(实时行情/策略回测/数据管理)
626 lines
19 KiB
HTML
626 lines
19 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>登录 / 注册 - 可转债轮动分析系统</title>
|
||
<link rel="stylesheet" href="/style.css">
|
||
<style>
|
||
.auth-bg {
|
||
position: fixed;
|
||
top: 0; left: 0; right: 0; bottom: 0;
|
||
background: linear-gradient(135deg, var(--navy) 0%, #0F2240 50%, var(--navy-light) 100%);
|
||
z-index: -1;
|
||
}
|
||
.auth-bg::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: -50%; left: -50%;
|
||
width: 200%; height: 200%;
|
||
background: radial-gradient(ellipse at 30% 20%, rgba(30,95,187,0.15) 0%, transparent 50%),
|
||
radial-gradient(ellipse at 70% 80%, rgba(0,122,94,0.08) 0%, transparent 50%);
|
||
animation: bgShift 30s ease-in-out infinite alternate;
|
||
}
|
||
@keyframes bgShift {
|
||
0% { transform: translate(0, 0); }
|
||
100% { transform: translate(-5%, -5%); }
|
||
}
|
||
|
||
.auth-page {
|
||
min-height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 24px;
|
||
position: relative;
|
||
}
|
||
|
||
.auth-card {
|
||
background: var(--white);
|
||
border-radius: 16px;
|
||
box-shadow: 0 8px 40px rgba(13,27,42,0.20);
|
||
width: 100%;
|
||
max-width: 420px;
|
||
padding: 36px 32px 28px;
|
||
animation: cardIn 0.4s ease-out;
|
||
}
|
||
@keyframes cardIn {
|
||
from { opacity: 0; transform: translateY(20px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.auth-header {
|
||
text-align: center;
|
||
margin-bottom: 28px;
|
||
}
|
||
.auth-header .logo {
|
||
width: 48px;
|
||
height: 48px;
|
||
background: linear-gradient(135deg, var(--blue), var(--navy));
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin: 0 auto 14px;
|
||
font-size: 22px;
|
||
color: var(--white);
|
||
font-weight: 700;
|
||
}
|
||
.auth-header h2 {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: var(--navy);
|
||
margin-bottom: 4px;
|
||
}
|
||
.auth-header p {
|
||
font-size: 13px;
|
||
color: var(--gray-500);
|
||
}
|
||
|
||
.auth-tabs {
|
||
display: flex;
|
||
background: var(--gray-50);
|
||
border-radius: 10px;
|
||
padding: 4px;
|
||
margin-bottom: 24px;
|
||
}
|
||
.auth-tab {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: 10px 0;
|
||
cursor: pointer;
|
||
color: var(--gray-500);
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
border-radius: 8px;
|
||
transition: all var(--transition);
|
||
user-select: none;
|
||
}
|
||
.auth-tab.active {
|
||
background: var(--white);
|
||
color: var(--blue);
|
||
font-weight: 600;
|
||
box-shadow: 0 1px 4px rgba(13,27,42,0.08);
|
||
}
|
||
|
||
.login-method-tabs {
|
||
display: flex;
|
||
gap: 0;
|
||
margin-bottom: 20px;
|
||
border: 1px solid var(--gray-100);
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
.login-method-tab {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: 9px 0;
|
||
cursor: pointer;
|
||
color: var(--gray-500);
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
transition: all var(--transition);
|
||
user-select: none;
|
||
background: var(--gray-50);
|
||
}
|
||
.login-method-tab:first-child {
|
||
border-right: 1px solid var(--gray-100);
|
||
}
|
||
.login-method-tab.active {
|
||
background: var(--white);
|
||
color: var(--blue);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.auth-form {
|
||
transition: opacity 0.2s;
|
||
}
|
||
.auth-form .form-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
margin-bottom: 16px;
|
||
transition: all 0.25s ease;
|
||
}
|
||
.auth-form .form-item[style*="display:none"],
|
||
.auth-form .form-item[style*="display: none"] {
|
||
margin-bottom: 0;
|
||
}
|
||
.auth-form label {
|
||
font-size: 12px;
|
||
color: var(--gray-700);
|
||
font-weight: 500;
|
||
}
|
||
.auth-form .input-wrap {
|
||
position: relative;
|
||
}
|
||
.auth-form input {
|
||
width: 100%;
|
||
padding: 11px 12px;
|
||
border: 1.5px solid var(--gray-200);
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
color: var(--gray-900);
|
||
background: var(--gray-50);
|
||
transition: all var(--transition);
|
||
}
|
||
.auth-form input:focus {
|
||
border-color: var(--blue);
|
||
background: var(--white);
|
||
outline: none;
|
||
box-shadow: 0 0 0 3px var(--blue-light);
|
||
}
|
||
.auth-form input::placeholder {
|
||
color: var(--gray-500);
|
||
}
|
||
|
||
.code-input-group {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
.code-input-group input {
|
||
flex: 1;
|
||
}
|
||
.code-input-group button {
|
||
white-space: nowrap;
|
||
min-width: 100px;
|
||
}
|
||
|
||
.password-toggle {
|
||
position: absolute;
|
||
right: 10px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
background: none;
|
||
border: none;
|
||
color: var(--gray-500);
|
||
cursor: pointer;
|
||
padding: 4px;
|
||
font-size: 16px;
|
||
line-height: 1;
|
||
}
|
||
.password-toggle:hover {
|
||
color: var(--gray-700);
|
||
}
|
||
|
||
.auth-hint {
|
||
text-align: center;
|
||
font-size: 12px;
|
||
color: var(--gray-500);
|
||
margin-top: 20px;
|
||
}
|
||
.auth-hint a {
|
||
color: var(--blue);
|
||
text-decoration: none;
|
||
}
|
||
.auth-hint a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.btn-primary {
|
||
width: 100%;
|
||
padding: 12px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
background: linear-gradient(135deg, var(--blue), #1A52A3);
|
||
color: var(--white);
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all var(--transition);
|
||
box-shadow: 0 2px 8px rgba(30,95,187,0.3);
|
||
position: relative;
|
||
}
|
||
.btn-primary:hover:not(:disabled) {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 14px rgba(30,95,187,0.4);
|
||
}
|
||
.btn-primary:active:not(:disabled) {
|
||
transform: translateY(0);
|
||
}
|
||
.btn-primary:disabled {
|
||
opacity: 0.7;
|
||
cursor: not-allowed;
|
||
}
|
||
.btn-primary .spinner {
|
||
display: none;
|
||
width: 18px;
|
||
height: 18px;
|
||
border: 2px solid rgba(255,255,255,0.3);
|
||
border-top-color: var(--white);
|
||
border-radius: 50%;
|
||
animation: spin 0.6s linear infinite;
|
||
margin: 0 auto;
|
||
}
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
.btn-primary.loading .btn-text { display: none; }
|
||
.btn-primary.loading .spinner { display: block; }
|
||
|
||
.btn-default {
|
||
padding: 9px 16px;
|
||
border: 1.5px solid var(--gray-200);
|
||
border-radius: 8px;
|
||
background: var(--white);
|
||
color: var(--gray-700);
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all var(--transition);
|
||
}
|
||
.btn-default:hover:not(:disabled) {
|
||
border-color: var(--blue);
|
||
color: var(--blue);
|
||
background: var(--blue-light);
|
||
}
|
||
.btn-default:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.error {
|
||
color: var(--red);
|
||
background: var(--red-light);
|
||
border: 1px solid #F5A3B0;
|
||
padding: 10px 14px;
|
||
border-radius: 8px;
|
||
margin-bottom: 16px;
|
||
font-size: 13px;
|
||
display: none;
|
||
animation: shake 0.3s ease-out;
|
||
}
|
||
@keyframes shake {
|
||
0%, 100% { transform: translateX(0); }
|
||
25% { transform: translateX(-4px); }
|
||
75% { transform: translateX(4px); }
|
||
}
|
||
|
||
/* 验证码成功提示 */
|
||
.error.success {
|
||
background: var(--green-light, #E6F4F0) !important;
|
||
border-color: #A3D9B1 !important;
|
||
color: var(--green, #007A5E) !important;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.auth-card { padding: 28px 20px 24px; }
|
||
.auth-header .logo { width: 40px; height: 40px; font-size: 18px; }
|
||
.auth-header h2 { font-size: 17px; }
|
||
.code-input-group { flex-direction: column; }
|
||
.code-input-group button { min-width: 100%; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="auth-bg"></div>
|
||
<div class="auth-page">
|
||
<div class="auth-card">
|
||
<div class="auth-header">
|
||
<div class="logo">KZ</div>
|
||
<h2>可转债轮动分析系统</h2>
|
||
<p>登录 / 注册</p>
|
||
</div>
|
||
|
||
<div class="auth-tabs">
|
||
<div class="auth-tab active" data-mode="login">登录</div>
|
||
<div class="auth-tab" data-mode="register">注册</div>
|
||
</div>
|
||
|
||
<form id="auth-form" class="auth-form">
|
||
<div class="form-item" id="username-row" style="display:none">
|
||
<label>用户名</label>
|
||
<input type="text" id="username" placeholder="输入用户名">
|
||
</div>
|
||
|
||
<div class="form-item" id="email-row">
|
||
<label>邮箱</label>
|
||
<input type="email" id="email" placeholder="name@example.com">
|
||
</div>
|
||
|
||
<!-- 登录方式切换:仅登录模式显示 -->
|
||
<div class="login-method-tabs" id="login-method-tabs" style="display:none">
|
||
<div class="login-method-tab active" data-method="username">密码登录</div>
|
||
<div class="login-method-tab" data-method="email">验证码登录</div>
|
||
</div>
|
||
|
||
<div class="form-item code-row" id="code-row" style="display:none">
|
||
<label>验证码</label>
|
||
<div class="code-input-group">
|
||
<input type="text" id="code" placeholder="6位验证码" maxlength="6">
|
||
<button type="button" class="btn-default" id="send-code">获取验证码</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-item password-row" id="password-row" style="display:none">
|
||
<label>密码</label>
|
||
<div class="input-wrap">
|
||
<input type="password" id="password" placeholder="不少于6位">
|
||
<button type="button" class="password-toggle" id="pwd-toggle" onclick="togglePassword()">👁</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-item" id="login-hint" style="display:none">
|
||
<span style="font-size:12px;color:var(--gray-500)">管理员账号:admin / 123456</span>
|
||
</div>
|
||
|
||
<div id="auth-error" class="error" style="display:none"></div>
|
||
|
||
<button type="button" class="btn-primary" id="submit-btn" onclick="handleAuthSubmit()">
|
||
<span class="btn-text">登录</span>
|
||
<div class="spinner"></div>
|
||
</button>
|
||
</form>
|
||
|
||
<div class="auth-hint">
|
||
登录即表示同意 <a href="#">服务条款</a> 与 <a href="#">隐私政策</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
console.log('[auth] page script loaded');
|
||
const modeTabs = document.querySelectorAll('.auth-tab');
|
||
const methodTabs = document.querySelectorAll('.login-method-tab');
|
||
const methodTabsWrap = document.getElementById('login-method-tabs');
|
||
const submitBtn = document.getElementById('submit-btn');
|
||
const usernameRow = document.getElementById('username-row');
|
||
const emailRow = document.getElementById('email-row');
|
||
const codeRow = document.getElementById('code-row');
|
||
const passwordRow = document.getElementById('password-row');
|
||
const sendBtn = document.getElementById('send-code');
|
||
const form = document.getElementById('auth-form');
|
||
const errorEl = document.getElementById('auth-error');
|
||
const usernameInput = document.getElementById('username');
|
||
const emailInput = document.getElementById('email');
|
||
const codeInput = document.getElementById('code');
|
||
const passwordInput = document.getElementById('password');
|
||
const loginHint = document.getElementById('login-hint');
|
||
|
||
let mode = 'login';
|
||
let loginMethod = 'username'; // 默认密码登录
|
||
|
||
function togglePassword() {
|
||
const pwd = document.getElementById('password');
|
||
const btn = document.getElementById('pwd-toggle');
|
||
if (pwd.type === 'password') {
|
||
pwd.type = 'text';
|
||
btn.textContent = '🙈';
|
||
} else {
|
||
pwd.type = 'password';
|
||
btn.textContent = '👁';
|
||
}
|
||
}
|
||
|
||
function applyMode() {
|
||
const btnText = submitBtn.querySelector('.btn-text');
|
||
btnText.textContent = mode === 'login' ? '登录' : '注册';
|
||
const isLogin = mode === 'login';
|
||
const isEmailLogin = isLogin && loginMethod === 'email';
|
||
const isUsernameLogin = isLogin && loginMethod === 'username';
|
||
|
||
// 用户名行:用户名登录模式 或 注册模式显示
|
||
usernameRow.style.display = isUsernameLogin || mode === 'register' ? 'flex' : 'none';
|
||
usernameInput.required = isUsernameLogin || mode === 'register';
|
||
|
||
// 邮箱行:邮箱登录 或 注册模式显示
|
||
emailRow.style.display = isEmailLogin || mode === 'register' ? 'flex' : 'none';
|
||
emailInput.required = isEmailLogin || mode === 'register';
|
||
|
||
// 验证码行:邮箱登录 或 注册模式显示
|
||
codeRow.style.display = isEmailLogin || mode === 'register' ? 'flex' : 'none';
|
||
codeInput.required = isEmailLogin || mode === 'register';
|
||
|
||
// 密码行:用户名登录 或 注册模式显示
|
||
passwordRow.style.display = isUsernameLogin || mode === 'register' ? 'flex' : 'none';
|
||
passwordInput.required = isUsernameLogin || mode === 'register';
|
||
|
||
// 登录方式切换仅登录模式显示
|
||
methodTabsWrap.style.display = isLogin ? 'flex' : 'none';
|
||
// 提示信息
|
||
loginHint.style.display = isUsernameLogin ? 'flex' : 'none';
|
||
|
||
errorEl.style.display = 'none';
|
||
}
|
||
|
||
modeTabs.forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
modeTabs.forEach(t => t.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
mode = tab.dataset.mode;
|
||
applyMode();
|
||
});
|
||
});
|
||
|
||
methodTabs.forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
methodTabs.forEach(t => t.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
loginMethod = tab.dataset.method;
|
||
applyMode();
|
||
});
|
||
});
|
||
|
||
function showError(msg, type) {
|
||
errorEl.textContent = msg;
|
||
errorEl.style.display = 'block';
|
||
if (type === 'success') {
|
||
errorEl.style.background = 'var(--green-light)';
|
||
errorEl.style.borderColor = '#A3D9B1';
|
||
errorEl.style.color = 'var(--green)';
|
||
} else {
|
||
errorEl.style.background = 'var(--red-light)';
|
||
errorEl.style.borderColor = '#F5A3B0';
|
||
errorEl.style.color = 'var(--red)';
|
||
}
|
||
}
|
||
|
||
function clearError() {
|
||
errorEl.style.display = 'none';
|
||
}
|
||
|
||
sendBtn.addEventListener('click', async () => {
|
||
clearError();
|
||
const email = emailInput.value.trim();
|
||
if (!email) {
|
||
showError('请输入邮箱');
|
||
emailInput.focus();
|
||
return;
|
||
}
|
||
sendBtn.disabled = true;
|
||
sendBtn.textContent = '发送中...';
|
||
try {
|
||
const resp = await fetch('/api/auth/send-code', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ email, purpose: mode }),
|
||
});
|
||
const data = await resp.json();
|
||
if (!resp.ok) throw new Error(data.error || '发送失败');
|
||
|
||
showError('验证码已发送到邮箱,请查看服务器日志', 'success');
|
||
let sec = 60;
|
||
sendBtn.textContent = sec + 's';
|
||
const timer = setInterval(() => {
|
||
sec--;
|
||
if (sec <= 0) {
|
||
clearInterval(timer);
|
||
sendBtn.disabled = false;
|
||
sendBtn.textContent = '获取验证码';
|
||
} else {
|
||
sendBtn.textContent = sec + 's';
|
||
}
|
||
}, 1000);
|
||
} catch (e) {
|
||
showError(e.message);
|
||
sendBtn.disabled = false;
|
||
sendBtn.textContent = '获取验证码';
|
||
}
|
||
});
|
||
|
||
async function handleAuthSubmit() {
|
||
clearError();
|
||
const username = usernameInput.value.trim();
|
||
const email = emailInput.value.trim();
|
||
const code = codeInput.value.trim();
|
||
const password = passwordInput.value;
|
||
|
||
let endpoint, body;
|
||
if (mode === 'register') {
|
||
if (!username || !email || !code || !password) {
|
||
showError('请填写所有字段');
|
||
return;
|
||
}
|
||
if (password.length < 6) {
|
||
showError('密码长度不能少于6位');
|
||
return;
|
||
}
|
||
endpoint = '/api/auth/register';
|
||
body = { username, email, code, password };
|
||
} else if (loginMethod === 'username') {
|
||
if (!username) {
|
||
showError('请输入用户名');
|
||
return;
|
||
}
|
||
if (!password) {
|
||
showError('请输入密码');
|
||
return;
|
||
}
|
||
endpoint = '/api/auth/login';
|
||
body = { username, password };
|
||
} else {
|
||
// 邮箱登录
|
||
if (!email) {
|
||
showError('请输入邮箱');
|
||
return;
|
||
}
|
||
if (!code) {
|
||
showError('请输入验证码');
|
||
return;
|
||
}
|
||
endpoint = '/api/auth/login';
|
||
body = { email, code };
|
||
}
|
||
|
||
submitBtn.disabled = true;
|
||
submitBtn.classList.add('loading');
|
||
try {
|
||
console.log('[auth] sending', endpoint, JSON.stringify(body));
|
||
const resp = await fetch(endpoint, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body),
|
||
});
|
||
console.log('[auth] response status', resp.status);
|
||
const data = await resp.json();
|
||
console.log('[auth] response data', data);
|
||
if (!resp.ok) {
|
||
throw new Error(data.error || ('请求失败: ' + resp.status));
|
||
}
|
||
if (!data.token) {
|
||
throw new Error('响应中缺少 token');
|
||
}
|
||
|
||
localStorage.setItem('auth_token', data.token);
|
||
localStorage.setItem('auth_username', data.username || '');
|
||
localStorage.setItem('auth_email', data.email || email);
|
||
// 设置 cookie 供服务端 requireAuth 中间件校验
|
||
document.cookie = 'auth_token=' + data.token + '; path=/; max-age=86400';
|
||
console.log('[auth] redirecting to /');
|
||
window.location.href = '/';
|
||
} catch (e) {
|
||
console.error('[auth] error', e);
|
||
showError(e.message || '请求失败,请稍后重试');
|
||
} finally {
|
||
submitBtn.disabled = false;
|
||
submitBtn.classList.remove('loading');
|
||
}
|
||
}
|
||
|
||
// 兼容回车提交
|
||
form.addEventListener('submit', e => {
|
||
e.preventDefault();
|
||
handleAuthSubmit();
|
||
});
|
||
|
||
// 自动聚焦第一个可见输入框
|
||
function autoFocus() {
|
||
const visibleInputs = Array.from(form.querySelectorAll('input:not([type="hidden"])'))
|
||
.filter(inp => inp.offsetParent !== null);
|
||
if (visibleInputs.length > 0) {
|
||
setTimeout(() => visibleInputs[0].focus(), 100);
|
||
}
|
||
}
|
||
|
||
// 输入框切换时自动聚焦
|
||
const observer = new MutationObserver(() => autoFocus());
|
||
observer.observe(document.getElementById('username-row'), { attributes: true, attributeFilter: ['style'] });
|
||
observer.observe(document.getElementById('email-row'), { attributes: true, attributeFilter: ['style'] });
|
||
observer.observe(document.getElementById('code-row'), { attributes: true, attributeFilter: ['style'] });
|
||
observer.observe(document.getElementById('password-row'), { attributes: true, attributeFilter: ['style'] });
|
||
|
||
applyMode();
|
||
autoFocus();
|
||
</script>
|
||
</body>
|
||
</html> |