AIClient-2-API/static/login.html
hex2077 fa19bae517 refactor(potluck): 简化 API 大锅饭系统并增强安全性和 UI
- 移除凭证管理和资源包系统,简化为基于每日限额的 Key 管理
- 新增登录安全防护(频率限制、账户锁定、IP 追踪)
- 重构日志系统使用 AsyncLocalStorage 替代全局状态
- 全面升级 UI 界面(主题切换、使用分布统计、响应式设计)
- 优化安装脚本(PowerShell 支持、手动安装指引)

BREAKING CHANGE: API Potluck 插件不再支持凭证资源包功能,所有 Key 仅基于每日限额进行配额管理。user-data-manager 模块已禁用,相关 API 端点已移除。
2026-03-05 17:21:47 +08:00

350 lines
No EOL
11 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" id="html-root">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n="login.title">登录 - AIClient2API</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-container {
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
padding: 40px;
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.logo {
text-align: center;
margin-bottom: 30px;
}
.logo img {
width: 80px;
height: 80px;
border-radius: 50%;
margin-bottom: 15px;
}
.logo h1 {
font-size: 24px;
color: #333;
margin-bottom: 5px;
}
.logo p {
font-size: 14px;
color: #666;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-size: 14px;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 12px 15px;
border: 2px solid #e1e8ed;
border-radius: 8px;
font-size: 14px;
transition: all 0.3s ease;
outline: none;
}
.form-group input:focus {
border-color: #059669;
box-shadow: 0 0 0 3px rgba(5, 150, 105, 0.1);
}
.form-group input::placeholder {
color: #aaa;
}
.error-message {
color: #e74c3c;
font-size: 13px;
margin-top: 8px;
display: none;
animation: shake 0.3s ease-in-out;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-10px); }
75% { transform: translateX(10px); }
}
.error-message.show {
display: block;
}
.login-button {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #059669 0%, #10b981 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 10px;
}
.login-button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(5, 150, 105, 0.4);
}
.login-button:active {
transform: translateY(0);
}
.login-button:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e1e8ed;
}
.footer p {
font-size: 13px;
color: #999;
}
.loading {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #ffffff;
border-radius: 50%;
border-top-color: transparent;
animation: spin 0.8s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (max-width: 480px) {
.login-container {
padding: 30px 20px;
}
.logo h1 {
font-size: 20px;
}
}
</style>
<link rel="stylesheet" href="app/base.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div style="position: absolute; top: 20px; right: 20px;" id="langSwitcherContainer"></div>
<div class="login-container">
<div class="logo">
<img src="/favicon.ico" alt="Logo" onerror="this.style.display='none'">
<h1>AIClient2API</h1>
<p data-i18n="login.heading">请登录以继续</p>
</div>
<form id="loginForm">
<div class="form-group">
<label for="password" data-i18n="login.password">密码</label>
<input
type="password"
id="password"
name="password"
data-i18n="login.passwordPlaceholder"
placeholder="请输入密码"
autocomplete="current-password"
required
>
<div class="error-message" id="errorMessage" data-i18n="login.error.incorrect">密码错误,请重试</div>
</div>
<button type="submit" class="login-button" id="loginButton" data-i18n="login.button">
登录
</button>
</form>
<div class="footer">
<p>&copy; 2025 AIClient2API. All rights reserved.</p>
</div>
</div>
<script type="module">
import { initI18n, t, setLanguage } from './app/i18n.js';
import { initLanguageSwitcher } from './app/language-switcher.js';
// 初始化多语言
initI18n();
// 初始化语言切换器
const langContainer = document.getElementById('langSwitcherContainer');
if (langContainer) {
import('./app/language-switcher.js').then(module => {
const switcher = module.createLanguageSwitcher();
langContainer.appendChild(switcher);
// 绑定事件逻辑(由于是动态创建,复用逻辑)
const languageBtn = switcher.querySelector('#languageBtn');
const languageDropdown = switcher.querySelector('#languageDropdown');
const languageOptions = switcher.querySelectorAll('.language-option');
languageBtn.addEventListener('click', (e) => {
e.stopPropagation();
languageDropdown.classList.toggle('show');
});
languageOptions.forEach(option => {
option.addEventListener('click', (e) => {
e.stopPropagation();
const lang = option.getAttribute('data-lang');
setLanguage(lang);
switcher.querySelector('.current-lang').textContent = lang === 'zh-CN' ? '中文' : 'EN';
languageOptions.forEach(opt => opt.classList.remove('active'));
option.classList.add('active');
languageDropdown.classList.remove('show');
});
});
document.addEventListener('click', () => {
languageDropdown.classList.remove('show');
});
});
}
const loginForm = document.getElementById('loginForm');
const passwordInput = document.getElementById('password');
const errorMessage = document.getElementById('errorMessage');
const loginButton = document.getElementById('loginButton');
// 检查是否已经登录
checkLoginStatus();
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
const password = passwordInput.value.trim();
if (!password) {
showError(t('login.error.empty'));
return;
}
// 禁用按钮并显示加载状态
loginButton.disabled = true;
loginButton.innerHTML = `<span class="loading"></span>${t('login.loggingIn')}`;
errorMessage.classList.remove('show');
try {
// 直接使用fetch进行登录请求登录页面不需要token
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
password
})
});
const data = await response.json();
if (response.ok && data.success) {
// 登录成功保存token
localStorage.setItem('authToken', data.token);
// 跳转到主页
window.location.href = '/';
} else {
const errorMsg = data.messageCode ? t(data.messageCode, data.messageParams) : (data.message || t('login.error.incorrect'));
showError(errorMsg);
loginButton.disabled = false;
loginButton.innerHTML = t('login.button');
passwordInput.value = '';
passwordInput.focus();
}
} catch (error) {
console.error('登录错误:', error);
showError(t('login.error.failed'));
loginButton.disabled = false;
loginButton.innerHTML = t('login.button');
}
});
function showError(message) {
errorMessage.textContent = message;
errorMessage.classList.add('show');
passwordInput.classList.add('error');
setTimeout(() => {
passwordInput.classList.remove('error');
}, 300);
}
function checkLoginStatus() {
const token = localStorage.getItem('authToken');
if (token) {
// Token存在跳转到主页
window.location.href = '/';
}
}
// 监听输入,清除错误提示
passwordInput.addEventListener('input', () => {
errorMessage.classList.remove('show');
});
// 页面加载时聚焦到密码输入框
passwordInput.focus();
</script>
</body>
</html>