Files
zsglpt/templates/login.html
yuyx 87b04e84d3 fix: 增大验证码显示尺寸
前端验证码图片高度从36/40px增大到50px,配合后端生成的160x60图片

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 22:49:35 +08:00

449 lines
26 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>登录 - 知识管理平台</title>
<style>
:root {
--md-primary: #1976D2;
--md-primary-dark: #1565C0;
--md-primary-light: #BBDEFB;
--md-background: #FAFAFA;
--md-surface: #FFFFFF;
--md-error: #B00020;
--md-success: #4CAF50;
--md-on-primary: #FFFFFF;
--md-on-surface: #212121;
--md-on-surface-medium: #666666;
--md-shadow-lg: 0 8px 30px rgba(0,0,0,0.12);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.login-card {
background: var(--md-surface);
border-radius: 16px;
box-shadow: var(--md-shadow-lg);
width: 100%;
max-width: 400px;
overflow: hidden;
}
.login-header {
background: var(--md-primary);
color: var(--md-on-primary);
padding: 32px 24px;
text-align: center;
}
.login-header .logo { font-size: 48px; margin-bottom: 12px; }
.login-header h1 { font-size: 24px; font-weight: 500; margin-bottom: 4px; }
.login-header p { font-size: 14px; opacity: 0.9; }
.login-body { padding: 32px 24px; }
.form-group { margin-bottom: 24px; }
.form-group label { display: block; font-size: 14px; font-weight: 500; color: var(--md-on-surface-medium); margin-bottom: 8px; }
.form-group input {
width: 100%;
padding: 14px 16px;
border: 2px solid #E0E0E0;
border-radius: 8px;
font-size: 16px;
transition: all 0.2s;
background: #FAFAFA;
}
.form-group input:focus {
outline: none;
border-color: var(--md-primary);
background: var(--md-surface);
box-shadow: 0 0 0 3px var(--md-primary-light);
}
.captcha-row { display: flex; gap: 12px; align-items: center; }
.captcha-row input { flex: 1; }
.captcha-code {
font-size: 20px;
font-weight: bold;
letter-spacing: 4px;
color: var(--md-primary);
padding: 10px 16px;
background: var(--md-primary-light);
border-radius: 8px;
user-select: none;
}
.captcha-refresh {
padding: 10px 16px;
background: #F5F5F5;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 18px;
}
.captcha-refresh:hover { background: #EEEEEE; }
.forgot-link { text-align: right; margin-top: -16px; margin-bottom: 24px; }
.forgot-link a { color: var(--md-primary); text-decoration: none; font-size: 14px; font-weight: 500; }
.forgot-link a:hover { text-decoration: underline; }
.btn-login {
width: 100%;
padding: 16px;
background: var(--md-primary);
color: var(--md-on-primary);
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
text-transform: uppercase;
letter-spacing: 1px;
}
.btn-login:hover { background: var(--md-primary-dark); box-shadow: 0 4px 12px rgba(25, 118, 210, 0.4); }
.register-link {
text-align: center;
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid #E0E0E0;
color: var(--md-on-surface-medium);
font-size: 14px;
}
.register-link a { color: var(--md-primary); text-decoration: none; font-weight: 600; }
.register-link a:hover { text-decoration: underline; }
.message { padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; font-size: 14px; display: none; }
.message.error { background: #FFEBEE; color: var(--md-error); border: 1px solid #FFCDD2; }
.message.success { background: #E8F5E9; color: var(--md-success); border: 1px solid #C8E6C9; }
.modal-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
display: flex; justify-content: center; align-items: center;
opacity: 0; visibility: hidden; transition: all 0.3s; z-index: 1000; padding: 20px;
}
.modal-overlay.active { opacity: 1; visibility: visible; }
.modal {
background: var(--md-surface);
border-radius: 16px;
width: 100%; max-width: 400px;
box-shadow: var(--md-shadow-lg);
transform: translateY(-20px); transition: transform 0.3s;
}
.modal-overlay.active .modal { transform: translateY(0); }
.modal-header { padding: 24px; border-bottom: 1px solid #E0E0E0; }
.modal-header h2 { font-size: 20px; font-weight: 500; margin-bottom: 4px; }
.modal-header p { font-size: 14px; color: var(--md-on-surface-medium); }
.modal-body { padding: 24px; }
.modal-footer { padding: 16px 24px; border-top: 1px solid #E0E0E0; display: flex; gap: 12px; justify-content: flex-end; }
.btn-secondary { padding: 12px 24px; background: #F5F5F5; color: var(--md-on-surface); border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; }
.btn-secondary:hover { background: #EEEEEE; }
.btn-primary { padding: 12px 24px; background: var(--md-primary); color: var(--md-on-primary); border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; }
.btn-primary:hover { background: var(--md-primary-dark); }
@media (max-width: 480px) {
body { padding: 12px; }
.login-card { max-width: 100%; }
.login-header { padding: 24px 20px; }
.login-header .logo { font-size: 40px; margin-bottom: 10px; }
.login-header h1 { font-size: 20px; }
.login-header p { font-size: 13px; }
.login-body { padding: 24px 20px; }
.form-group { margin-bottom: 20px; }
.form-group label { font-size: 13px; }
.form-group input { padding: 12px 14px; font-size: 16px; } /* iOS防止自动缩放 */
.captcha-code { padding: 8px 12px; font-size: 18px; letter-spacing: 3px; }
.captcha-refresh { padding: 8px 12px; font-size: 16px; }
.btn-login { padding: 14px; font-size: 15px; }
.modal { max-width: 100%; }
.modal-header, .modal-body { padding: 20px; }
.modal-header h2 { font-size: 18px; }
.modal-footer { padding: 14px 20px; flex-direction: column; }
.modal-footer button { width: 100%; }
}
</style>
</head>
<body>
<div class="login-card">
<div class="login-header">
<div class="logo">📚</div>
<h1>知识管理平台</h1>
<p>自动化浏览学习内容</p>
</div>
<div class="login-body">
<div id="errorMessage" class="message error"></div>
<div id="successMessage" class="message success"></div>
<form id="loginForm" onsubmit="handleLogin(event)">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" placeholder="请输入用户名" required>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" placeholder="请输入密码" required>
</div>
<div id="captchaGroup" class="form-group" style="display: none;">
<label for="captcha">验证码</label>
<div class="captcha-row">
<input type="text" id="captcha" name="captcha" placeholder="请输入验证码">
<img id="captchaImage" src="" alt="验证码" style="height: 50px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;" onclick="refreshCaptcha()" title="点击刷新">
<button type="button" class="captcha-refresh" onclick="refreshCaptcha()">🔄</button>
</div>
</div>
<div class="forgot-link">
<a href="#" onclick="showForgotPassword(event)">忘记密码?</a>
<span id="resendVerifyLink" style="display: none; margin-left: 16px;"><a href="#" onclick="showResendVerify(event)">重发验证邮件</a></span>
</div>
<button type="submit" class="btn-login">登 录</button>
</form>
<div class="register-link">还没有账号? <a href="/register">立即注册</a></div>
</div>
</div>
<div id="forgotPasswordModal" class="modal-overlay" onclick="if(event.target===this)closeForgotPassword()">
<div class="modal">
<div class="modal-header"><h2>重置密码</h2><p id="resetModalDesc">填写信息后等待管理员审核</p></div>
<div class="modal-body">
<div id="modalErrorMessage" class="message error"></div>
<div id="modalSuccessMessage" class="message success"></div>
<!-- 邮件重置方式(启用邮件功能时显示) -->
<form id="emailResetForm" onsubmit="handleEmailReset(event)" style="display: none;">
<div class="form-group"><label>邮箱</label><input type="email" id="emailResetEmail" placeholder="请输入注册邮箱" required></div>
<div class="form-group">
<label>验证码</label>
<div class="captcha-row">
<input type="text" id="emailResetCaptcha" placeholder="请输入验证码" required>
<img id="emailResetCaptchaImage" src="" alt="验证码" style="height: 50px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;" onclick="refreshEmailResetCaptcha()" title="点击刷新">
<button type="button" class="captcha-refresh" onclick="refreshEmailResetCaptcha()">🔄</button>
</div>
</div>
</form>
<!-- 管理员审核方式(未启用邮件功能时显示) -->
<form id="resetPasswordForm" onsubmit="handleResetPassword(event)">
<div class="form-group"><label>用户名</label><input type="text" id="resetUsername" placeholder="请输入用户名" required></div>
<div class="form-group"><label>邮箱(可选)</label><input type="email" id="resetEmail" placeholder="用于验证身份"></div>
<div class="form-group"><label>新密码</label><input type="password" id="resetNewPassword" placeholder="至少8位包含字母和数字" required></div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn-secondary" onclick="closeForgotPassword()">取消</button>
<button type="button" class="btn-primary" id="resetSubmitBtn" onclick="submitResetForm()">提交申请</button>
</div>
</div>
</div>
<!-- 重发验证邮件弹窗 -->
<div id="resendVerifyModal" class="modal-overlay" onclick="if(event.target===this)closeResendVerify()">
<div class="modal">
<div class="modal-header"><h2>重发验证邮件</h2><p>输入注册时使用的邮箱</p></div>
<div class="modal-body">
<div id="resendErrorMessage" class="message error"></div>
<div id="resendSuccessMessage" class="message success"></div>
<form id="resendVerifyForm" onsubmit="handleResendVerify(event)">
<div class="form-group"><label>邮箱</label><input type="email" id="resendEmail" placeholder="请输入注册邮箱" required></div>
<div class="form-group">
<label>验证码</label>
<div class="captcha-row">
<input type="text" id="resendCaptcha" placeholder="请输入验证码" required>
<img id="resendCaptchaImage" src="" alt="验证码" style="height: 50px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;" onclick="refreshResendCaptcha()" title="点击刷新">
<button type="button" class="captcha-refresh" onclick="refreshResendCaptcha()">🔄</button>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn-secondary" onclick="closeResendVerify()">取消</button>
<button type="button" class="btn-primary" onclick="document.getElementById('resendVerifyForm').dispatchEvent(new Event('submit'))">发送验证邮件</button>
</div>
</div>
</div>
<script>
let captchaSession = '';
let resendCaptchaSession = '';
let emailResetCaptchaSession = '';
let needCaptcha = false;
let emailEnabled = false;
// 页面加载时检查邮箱验证是否启用
window.onload = async function() {
try {
const resp = await fetch('/api/email/verify-status');
const data = await resp.json();
emailEnabled = data.email_enabled;
if (data.register_verify_enabled) {
document.getElementById('resendVerifyLink').style.display = 'inline';
}
} catch (e) {
console.log('获取邮箱验证状态失败', e);
}
};
async function handleLogin(event) {
event.preventDefault();
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value.trim();
const captcha = document.getElementById('captcha') ? document.getElementById('captcha').value.trim() : '';
const errorDiv = document.getElementById('errorMessage');
const successDiv = document.getElementById('successMessage');
errorDiv.style.display = 'none';
successDiv.style.display = 'none';
if (!username || !password) { errorDiv.textContent = '用户名和密码不能为空'; errorDiv.style.display = 'block'; return; }
if (needCaptcha && !captcha) { errorDiv.textContent = '请输入验证码'; errorDiv.style.display = 'block'; return; }
try {
const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password, captcha_session: captchaSession, captcha, need_captcha: needCaptcha }) });
const data = await response.json();
if (response.ok) { successDiv.textContent = '登录成功,正在跳转...'; successDiv.style.display = 'block'; setTimeout(() => { window.location.href = '/app'; }, 500); }
else { errorDiv.textContent = data.error || '登录失败'; errorDiv.style.display = 'block'; if (data.need_captcha) { needCaptcha = true; document.getElementById('captchaGroup').style.display = 'block'; await generateCaptcha(); } }
} catch (error) { errorDiv.textContent = '网络错误'; errorDiv.style.display = 'block'; }
}
async function showForgotPassword(event) {
event.preventDefault();
document.getElementById('forgotPasswordModal').classList.add('active');
document.getElementById('modalErrorMessage').style.display = 'none';
document.getElementById('modalSuccessMessage').style.display = 'none';
// 根据邮件功能状态切换显示
if (emailEnabled) {
document.getElementById('emailResetForm').style.display = 'block';
document.getElementById('resetPasswordForm').style.display = 'none';
document.getElementById('resetModalDesc').textContent = '输入注册邮箱,我们将发送重置链接';
document.getElementById('resetSubmitBtn').textContent = '发送重置邮件';
await generateEmailResetCaptcha();
} else {
document.getElementById('emailResetForm').style.display = 'none';
document.getElementById('resetPasswordForm').style.display = 'block';
document.getElementById('resetModalDesc').textContent = '填写信息后等待管理员审核';
document.getElementById('resetSubmitBtn').textContent = '提交申请';
}
}
function closeForgotPassword() {
document.getElementById('forgotPasswordModal').classList.remove('active');
document.getElementById('resetPasswordForm').reset();
document.getElementById('emailResetForm').reset();
document.getElementById('modalErrorMessage').style.display = 'none';
document.getElementById('modalSuccessMessage').style.display = 'none';
}
function submitResetForm() {
if (emailEnabled) {
document.getElementById('emailResetForm').dispatchEvent(new Event('submit'));
} else {
document.getElementById('resetPasswordForm').dispatchEvent(new Event('submit'));
}
}
async function handleResetPassword(event) {
event.preventDefault();
const username = document.getElementById('resetUsername').value.trim();
const email = document.getElementById('resetEmail').value.trim();
const newPassword = document.getElementById('resetNewPassword').value.trim();
const errorDiv = document.getElementById('modalErrorMessage');
const successDiv = document.getElementById('modalSuccessMessage');
errorDiv.style.display = 'none'; successDiv.style.display = 'none';
if (!username || !newPassword) { errorDiv.textContent = '用户名和新密码不能为空'; errorDiv.style.display = 'block'; return; }
if (newPassword.length < 8) { errorDiv.textContent = '密码长度至少8位'; errorDiv.style.display = 'block'; return; }
if (!/[a-zA-Z]/.test(newPassword) || !/\d/.test(newPassword)) { errorDiv.textContent = '密码必须包含字母和数字'; errorDiv.style.display = 'block'; return; }
try {
const response = await fetch('/api/reset_password_request', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, email, new_password: newPassword }) });
const data = await response.json();
if (response.ok) { successDiv.textContent = '申请已提交,请等待审核'; successDiv.style.display = 'block'; setTimeout(closeForgotPassword, 2000); }
else { errorDiv.textContent = data.error || '申请失败'; errorDiv.style.display = 'block'; }
} catch (error) { errorDiv.textContent = '网络错误'; errorDiv.style.display = 'block'; }
}
async function generateCaptcha() { try { const response = await fetch('/api/generate_captcha', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.session_id && data.captcha_image) { captchaSession = data.session_id; document.getElementById('captchaImage').src = data.captcha_image; } } catch (error) { console.error('生成验证码失败:', error); } }
async function refreshCaptcha() { await generateCaptcha(); document.getElementById('captcha').value = ''; }
// 邮件方式重置密码相关函数
async function generateEmailResetCaptcha() {
try {
const response = await fetch('/api/generate_captcha', { method: 'POST', headers: { 'Content-Type': 'application/json' } });
const data = await response.json();
if (data.session_id && data.captcha_image) {
emailResetCaptchaSession = data.session_id;
document.getElementById('emailResetCaptchaImage').src = data.captcha_image;
}
} catch (error) { console.error('生成验证码失败:', error); }
}
async function refreshEmailResetCaptcha() { await generateEmailResetCaptcha(); document.getElementById('emailResetCaptcha').value = ''; }
async function handleEmailReset(event) {
event.preventDefault();
const email = document.getElementById('emailResetEmail').value.trim();
const captcha = document.getElementById('emailResetCaptcha').value.trim();
const errorDiv = document.getElementById('modalErrorMessage');
const successDiv = document.getElementById('modalSuccessMessage');
errorDiv.style.display = 'none'; successDiv.style.display = 'none';
if (!email) { errorDiv.textContent = '请输入邮箱'; errorDiv.style.display = 'block'; return; }
if (!captcha) { errorDiv.textContent = '请输入验证码'; errorDiv.style.display = 'block'; return; }
try {
const response = await fetch('/api/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, captcha_session: emailResetCaptchaSession, captcha })
});
const data = await response.json();
if (response.ok) {
successDiv.innerHTML = data.message + '<br><small style="color: #666;">请检查您的邮箱(包括垃圾邮件文件夹)</small>';
successDiv.style.display = 'block';
setTimeout(closeForgotPassword, 3000);
} else {
errorDiv.textContent = data.error || '发送失败';
errorDiv.style.display = 'block';
await refreshEmailResetCaptcha();
}
} catch (error) { errorDiv.textContent = '网络错误'; errorDiv.style.display = 'block'; }
}
// 重发验证邮件相关函数
async function showResendVerify(event) {
event.preventDefault();
document.getElementById('resendVerifyModal').classList.add('active');
await generateResendCaptcha();
}
function closeResendVerify() {
document.getElementById('resendVerifyModal').classList.remove('active');
document.getElementById('resendVerifyForm').reset();
document.getElementById('resendErrorMessage').style.display = 'none';
document.getElementById('resendSuccessMessage').style.display = 'none';
}
async function generateResendCaptcha() {
try {
const response = await fetch('/api/generate_captcha', { method: 'POST', headers: { 'Content-Type': 'application/json' } });
const data = await response.json();
if (data.session_id && data.captcha_image) {
resendCaptchaSession = data.session_id;
document.getElementById('resendCaptchaImage').src = data.captcha_image;
}
} catch (error) { console.error('生成验证码失败:', error); }
}
async function refreshResendCaptcha() { await generateResendCaptcha(); document.getElementById('resendCaptcha').value = ''; }
async function handleResendVerify(event) {
event.preventDefault();
const email = document.getElementById('resendEmail').value.trim();
const captcha = document.getElementById('resendCaptcha').value.trim();
const errorDiv = document.getElementById('resendErrorMessage');
const successDiv = document.getElementById('resendSuccessMessage');
errorDiv.style.display = 'none'; successDiv.style.display = 'none';
if (!email) { errorDiv.textContent = '请输入邮箱'; errorDiv.style.display = 'block'; return; }
if (!captcha) { errorDiv.textContent = '请输入验证码'; errorDiv.style.display = 'block'; return; }
try {
const response = await fetch('/api/resend-verify-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, captcha_session: resendCaptchaSession, captcha })
});
const data = await response.json();
if (response.ok) {
successDiv.textContent = data.message || '验证邮件已发送,请查收';
successDiv.style.display = 'block';
setTimeout(closeResendVerify, 2000);
} else {
errorDiv.textContent = data.error || '发送失败';
errorDiv.style.display = 'block';
await refreshResendCaptcha();
}
} catch (error) { errorDiv.textContent = '网络错误'; errorDiv.style.display = 'block'; }
}
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeForgotPassword(); closeResendVerify(); } });
</script>
</body>
</html>