gate / static /admin-login.html
harii66's picture
Upload 23 files
b4edbc0 verified
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管理员登录 - Media Gateway</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background-size: 200% 200%;
animation: gradientShift 15s ease infinite;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-10px); }
75% { transform: translateX(10px); }
}
.login-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 30px;
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.3);
overflow: hidden;
max-width: 480px;
width: 100%;
animation: slideIn 0.6s ease;
}
.login-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 50px 40px;
text-align: center;
}
.login-header h1 {
font-size: 2.5em;
margin-bottom: 12px;
font-weight: 800;
}
.login-header p {
font-size: 1.1em;
opacity: 0.95;
}
.login-body {
padding: 40px;
}
.form-group {
margin-bottom: 25px;
}
.form-group label {
display: block;
margin-bottom: 10px;
font-weight: 600;
color: #1e293b;
font-size: 0.95em;
}
.input-wrapper {
position: relative;
}
.form-group input {
width: 100%;
padding: 16px 20px;
border: 2px solid #e2e8f0;
border-radius: 12px;
font-size: 1em;
transition: all 0.3s ease;
background: white;
font-family: inherit;
}
.form-group input:focus {
outline: none;
border-color: #6366f1;
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
}
.password-toggle {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
color: #64748b;
font-size: 1.3em;
padding: 6px;
}
.password-toggle:hover {
color: #6366f1;
}
.btn-login {
width: 100%;
padding: 18px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 12px;
font-size: 1.1em;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-login:hover:not(:disabled) {
transform: translateY(-3px);
box-shadow: 0 12px 35px rgba(99, 102, 241, 0.4);
}
.btn-login:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.error-message {
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
color: #991b1b;
padding: 16px;
border-radius: 12px;
margin-bottom: 20px;
display: none;
animation: shake 0.5s;
border: 2px solid #fca5a5;
}
.error-message.show {
display: block;
}
.success-message {
background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
color: #065f46;
padding: 16px;
border-radius: 12px;
margin-bottom: 20px;
display: none;
border: 2px solid #6ee7b7;
}
.success-message.show {
display: block;
}
.info-box {
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
color: #1e40af;
padding: 16px;
border-radius: 12px;
margin-bottom: 25px;
font-size: 0.9em;
line-height: 1.6;
border: 1px solid #93c5fd;
}
.info-box strong {
display: block;
margin-bottom: 8px;
font-size: 1.05em;
}
.login-footer {
padding: 25px 40px;
background: rgba(248, 250, 252, 0.8);
text-align: center;
color: #64748b;
font-size: 0.9em;
border-top: 1px solid #e2e8f0;
}
.login-footer a {
color: #6366f1;
text-decoration: none;
font-weight: 600;
}
.login-footer a:hover {
text-decoration: underline;
}
@media (max-width: 480px) {
.login-container {
margin: 10px;
}
.login-header {
padding: 40px 30px;
}
.login-header h1 {
font-size: 2em;
}
.login-body {
padding: 30px 25px;
}
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<h1>🔐 管理员登录</h1>
<p>Media Gateway Admin Panel</p>
</div>
<div class="login-body">
<div id="errorMessage" class="error-message"></div>
<div id="successMessage" class="success-message"></div>
<div class="info-box">
<strong>💡 管理员功能</strong>
登录后可创建和管理用户账号,配置系统设置
</div>
<form id="loginForm">
<div class="form-group">
<label for="username">👤 管理员账号</label>
<input
type="text"
id="username"
placeholder="请输入管理员账号"
required
autocomplete="username"
>
</div>
<div class="form-group">
<label for="password">🔒 管理员密码</label>
<div class="input-wrapper">
<input
type="password"
id="password"
placeholder="请输入管理员密码"
required
autocomplete="current-password"
>
<button type="button" class="password-toggle" id="togglePassword">
👁️
</button>
</div>
</div>
<button type="submit" class="btn-login" id="loginBtn">
🚀 登录管理后台
</button>
</form>
</div>
<div class="login-footer">
<a href="/">← 返回首页</a>
</div>
</div>
<script>
(function() {
'use strict';
const API = window.location.origin;
function togglePassword() {
const passwordInput = document.getElementById('password');
const toggleBtn = document.getElementById('togglePassword');
if (!passwordInput || !toggleBtn) return;
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
toggleBtn.textContent = '🙈';
} else {
passwordInput.type = 'password';
toggleBtn.textContent = '👁️';
}
}
function showError(message) {
const errorEl = document.getElementById('errorMessage');
const successEl = document.getElementById('successMessage');
if (!errorEl) return;
successEl.classList.remove('show');
errorEl.textContent = '❌ ' + message;
errorEl.classList.add('show');
setTimeout(() => {
errorEl.classList.remove('show');
}, 5000);
}
function showSuccess(message) {
const successEl = document.getElementById('successMessage');
const errorEl = document.getElementById('errorMessage');
if (!successEl) return;
errorEl.classList.remove('show');
successEl.textContent = '✅ ' + message;
successEl.classList.add('show');
}
async function sha256(message) {
try {
const msgBuffer = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
} catch (error) {
throw error;
}
}
function clearAuth() {
localStorage.removeItem('admin_token');
localStorage.removeItem('admin_token_expiry');
localStorage.removeItem('admin_username');
}
function saveAuth(token, username) {
const expiry = Date.now() + (24 * 60 * 60 * 1000);
localStorage.setItem('admin_token', token);
localStorage.setItem('admin_token_expiry', expiry.toString());
localStorage.setItem('admin_username', username);
}
async function login(username, password) {
try {
const passwordHash = await sha256(password);
const response = await fetch(`${API}/api/admin/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username,
password_hash: passwordHash
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || '登录失败');
}
const data = await response.json();
if (data.success && data.token) {
saveAuth(data.token, username);
return true;
} else {
throw new Error('登录失败:未返回有效 token');
}
} catch (error) {
throw error;
}
}
async function checkAuthStatus() {
const token = localStorage.getItem('admin_token');
const expiry = localStorage.getItem('admin_token_expiry');
if (!token || !expiry) {
return false;
}
if (Date.now() > parseInt(expiry)) {
clearAuth();
return false;
}
try {
const response = await fetch(`${API}/api/admin/check`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
if (data.authenticated) {
return true;
}
}
clearAuth();
return false;
} catch (error) {
clearAuth();
return false;
}
}
function initialize() {
const togglePasswordBtn = document.getElementById('togglePassword');
if (togglePasswordBtn) {
togglePasswordBtn.addEventListener('click', togglePassword);
}
const loginForm = document.getElementById('loginForm');
if (loginForm) {
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const loginBtn = document.getElementById('loginBtn');
if (!usernameInput || !passwordInput || !loginBtn) return;
const username = usernameInput.value.trim();
const password = passwordInput.value;
if (!username || !password) {
showError('请输入用户名和密码');
return;
}
loginBtn.disabled = true;
loginBtn.textContent = '验证中...';
try {
const success = await login(username, password);
if (success) {
showSuccess('登录成功!正在跳转...');
setTimeout(() => {
window.location.href = '/admin';
}, 1500);
} else {
showError('登录失败,请重试');
loginBtn.disabled = false;
loginBtn.textContent = '🚀 登录管理后台';
passwordInput.value = '';
passwordInput.focus();
}
} catch (error) {
showError(error.message || '登录失败,请重试');
loginBtn.disabled = false;
loginBtn.textContent = '🚀 登录管理后台';
passwordInput.value = '';
passwordInput.focus();
}
});
}
checkAuthStatus().then(isValid => {
if (isValid) {
window.location.href = '/admin';
} else {
}
});
setTimeout(() => {
const usernameInput = document.getElementById('username');
if (usernameInput) {
usernameInput.focus();
}
}, 300);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
})();
</script>
</body>
</html>