This commit is contained in:
XOF
2025-11-20 12:24:05 +08:00
commit f28bdc751f
164 changed files with 64248 additions and 0 deletions

245
web/templates/auth.html Normal file
View File

@@ -0,0 +1,245 @@
<!DOCTYPE html>
<html lang="zh-CN" class="">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 块:标题 (继承自旧模板的 block title) -->
<title>登录 - GEMINI BALANCER</title>
<!-- 引入项目主CSS文件这是独立页面所必需的 -->
<link rel="stylesheet" href="/static/css/output.css">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- 块:额外的头部内容 (继承自旧模板的 block head_extra) -->
<style>
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in-up {
animation: fadeInUp 0.8s ease-out forwards;
}
</style>
</head>
<body class="bg-zinc-100 dark:bg-zinc-900">
<!-- 块:主内容区 (继承自旧模板的 block app_container) -->
<div class="relative w-full h-screen flex flex-col justify-center items-center p-8 overflow-hidden">
<div class="absolute top-6 right-6 flex items-center space-x-4 animate-fade-in-up" style="animation-delay: 0.3s;">
<!-- 注意这个按钮现在只在这个页面生效其JS逻辑也包含在下方 -->
<div id="theme-switcher-container">
<!-- 我们将您的三态切换器HTML暂时放在这里虽然JS还没准备好 -->
<div id="theme-switcher" class="relative z-10 inline-grid grid-cols-3 gap-1 rounded-full bg-gray-950/5 p-1 text-gray-950 dark:bg-white/10 dark:text-white">
<button type="button" data-theme="system" class="theme-btn rounded-full p-1.5 focus:outline-none">
<svg class="size-6" viewBox="0 0 28 28" fill="none"><path d="M7.5 8.5C7.5 7.94772 7.94772 7.5 8.5 7.5H19.5C20.0523 7.5 20.5 7.94772 20.5 8.5V16.5C20.5 17.0523 20.0523 17.5 19.5 17.5H8.5C7.94772 17.5 7.5 17.0523 7.5 16.5V8.5Z" stroke="currentColor"></path><path d="M7.5 8.5C7.5 7.94772 7.94772 7.5 8.5 7.5H19.5C20.0523 7.5 20.5 7.94772 20.5 8.5V14.5C20.5 15.0523 20.0523 15.5 19.5 15.5H8.5C7.94772 15.5 7.5 15.0523 7.5 14.5V8.5Z" stroke="currentColor"></path><path d="M16.5 20.5V17.5H11.5V20.5M16.5 20.5H11.5M16.5 20.5H17.5M11.5 20.5H10.5" stroke="currentColor" stroke-linecap="round"></path></svg>
</button>
<button type="button" data-theme="light" class="theme-btn rounded-full p-1.5 focus:outline-none">
<svg class="size-6" viewBox="0 0 28 28" fill="none"><circle cx="14" cy="14" r="3.5" stroke="currentColor"></circle><path d="M14 8.5V6.5" stroke="currentColor" stroke-linecap="round"></path><path d="M17.889 10.1115L19.3032 8.69727" stroke="currentColor" stroke-linecap="round"></path><path d="M19.5 14L21.5 14" stroke="currentColor" stroke-linecap="round"></path><path d="M17.889 17.8885L19.3032 19.3027" stroke="currentColor" stroke-linecap="round"></path><path d="M14 21.5V19.5" stroke="currentColor" stroke-linecap="round"></path><path d="M8.69663 19.3029L10.1108 17.8887" stroke="currentColor" stroke-linecap="round"></path><path d="M6.5 14L8.5 14" stroke="currentColor" stroke-linecap="round"></path><path d="M8.69663 8.69711L10.1108 10.1113" stroke="currentColor" stroke-linecap="round"></path></svg>
</button>
<button type="button" data-theme="dark" class="theme-btn rounded-full p-1.5 focus:outline-none">
<svg class="size-6" viewBox="0 0 28 28" fill="none"><path d="M10.5 9.99914C10.5 14.1413 13.8579 17.4991 18 17.4991C19.0332 17.4991 20.0176 17.2902 20.9132 16.9123C19.7761 19.6075 17.109 21.4991 14 21.4991C9.85786 21.4991 6.5 18.1413 6.5 13.9991C6.5 10.8902 8.39167 8.22304 11.0868 7.08594C10.7089 7.98159 10.5 8.96597 10.5 9.99914Z" stroke="currentColor" stroke-linejoin="round"></path><path d="M16.3561 6.50754L16.5 5.5L16.6439 6.50754C16.7068 6.94752 17.0525 7.29321 17.4925 7.35607L18.5 7.5L17.4925 7.64393C17.0525 7.70679 16.7068 8.05248 16.6439 8.49246L16.5 9.5L16.3561 8.49246C16.2932 8.05248 15.9475 7.70679 15.5075 7.64393L14.5 7.5L15.5075 7.35607C15.9475 7.29321 16.2932 6.94752 16.3561 6.50754Z" fill="currentColor" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20.3561 11.5075L20.5 10.5L20.6439 11.5075C20.7068 11.9475 21.0525 12.2932 21.4925 12.3561L22.5 12.5L21.4925 12.6439C21.0525 12.7068 20.7068 13.0525 20.6439 13.4925L20.5 14.5L20.3561 13.4925C20.2932 13.0525 19.9475 12.7068 19.5075 12.6439L18.5 12.5L19.5075 12.3561C19.9475 12.2932 20.2932 11.9475 20.3561 11.5075Z" fill="currentColor" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path></svg>
</button>
</div>
</div>
</div>
<div class="w-full max-w-lg mx-auto text-center animate-fade-in-up">
<div class="mb-12">
<img src="/static/icons/logo.png" id="logo-image" alt="GEMINI BALANCER Logo" class="h-20 w-20 mx-auto mb-6 transition-transform duration-300 ease-in-out">
<h1 class="text-4xl font-bold text-zinc-800 dark:text-zinc-100">
GEMINI BALANCER
</h1>
<p class="mt-4 text-lg text-zinc-500 dark:text-zinc-400">
一个强大且优雅的管理后台
</p>
</div>
<p class="mb-8 max-w-md mx-auto text-zinc-600 dark:text-zinc-400">
请输入授权令牌以访问系统。
</p>
<form id="auth-form" class="max-w-md mx-auto" novalidate>
<div class="relative flex items-center group transition-all duration-300 rounded-full bg-white dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700
hover:-translate-y-1 hover:border-blue-500 dark:hover:border-blue-400
focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-opacity-50 focus-within:border-transparent">
<!-- [修正#2] 移除了冲突的 focus:border-* 和 focus:ring-* 类 -->
<input
type="password"
id="auth-token"
name="auth_token"
required
placeholder="在此处粘贴您的令牌..."
class="w-full h-14 pl-6 pr-20 rounded-full text-lg bg-white dark:bg-zinc-800 border-2 border-transparent focus:ring-blue-500/20 transition-all duration-300 outline-none text-zinc-800 dark:text-zinc-200"
>
<!-- [修正#1] 按钮恢复为原始的 span/i 结构,解决了椭圆问题 -->
<button type="submit" id="login-button" class="absolute right-2 h-10 w-10 flex items-center justify-center rounded-full bg-blue-600 hover:bg-blue-700 text-white transition-all duration-300 transform hover:scale-110">
<span id="button-icon" class="transition-opacity duration-200">
<i class="fas fa-arrow-right"></i>
</span>
<i id="button-spinner" class="fas fa-spinner fa-spin absolute transition-opacity duration-200 opacity-0 pointer-events-none"></i>
</button>
</div>
</form>
<div id="error-container" class="mt-6 max-w-md mx-auto min-h-[56px]"></div>
</div>
<footer class="absolute bottom-6 text-zinc-500 dark:text-zinc-600 text-sm animate-fade-in-up" style="animation-delay: 0.2s;">
<p>&copy; 2024 GEMINI BALANCER. All Rights Reserved.</p>
</footer>
</div>
<!-- 块:页面脚本 (继承自旧模板的 block body_scripts) -->
<script>
const ThemeManager = {
// 初始化,绑定所有事件
init: function() {
this.html = document.documentElement;
this.buttons = document.querySelectorAll('.theme-btn');
this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
this.buttons.forEach(btn => {
btn.addEventListener('click', () => this.setTheme(btn.dataset.theme));
});
this.mediaQuery.addEventListener('change', () => this.applyTheme());
this.applyTheme();
},
// 核心函数:应用当前主题
applyTheme: function() {
let theme = this.getTheme();
// 解析 'system' 模式
if (theme === 'system') {
theme = this.mediaQuery.matches ? 'dark' : 'light';
}
// 应用 'dark' 或 'light' 类到 <html>
if (theme === 'dark') {
this.html.classList.add('dark');
} else {
this.html.classList.remove('dark');
}
// 更新按钮的选中状态
this.updateButtons();
},
// 设置新主题并存入 localStorage
setTheme: function(theme) {
localStorage.setItem('theme', theme);
this.applyTheme();
},
// 从 localStorage 获取主题,若无则默认为 'system'
getTheme: function() {
return localStorage.getItem('theme') || 'system';
},
// 更新按钮UI
updateButtons: function() {
const currentTheme = this.getTheme();
this.buttons.forEach(btn => {
if (btn.dataset.theme === currentTheme) {
// 这是选中状态
btn.classList.add('bg-white', 'dark:bg-zinc-700');
} else {
// 这是非选中状态
btn.classList.remove('bg-white', 'dark:bg-zinc-700');
}
});
}
};
document.addEventListener('DOMContentLoaded', function() {
// 初始化主题管理器
ThemeManager.init();
// 原有的登录页逻辑
localStorage.removeItem('bearerToken');
const logo = document.getElementById('logo-image');
if (logo) {
let rotationAngle = 0;
setInterval(() => {
rotationAngle += 90;
logo.style.transform = `rotate(${rotationAngle}deg)`;
}, 2000);
}
// --- 登录表单逻辑 (优化版) ---
const form = document.getElementById('auth-form');
const tokenInput = document.getElementById('auth-token');
const loginButton = document.getElementById('login-button');
const buttonIcon = document.getElementById('button-icon');
const buttonSpinner = document.getElementById('button-spinner');
const errorContainer = document.getElementById('error-container');
// [新增] UI状态管理函数确保图标互斥
function setButtonLoading(isLoading) {
// 获取最新的元素引用
const buttonIcon = document.getElementById('button-icon');
const buttonSpinner = document.getElementById('button-spinner');
const loginButton = document.getElementById('login-button');
const tokenInput = document.getElementById('auth-token');
if (isLoading) {
// 隐藏箭头显示Spinner
buttonIcon.classList.add('opacity-0', 'pointer-events-none');
buttonSpinner.classList.remove('opacity-0', 'pointer-events-none');
loginButton.disabled = true;
tokenInput.disabled = true;
} else {
// 显示箭头隐藏Spinner
buttonIcon.classList.remove('opacity-0', 'pointer-events-none');
buttonSpinner.classList.add('opacity-0', 'pointer-events-none');
loginButton.disabled = false;
tokenInput.disabled = false;
}
}
form.addEventListener('submit', async function(e) {
e.preventDefault();
errorContainer.innerHTML = '';
const token = tokenInput.value.trim();
if (!token) {
displayError('请输入验证令牌');
return;
}
// 进入加载状态
setButtonLoading(true);
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: token }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || '无效的令牌或服务器错误');
}
localStorage.setItem('bearerToken', data.token);
displaySuccess('验证成功!正在进入...');
// 成功后,不需要重置按钮状态,因为页面即将跳转
setTimeout(() => {
window.location.href = '/dashboard';
}, 800);
} catch (error) {
displayError(error.message);
// 发生错误,恢复按钮为可交互状态
setButtonLoading(false);
}
});
function displayError(message) {
errorContainer.innerHTML = `<p class="text-red-500 font-medium p-3 bg-red-100 dark:bg-red-900/40 rounded-full">${message}</p>`;
}
function displaySuccess(message) {
errorContainer.innerHTML = `<p class="text-green-500 font-medium p-3 bg-green-100 dark:bg-green-900/40 rounded-full">${message}</p>`;
}
const urlParams = new URLSearchParams(window.location.search);
const error = urlParams.get('error');
if (error) {
displayError(error);
}
});
</script>
</body>
</html>