Files
resume/templates/admin/login.html
T

483 lines
15 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>{{ .SystemConfig.WebsiteTitle }} - 后台登录</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary: #7B8EA2;
--primary-hover: #6B7E92;
--accent: #987E68;
--text-primary: #2F343A;
--text-secondary: #6C737C;
--text-muted: #9CA3AF;
--bg: #F8F7F4;
--bg-card: #FFFFFF;
--border: #E5E3DF;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Noto Sans SC', sans-serif;
background-color: var(--bg);
color: var(--text-primary);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
width: 100%;
max-width: 26.25rem;
padding: 3rem;
background: var(--bg-card);
border-radius: 1rem;
box-shadow: 0 0.25rem 1.50rem rgba(47, 52, 58, 0.08);
border: 0.06rem solid var(--border);
}
.brand-section {
text-align: center;
margin-bottom: 2.50rem;
}
.brand-logo {
width: 3.50rem;
height: 3.50rem;
margin: 0 auto 1.25rem;
background: var(--primary);
border-radius: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.50rem;
transition: all 0.3s ease;
}
.brand-logo img {
border-radius: 0.50rem;
}
.brand-logo:hover {
transform: scale(1.05);
}
.brand-title {
font-size: 1.38rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.38rem;
letter-spacing: 0.12rem;
}
.brand-subtitle {
font-size: 0.81rem;
color: var(--text-secondary);
}
.input-group {
margin-bottom: 1.25rem;
}
.input-group label {
display: block;
font-size: 0.81rem;
color: var(--text-secondary);
margin-bottom: 0.50rem;
font-weight: 500;
}
.input-group input {
width: 100%;
padding: 0.75rem 0.88rem;
border: 0.06rem solid var(--border);
border-radius: 0.50rem;
font-size: 0.88rem;
color: var(--text-primary);
background: var(--bg);
transition: all 0.25s ease;
outline: none;
font-family: inherit;
}
.input-group input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 0.19rem rgba(123, 142, 162, 0.1);
background: white;
}
.input-group input::placeholder {
color: var(--text-muted);
}
.input-wrapper {
position: relative;
}
.input-icon {
position: absolute;
left: 0.75rem;
top: 50%;
transform: translateY(-50%);
font-size: 0.88rem;
color: var(--text-muted);
pointer-events: none;
}
.input-group input {
padding-left: 2.50rem;
}
.input-toggle {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
font-size: 0.88rem;
color: var(--text-muted);
cursor: pointer;
transition: color 0.2s ease;
}
.input-toggle:hover {
color: var(--primary);
}
.captcha-row {
display: flex;
gap: 0.75rem;
align-items: center;
}
.captcha-row .input-group {
flex: 1;
margin-bottom: 0;
}
.captcha-img {
width: 6.25rem;
height: 2.75rem;
border-radius: 0.50rem;
cursor: pointer;
border: 0.06rem solid var(--border);
background: var(--bg);
transition: all 0.25s ease;
}
.captcha-img:hover {
border-color: var(--primary);
transform: scale(1.02);
}
.btn-login {
width: 100%;
padding: 0.81rem;
border: none;
border-radius: 0.50rem;
font-size: 0.88rem;
font-weight: 500;
color: white;
background: var(--primary);
cursor: pointer;
transition: all 0.25s ease;
margin-top: 1.50rem;
letter-spacing: 0.06rem;
}
.btn-login:hover {
background: var(--primary-hover);
transform: translateY(-0.06rem);
box-shadow: 0 0.25rem 0.75rem rgba(123, 142, 162, 0.25);
}
.btn-login:active {
transform: translateY(0);
box-shadow: none;
}
.btn-login:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.footer-text {
text-align: center;
margin-top: 2rem;
font-size: 0.75rem;
color: var(--text-muted);
}
.alert-overlay {
position: fixed;
inset: 0;
background: rgba(47, 52, 58, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.alert-overlay.show {
opacity: 1;
visibility: visible;
}
.alert-box {
background: white;
border-radius: 0.75rem;
padding: 2rem;
max-width: 22.50rem;
width: 90%;
text-align: center;
box-shadow: 0 0.75rem 2.50rem rgba(47, 52, 58, 0.15);
transform: scale(0.9) translateY(1.25rem);
transition: all 0.3s ease;
}
.alert-overlay.show .alert-box {
transform: scale(1) translateY(0);
}
.alert-icon {
width: 3rem;
height: 3rem;
margin: 0 auto 1rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.38rem;
}
.alert-icon.error {
background: #FEF2F2;
color: #DC2626;
}
.alert-icon.success {
background: #ECFDF5;
color: #16A34A;
}
.alert-icon.info {
background: #EFF6FF;
color: #2563EB;
}
.alert-title {
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.50rem;
}
.alert-message {
font-size: 0.81rem;
color: var(--text-secondary);
margin-bottom: 1.25rem;
line-height: 1.6;
}
.btn-alert {
padding: 0.62rem 1.50rem;
border: none;
border-radius: 0.38rem;
font-size: 0.81rem;
font-weight: 500;
color: white;
background: var(--primary);
cursor: pointer;
transition: all 0.2s ease;
}
.btn-alert:hover {
background: var(--primary-hover);
}
</style>
</head>
<body>
<div class="login-container">
<div class="brand-section">
<div class="brand-logo">
{{ if .SystemConfig.LogoPath }}
<img src="{{.SystemConfig.LogoPath}}" alt="Logo">
{{ else }}
<i class="fas fa-file-alt"></i>
{{ end }}
</div>
<div class="brand-title">{{ .SystemConfig.WebsiteTitle }}</div>
<div class="brand-subtitle">{{ .SystemConfig.WebsiteDesc }}</div>
</div>
<form id="loginForm" onsubmit="login(event)">
<div class="input-group">
<label for="username">用户名</label>
<div class="input-wrapper">
<i class="fas fa-user input-icon"></i>
<input type="text" id="username" placeholder="请输入用户名" required>
</div>
</div>
<div class="input-group">
<label for="password">密码</label>
<div class="input-wrapper">
<i class="fas fa-lock input-icon"></i>
<input type="password" id="password" placeholder="请输入密码" required>
<i class="fas fa-eye input-toggle" id="passwordToggle" onclick="togglePassword()"></i>
</div>
</div>
<div class="captcha-row">
<div class="input-group">
<label for="captcha">验证码</label>
<div class="input-wrapper">
<i class="fas fa-shield-halved input-icon"></i>
<input type="text" id="captcha" placeholder="验证码" required>
</div>
</div>
<div class="flex flex-col justify-center">
<img id="captchaImg" src="" alt="验证码" class="captcha-img" onclick="refreshCaptcha()">
</div>
</div>
<button type="submit" id="loginBtn" class="btn-login">登 录</button>
</form>
<div class="footer-text">
© 2026 {{ .SystemConfig.WebsiteTitle }}. All rights reserved.
</div>
</div>
<div id="customAlert" class="alert-overlay">
<div class="alert-box" id="alertContent">
<div id="alertIcon" class="alert-icon"></div>
<h3 id="alertTitle" class="alert-title"></h3>
<p id="alertMessage" class="alert-message"></p>
<button onclick="closeCustomAlert()" class="btn-alert">确定</button>
</div>
</div>
<script>
const adminPath = "{{.adminPath}}";
let captchaToken = '';
function togglePassword() {
const password = document.getElementById('password');
const toggle = document.getElementById('passwordToggle');
if (password.type === 'password') {
password.type = 'text';
toggle.classList.remove('fa-eye');
toggle.classList.add('fa-eye-slash');
} else {
password.type = 'password';
toggle.classList.remove('fa-eye-slash');
toggle.classList.add('fa-eye');
}
}
function refreshCaptcha() {
const img = document.getElementById('captchaImg');
const xhr = new XMLHttpRequest();
xhr.open('GET', adminPath + '/captcha?' + Date.now(), true);
xhr.responseType = 'blob';
xhr.onload = function() {
if (this.status === 200) {
captchaToken = this.getResponseHeader('X-Captcha-Token') || '';
const blob = this.response;
img.src = URL.createObjectURL(blob);
}
};
xhr.send();
}
function showCustomAlert(title, message, type) {
const alert = document.getElementById('customAlert');
const icon = document.getElementById('alertIcon');
const titleEl = document.getElementById('alertTitle');
const messageEl = document.getElementById('alertMessage');
titleEl.textContent = title;
messageEl.textContent = message;
icon.className = 'alert-icon ' + type;
if (type === 'error') {
icon.innerHTML = '<i class="fas fa-times-circle"></i>';
} else if (type === 'success') {
icon.innerHTML = '<i class="fas fa-check-circle"></i>';
} else {
icon.innerHTML = '<i class="fas fa-info-circle"></i>';
}
alert.classList.add('show');
}
function closeCustomAlert() {
const alert = document.getElementById('customAlert');
alert.classList.remove('show');
}
async function login(event) {
event.preventDefault();
const btn = document.getElementById('loginBtn');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> 登录中...';
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const captcha = document.getElementById('captcha').value;
try {
const response = await fetch(adminPath + '/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username,
password,
captcha,
captcha_token: captchaToken
})
});
const result = await response.json();
if (result.success) {
showCustomAlert('登录成功', '正在跳转...', 'success');
setTimeout(() => {
window.location.href = result.redirect || '/dashboard/home';
}, 1500);
} else {
showCustomAlert('登录失败', result.message || '请检查用户名、密码和验证码', 'error');
refreshCaptcha();
}
} catch (error) {
showCustomAlert('网络错误', '无法连接服务器,请稍后重试', 'error');
} finally {
btn.disabled = false;
btn.innerHTML = '登 录';
}
}
window.addEventListener('load', function() {
const xhr = new XMLHttpRequest();
xhr.open('GET', adminPath + '/captcha', true);
xhr.responseType = 'blob';
xhr.onload = function() {
if (this.status === 200) {
captchaToken = this.getResponseHeader('X-Captcha-Token') || '';
const blob = this.response;
document.getElementById('captchaImg').src = URL.createObjectURL(blob);
}
};
xhr.send();
});
</script>
</body>
</html>