Files
stock-analysis/web/auth.html
WarrenZhou e28bfec957 Initial commit: 可转债轮动分析系统
- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换)
- 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制)
- 增量同步:支持 -incr/-since 参数按存档日期增量入库
- 策略回测:净值曲线/10指标/基准对比,含基本面过滤
- 用户认证:用户名密码 + 邮箱验证码双登录方式
- 前端:三Tab SPA(实时行情/策略回测/数据管理)
2026-08-17 20:23:22 +08:00

626 lines
19 KiB
HTML
Raw Permalink 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>登录 / 注册 - 可转债轮动分析系统</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>