mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-08-28 11:37:00 +08:00
```git
feat: 添加密钥检查调度器并重构前端UI
主要变更:
- **调度器功能:**
- 集成 APScheduler 实现定时任务,用于定期检查API密钥的有效性。
- 在 `.env.example` 和 `app/config/config.py` 中添加了 `CHECK_INTERVAL_HOURS` 和 `TIMEZONE` 配置项。
- 在应用生命周期 (`app/core/application.py`) 中添加了调度器的启动和停止逻辑。
- 新增 `app/scheduler/` 目录及相关实现 (`key_checker.py`)。
- 新增 `app/router/scheduler_routes.py` 用于调度器相关API (如果未来需要)。
- 在 `requirements.txt` 中添加 `apscheduler` 依赖。
- **前端重构与改进:**
- 引入 `app/templates/base.html` 作为基础模板,统一页面结构和样式引入。
- 使用新的样式(推测为Tailwind CSS)重构了 `auth.html`, `config_editor.html`, `error_logs.html`, `keys_status.html` 页面,提升了UI一致性和响应式布局。
- 删除了旧的CSS文件 (`auth.css`, `config_editor.css`, `error_logs.css`, `keys_status.css`)。
- 更新了对应的 JavaScript 文件 (`config_editor.js`, `error_logs.js`, `keys_status.js`) 以适应新的HTML结构和交互。
- 在 `keys_status.html` 页面增加了按失败次数过滤密钥、批量重置失败次数、确认模态框等功能。
- 添加了新的 Logo 图片 (`logo.png`, `logo1.png`)。
- **其他:**
- 更新了 `app/router/routes.py` 以包含新的路由。
- 对 `app/service/key/key_manager.py` 和 `app/database/services.py` 进行了相关调整以支持新功能。
```
This commit is contained in:
@@ -310,9 +310,13 @@ function switchTab(tabId) {
|
||||
const tabButtons = document.querySelectorAll('.tab-btn');
|
||||
tabButtons.forEach(button => {
|
||||
if (button.getAttribute('data-tab') === tabId) {
|
||||
button.classList.add('active');
|
||||
// 激活状态:主色背景,白色文字,添加阴影
|
||||
button.classList.remove('bg-white', 'bg-opacity-50', 'text-gray-700', 'hover:bg-opacity-70');
|
||||
button.classList.add('bg-primary-600', 'text-white', 'shadow-md');
|
||||
} else {
|
||||
button.classList.remove('active');
|
||||
// 非激活状态:白色背景,灰色文字,无阴影
|
||||
button.classList.remove('bg-primary-600', 'text-white', 'shadow-md');
|
||||
button.classList.add('bg-white', 'bg-opacity-50', 'text-gray-700', 'hover:bg-opacity-70');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -354,18 +358,19 @@ function addArrayItemWithValue(key, value) {
|
||||
if (!container) return;
|
||||
|
||||
const arrayItem = document.createElement('div');
|
||||
arrayItem.className = 'array-item';
|
||||
arrayItem.className = 'array-item flex justify-between items-center mb-2'; // 使用 Flexbox 布局,垂直居中,底部增加间距
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.name = `${key}[]`;
|
||||
input.value = value;
|
||||
input.className = 'array-input';
|
||||
input.className = 'array-input flex-grow px-3 py-2 rounded-md border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 mr-2'; // 输入框占据大部分空间,添加样式和右边距
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.type = 'button';
|
||||
removeBtn.className = 'remove-btn';
|
||||
removeBtn.innerHTML = '<i class="fas fa-times"></i>';
|
||||
removeBtn.className = 'remove-btn text-gray-400 hover:text-red-500 focus:outline-none transition-colors duration-150 ml-2'; // 新的 Tailwind 样式
|
||||
removeBtn.innerHTML = '<i class="fas fa-trash-alt"></i>'; // 改用垃圾桶图标
|
||||
removeBtn.title = '删除'; // 添加悬停提示
|
||||
removeBtn.addEventListener('click', function() {
|
||||
arrayItem.remove();
|
||||
});
|
||||
@@ -411,12 +416,43 @@ function collectFormData() {
|
||||
return formData;
|
||||
}
|
||||
|
||||
// 辅助函数:停止定时任务
|
||||
async function stopScheduler() {
|
||||
try {
|
||||
const response = await fetch('/api/scheduler/stop', { method: 'POST' });
|
||||
if (!response.ok) {
|
||||
console.warn(`停止定时任务失败: ${response.status}`);
|
||||
} else {
|
||||
console.log('定时任务已停止');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('调用停止定时任务API时出错:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数:启动定时任务
|
||||
async function startScheduler() {
|
||||
try {
|
||||
const response = await fetch('/api/scheduler/start', { method: 'POST' });
|
||||
if (!response.ok) {
|
||||
console.warn(`启动定时任务失败: ${response.status}`);
|
||||
} else {
|
||||
console.log('定时任务已启动');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('调用启动定时任务API时出错:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
async function saveConfig() {
|
||||
try {
|
||||
const formData = collectFormData();
|
||||
|
||||
|
||||
showNotification('正在保存配置...', 'info');
|
||||
|
||||
// 1. 停止定时任务
|
||||
await stopScheduler();
|
||||
|
||||
const response = await fetch('/api/config', {
|
||||
method: 'PUT',
|
||||
@@ -435,24 +471,37 @@ async function saveConfig() {
|
||||
|
||||
// 显示保存状态
|
||||
const saveStatus = document.getElementById('saveStatus');
|
||||
saveStatus.classList.add('show');
|
||||
saveStatus.style.opacity = "1";
|
||||
saveStatus.style.transform = "translate(-50%, -50%) scale(1.1)";
|
||||
|
||||
setTimeout(() => {
|
||||
saveStatus.classList.remove('show');
|
||||
saveStatus.style.opacity = "0";
|
||||
saveStatus.style.transform = "translate(-50%, -50%) scale(0.95)";
|
||||
}, 3000);
|
||||
|
||||
showNotification('配置保存成功', 'success');
|
||||
|
||||
// 3. 启动新的定时任务
|
||||
await startScheduler();
|
||||
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error);
|
||||
|
||||
// 保存失败时,也尝试重启定时任务,以防万一
|
||||
await startScheduler();
|
||||
// 显示错误状态
|
||||
const saveStatus = document.getElementById('saveStatus');
|
||||
saveStatus.classList.add('show', 'error');
|
||||
saveStatus.style.backgroundColor = "#ef4444"; // 红色背景
|
||||
saveStatus.style.opacity = "1";
|
||||
saveStatus.style.transform = "translate(-50%, -50%) scale(1.1)";
|
||||
saveStatus.querySelector('.status-icon i').className = 'fas fa-times-circle';
|
||||
saveStatus.querySelector('.status-text').textContent = '配置保存失败';
|
||||
|
||||
setTimeout(() => {
|
||||
saveStatus.classList.remove('show', 'error');
|
||||
saveStatus.style.opacity = "0";
|
||||
saveStatus.style.transform = "translate(-50%, -50%) scale(0.95)";
|
||||
setTimeout(() => {
|
||||
saveStatus.style.backgroundColor = "#22c55e"; // 恢复绿色背景
|
||||
}, 300);
|
||||
}, 3000);
|
||||
|
||||
showNotification('保存配置失败: ' + error.message, 'error');
|
||||
@@ -491,6 +540,9 @@ function resetConfig(event) {
|
||||
async function executeReset() {
|
||||
try {
|
||||
showNotification('正在重置配置...', 'info');
|
||||
|
||||
// 1. 停止定时任务
|
||||
await stopScheduler();
|
||||
const response = await fetch('/api/config/reset', { method: 'POST' });
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
@@ -498,24 +550,48 @@ async function executeReset() {
|
||||
const config = await response.json();
|
||||
populateForm(config);
|
||||
showNotification('配置已重置为默认值', 'success');
|
||||
|
||||
// 3. 启动新的定时任务
|
||||
await startScheduler();
|
||||
|
||||
} catch (error) {
|
||||
console.error('重置配置失败:', error);
|
||||
showNotification('重置配置失败: ' + error.message, 'error');
|
||||
// 重置失败时,也尝试重启定时任务
|
||||
await startScheduler();
|
||||
}
|
||||
}
|
||||
|
||||
// 显示通知
|
||||
function showNotification(message, type = 'info') {
|
||||
const notification = document.getElementById('notification');
|
||||
notification.textContent = message;
|
||||
notification.className = 'notification show';
|
||||
|
||||
if (type) {
|
||||
notification.classList.add(type);
|
||||
// 设置适当的样式
|
||||
if (type === 'error') {
|
||||
notification.classList.add('bg-danger-500');
|
||||
notification.classList.remove('bg-black');
|
||||
} else {
|
||||
notification.classList.remove('bg-danger-500');
|
||||
notification.classList.add('bg-black');
|
||||
|
||||
// 可以为不同类型设置不同的颜色
|
||||
if (type === 'success') {
|
||||
notification.style.backgroundColor = '#22c55e'; // 绿色
|
||||
} else if (type === 'info') {
|
||||
notification.style.backgroundColor = '#3b82f6'; // 蓝色
|
||||
} else if (type === 'warning') {
|
||||
notification.style.backgroundColor = '#f59e0b'; // 橙色
|
||||
}
|
||||
}
|
||||
|
||||
// 应用过渡效果 - 与keys_status.js中一致
|
||||
notification.style.opacity = "1";
|
||||
notification.style.transform = "translate(-50%, 0)";
|
||||
|
||||
// 设置自动消失
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('show');
|
||||
notification.style.opacity = "0";
|
||||
notification.style.transform = "translate(-50%, 10px)";
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
@@ -527,8 +603,7 @@ function refreshPage(button) {
|
||||
|
||||
// 滚动到顶部
|
||||
function scrollToTop() {
|
||||
const container = document.querySelector('.container');
|
||||
container.scrollTo({
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
@@ -536,9 +611,8 @@ function scrollToTop() {
|
||||
|
||||
// 滚动到底部
|
||||
function scrollToBottom() {
|
||||
const container = document.querySelector('.container');
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
window.scrollTo({
|
||||
top: document.body.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
+131
-43
@@ -9,32 +9,12 @@ function scrollToBottom() {
|
||||
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// 刷新页面功能
|
||||
function refreshPage(button) {
|
||||
if (button) {
|
||||
// Use 'loading' class consistent with config_editor.css animation
|
||||
button.classList.add('loading');
|
||||
// Disable button while refreshing
|
||||
button.disabled = true;
|
||||
}
|
||||
|
||||
// Fetch new data instead of full reload for a smoother experience
|
||||
loadErrorLogs().finally(() => {
|
||||
if (button) {
|
||||
// Remove loading class and re-enable button after fetch completes
|
||||
button.classList.remove('loading');
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
// Optional: Keep reload as fallback or if preferred
|
||||
// setTimeout(() => {
|
||||
// window.location.reload();
|
||||
// }, 500);
|
||||
}
|
||||
// Refresh function removed as the buttons are gone.
|
||||
// If refresh functionality is needed elsewhere, it can be triggered directly by calling loadErrorLogs().
|
||||
|
||||
// 全局变量
|
||||
let currentPage = 1;
|
||||
let pageSize = 20;
|
||||
let pageSize = 10;
|
||||
// let totalPages = 1; // totalPages will be calculated dynamically based on API response if available, or based on fetched data length
|
||||
let errorLogs = []; // Store fetched logs for details view
|
||||
let currentSearch = { // Store current search parameters
|
||||
@@ -46,7 +26,7 @@ let currentSearch = { // Store current search parameters
|
||||
|
||||
// DOM Elements Cache
|
||||
let pageSizeSelector;
|
||||
let refreshBtn;
|
||||
// let refreshBtn; // Removed, as the button is deleted
|
||||
let tableBody;
|
||||
let paginationElement;
|
||||
let loadingIndicator;
|
||||
@@ -59,12 +39,14 @@ let errorSearchInput;
|
||||
let startDateInput;
|
||||
let endDateInput;
|
||||
let searchBtn;
|
||||
let pageInput; // 新增:页码输入框
|
||||
let goToPageBtn; // 新增:跳转按钮
|
||||
|
||||
// 页面加载完成后执行
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Cache DOM elements
|
||||
pageSizeSelector = document.getElementById('pageSize');
|
||||
refreshBtn = document.getElementById('refreshBtn');
|
||||
// refreshBtn = document.getElementById('refreshBtn'); // Removed
|
||||
tableBody = document.getElementById('errorLogsTable');
|
||||
paginationElement = document.getElementById('pagination');
|
||||
loadingIndicator = document.getElementById('loadingIndicator');
|
||||
@@ -78,6 +60,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
startDateInput = document.getElementById('startDate');
|
||||
endDateInput = document.getElementById('endDate');
|
||||
searchBtn = document.getElementById('searchBtn');
|
||||
pageInput = document.getElementById('pageInput'); // 新增
|
||||
goToPageBtn = document.getElementById('goToPageBtn'); // 新增
|
||||
|
||||
// Initialize page size selector
|
||||
if (pageSizeSelector) {
|
||||
@@ -89,18 +73,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize refresh button (using the one inside the controls container)
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', function() {
|
||||
// Add loading state to the button itself
|
||||
this.classList.add('loading');
|
||||
this.disabled = true;
|
||||
loadErrorLogs().finally(() => {
|
||||
this.classList.remove('loading');
|
||||
this.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
// Refresh button event listener removed
|
||||
|
||||
// Initialize search button
|
||||
if (searchBtn) {
|
||||
@@ -130,8 +103,113 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Initial load of error logs
|
||||
loadErrorLogs();
|
||||
|
||||
// Add event listeners for copy buttons inside the modal
|
||||
setupCopyButtons();
|
||||
|
||||
// 新增:为页码跳转按钮添加事件监听器
|
||||
if (goToPageBtn && pageInput) {
|
||||
goToPageBtn.addEventListener('click', function() {
|
||||
const targetPage = parseInt(pageInput.value);
|
||||
// 需要获取总页数来验证输入
|
||||
// 暂时无法直接获取 totalPages,需要在 updatePagination 中存储或重新计算
|
||||
// 简单的验证:必须是正整数
|
||||
if (!isNaN(targetPage) && targetPage >= 1) {
|
||||
// 理想情况下,应检查 targetPage <= totalPages
|
||||
// 但 totalPages 可能未知,所以暂时只跳转
|
||||
currentPage = targetPage;
|
||||
loadErrorLogs();
|
||||
pageInput.value = ''; // 清空输入框
|
||||
} else {
|
||||
showNotification('请输入有效的页码', 'error', 2000);
|
||||
pageInput.value = ''; // 清空无效输入
|
||||
}
|
||||
});
|
||||
// 允许按 Enter 键跳转
|
||||
pageInput.addEventListener('keypress', function(event) {
|
||||
if (event.key === 'Enter') {
|
||||
goToPageBtn.click(); // 触发按钮点击
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Fallback copy function using document.execCommand
|
||||
function fallbackCopyTextToClipboard(text) {
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
|
||||
// Avoid scrolling to bottom
|
||||
textArea.style.top = "0";
|
||||
textArea.style.left = "0";
|
||||
textArea.style.position = "fixed";
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
let successful = false;
|
||||
try {
|
||||
successful = document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.error('Fallback copy failed:', err);
|
||||
successful = false;
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
return successful;
|
||||
}
|
||||
|
||||
// Helper function to handle feedback after copy attempt (both modern and fallback)
|
||||
function handleCopyResult(buttonElement, success) {
|
||||
const originalIcon = buttonElement.querySelector('i').className; // Store original icon class
|
||||
const iconElement = buttonElement.querySelector('i');
|
||||
if (success) {
|
||||
iconElement.className = 'fas fa-check text-success-500'; // Use checkmark icon class
|
||||
showNotification('已复制到剪贴板', 'success', 2000);
|
||||
} else {
|
||||
iconElement.className = 'fas fa-times text-danger-500'; // Use error icon class
|
||||
showNotification('复制失败', 'error', 3000);
|
||||
}
|
||||
setTimeout(() => { iconElement.className = originalIcon; }, success ? 2000 : 3000); // Restore original icon class
|
||||
}
|
||||
|
||||
// Function to set up copy button listeners (using modern API with fallback)
|
||||
function setupCopyButtons() {
|
||||
const copyButtons = document.querySelectorAll('.copy-btn');
|
||||
copyButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const targetId = this.getAttribute('data-target');
|
||||
const targetElement = document.getElementById(targetId);
|
||||
|
||||
if (targetElement) {
|
||||
const textToCopy = targetElement.textContent;
|
||||
let copySuccess = false;
|
||||
|
||||
// Try modern clipboard API first (requires HTTPS or localhost)
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
handleCopyResult(this, true); // Use helper for feedback
|
||||
}).catch(err => {
|
||||
console.error('Clipboard API failed, attempting fallback:', err);
|
||||
// Attempt fallback if modern API fails
|
||||
copySuccess = fallbackCopyTextToClipboard(textToCopy);
|
||||
handleCopyResult(this, copySuccess); // Use helper for feedback
|
||||
});
|
||||
} else {
|
||||
// Use fallback if modern API is not available or context is insecure
|
||||
console.warn("Clipboard API not available or context insecure. Using fallback copy method.");
|
||||
copySuccess = fallbackCopyTextToClipboard(textToCopy);
|
||||
handleCopyResult(this, copySuccess); // Use helper for feedback
|
||||
}
|
||||
} else {
|
||||
console.error('Target element not found:', targetId);
|
||||
showNotification('复制出错:找不到目标元素', 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 加载错误日志数据
|
||||
async function loadErrorLogs() {
|
||||
showLoading(true);
|
||||
@@ -389,10 +467,18 @@ function updatePagination(currentItemCount, totalItems) {
|
||||
// Helper function to add pagination links
|
||||
function addPaginationLink(parentElement, text, enabled, clickHandler, isActive = false) {
|
||||
const pageItem = document.createElement('li');
|
||||
pageItem.className = `page-item ${!enabled ? 'disabled' : ''} ${isActive ? 'active' : ''}`;
|
||||
// 移除 'page-item' 和 'active' 类,使用 Tailwind 类进行样式化
|
||||
// pageItem.className = `page-item ${!enabled ? 'disabled' : ''} ${isActive ? 'active' : ''}`;
|
||||
|
||||
const pageLink = document.createElement('a');
|
||||
pageLink.className = 'page-link';
|
||||
// 使用 Tailwind 类进行样式化
|
||||
pageLink.className = `px-3 py-1 rounded-md text-sm transition duration-150 ease-in-out ${
|
||||
isActive
|
||||
? 'bg-primary-600 text-white font-semibold shadow-md cursor-default' // 突出当前页样式
|
||||
: enabled
|
||||
? 'bg-white text-gray-700 hover:bg-primary-50 hover:text-primary-600 border border-gray-300' // 可点击页码样式
|
||||
: 'bg-gray-100 text-gray-400 cursor-not-allowed border border-gray-200' // 禁用状态样式 (如 '...')
|
||||
}`;
|
||||
pageLink.href = '#'; // Prevent page jump
|
||||
pageLink.innerHTML = text;
|
||||
|
||||
@@ -402,12 +488,14 @@ function addPaginationLink(parentElement, text, enabled, clickHandler, isActive
|
||||
clickHandler();
|
||||
});
|
||||
} else if (!enabled) {
|
||||
pageLink.addEventListener('click', e => e.preventDefault()); // Prevent click on disabled
|
||||
pageLink.addEventListener('click', e => e.preventDefault()); // Prevent click on disabled or active
|
||||
} else if (isActive) {
|
||||
pageLink.addEventListener('click', e => e.preventDefault()); // Prevent click on active page
|
||||
}
|
||||
|
||||
|
||||
pageItem.appendChild(pageLink);
|
||||
parentElement.appendChild(pageItem);
|
||||
// 不再需要 li 元素,直接将 a 元素添加到父元素
|
||||
// pageItem.appendChild(pageLink);
|
||||
parentElement.appendChild(pageLink);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+258
-49
@@ -27,15 +27,21 @@ function copyToClipboard(text) {
|
||||
|
||||
function copyKeys(type) {
|
||||
const keys = Array.from(document.querySelectorAll(`#${type}Keys .key-text`)).map(span => span.dataset.fullKey);
|
||||
const jsonKeys = JSON.stringify(keys);
|
||||
|
||||
copyToClipboard(jsonKeys)
|
||||
if (keys.length === 0) {
|
||||
showCopyStatus('没有可复制的密钥', true);
|
||||
return;
|
||||
}
|
||||
|
||||
const keysText = keys.join('\n');
|
||||
|
||||
copyToClipboard(keysText)
|
||||
.then(() => {
|
||||
showCopyStatus(`已成功复制${type === 'valid' ? '有效' : '无效'}密钥到剪贴板`);
|
||||
showCopyStatus(`已成功复制${keys.length}个${type === 'valid' ? '有效' : '无效'}密钥到剪贴板`);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('无法复制文本: ', err);
|
||||
showCopyStatus('复制失败,请重试');
|
||||
showCopyStatus('复制失败,请重试', true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,21 +52,32 @@ function copyKey(key) {
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('无法复制文本: ', err);
|
||||
showCopyStatus('复制失败,请重试');
|
||||
showCopyStatus('复制失败,请重试', true);
|
||||
});
|
||||
}
|
||||
|
||||
function showCopyStatus(message, type = 'success') {
|
||||
function showCopyStatus(message, isError = false) {
|
||||
const statusElement = document.getElementById('copyStatus');
|
||||
statusElement.textContent = message;
|
||||
statusElement.className = type; // 设置样式类
|
||||
statusElement.style.opacity = 1;
|
||||
|
||||
// 添加适当的样式类
|
||||
if (isError) {
|
||||
statusElement.classList.add('bg-danger-500');
|
||||
statusElement.classList.remove('bg-black');
|
||||
} else {
|
||||
statusElement.classList.remove('bg-danger-500');
|
||||
statusElement.classList.add('bg-black');
|
||||
}
|
||||
|
||||
// 应用过渡效果
|
||||
statusElement.style.opacity = "1";
|
||||
statusElement.style.transform = "translate(-50%, 0)";
|
||||
|
||||
// 设置自动消失
|
||||
setTimeout(() => {
|
||||
statusElement.style.opacity = 0;
|
||||
setTimeout(() => {
|
||||
statusElement.className = ''; // 清除样式类
|
||||
}, 300);
|
||||
}, 2000);
|
||||
statusElement.style.opacity = "0";
|
||||
statusElement.style.transform = "translate(-50%, 10px)";
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
async function verifyKey(key, button) {
|
||||
@@ -70,59 +87,223 @@ async function verifyKey(key, button) {
|
||||
const originalHtml = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 验证中';
|
||||
|
||||
const response = await fetch(`/gemini/v1beta/verify-key/${key}`, {
|
||||
try {
|
||||
const response = await fetch(`/gemini/v1beta/verify-key/${key}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// 根据验证结果更新UI并显示模态提示框
|
||||
if (data.success || data.status === 'valid') {
|
||||
// 验证成功,显示成功结果
|
||||
button.style.backgroundColor = '#27ae60';
|
||||
// 使用结果模态框显示成功消息
|
||||
showResultModal(true, '密钥验证成功');
|
||||
// 模态框关闭时会自动刷新页面
|
||||
} else {
|
||||
// 验证失败,显示失败结果
|
||||
const errorMsg = data.error || '密钥无效';
|
||||
button.style.backgroundColor = '#e74c3c';
|
||||
// 使用结果模态框显示失败消息,但不自动刷新页面
|
||||
showResultModal(false, '密钥验证失败: ' + errorMsg, true); // 改为true以在关闭时刷新
|
||||
}
|
||||
} catch (fetchError) {
|
||||
console.error('API请求失败:', fetchError);
|
||||
showResultModal(false, '验证请求失败: ' + fetchError.message, true); // 改为true以在关闭时刷新
|
||||
} finally {
|
||||
// 1秒后恢复按钮原始状态
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHtml;
|
||||
button.disabled = false;
|
||||
button.style.backgroundColor = '';
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('验证失败:', error);
|
||||
button.disabled = false;
|
||||
button.innerHTML = '<i class="fas fa-check-circle"></i> 验证';
|
||||
showResultModal(false, '验证处理失败: ' + error.message, true); // 改为true以在关闭时刷新
|
||||
}
|
||||
}
|
||||
|
||||
async function resetKeyFailCount(key, button) {
|
||||
try {
|
||||
// 禁用按钮并显示加载状态
|
||||
button.disabled = true;
|
||||
const originalHtml = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 重置中';
|
||||
|
||||
const response = await fetch(`/gemini/v1beta/reset-fail-count/${key}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// 根据验证结果更新UI
|
||||
if (data.status === 'valid') {
|
||||
showCopyStatus('密钥验证成功', 'success');
|
||||
// 根据重置结果更新UI
|
||||
if (data.success) {
|
||||
showCopyStatus('失败计数重置成功');
|
||||
button.style.backgroundColor = '#27ae60';
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
} else {
|
||||
showCopyStatus('密钥验证失败', 'error');
|
||||
const errorMsg = data.message || '重置失败';
|
||||
showCopyStatus('重置失败: ' + errorMsg, true);
|
||||
button.style.backgroundColor = '#e74c3c';
|
||||
}
|
||||
|
||||
// 3秒后恢复按钮原始状态
|
||||
// 1秒后恢复按钮原始状态
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHtml;
|
||||
button.disabled = false;
|
||||
button.style.backgroundColor = '';
|
||||
}, 3000);
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('验证失败:', error);
|
||||
showCopyStatus('验证请求失败', 'error');
|
||||
console.error('重置失败:', error);
|
||||
showCopyStatus('重置请求失败: ' + error.message, true);
|
||||
button.disabled = false;
|
||||
button.innerHTML = '<i class="fas fa-check-circle"></i> 验证';
|
||||
button.innerHTML = '<i class="fas fa-redo-alt"></i> 重置';
|
||||
}
|
||||
}
|
||||
|
||||
function showResetModal(type) {
|
||||
const modalElement = document.getElementById('resetModal');
|
||||
const titleElement = document.getElementById('resetModalTitle');
|
||||
const messageElement = document.getElementById('resetModalMessage');
|
||||
const confirmButton = document.getElementById('confirmResetBtn');
|
||||
|
||||
// 设置标题和消息
|
||||
titleElement.textContent = '批量重置失败次数';
|
||||
messageElement.textContent = `确定要批量重置${type === 'valid' ? '有效' : '无效'}密钥的失败次数吗?`;
|
||||
|
||||
// 设置确认按钮事件
|
||||
confirmButton.onclick = () => executeResetAll(type);
|
||||
|
||||
// 显示模态框
|
||||
modalElement.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeResetModal() {
|
||||
document.getElementById('resetModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
// 触发显示模态框
|
||||
function resetAllKeysFailCount(type, event) {
|
||||
// 阻止事件冒泡
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
// 显示模态确认框
|
||||
showResetModal(type);
|
||||
}
|
||||
|
||||
// 执行批量重置
|
||||
// 关闭模态框并根据参数决定是否刷新页面
|
||||
function closeResultModal(reload = true) {
|
||||
document.getElementById('resultModal').classList.add('hidden');
|
||||
if (reload) {
|
||||
location.reload(); // 操作完成后刷新页面
|
||||
}
|
||||
}
|
||||
|
||||
// 显示操作结果模态框
|
||||
function showResultModal(success, message, autoReload = true) {
|
||||
const modalElement = document.getElementById('resultModal');
|
||||
const titleElement = document.getElementById('resultModalTitle');
|
||||
const messageElement = document.getElementById('resultModalMessage');
|
||||
const iconElement = document.getElementById('resultIcon');
|
||||
const confirmButton = document.getElementById('resultModalConfirmBtn');
|
||||
|
||||
// 设置标题
|
||||
titleElement.textContent = success ? '操作成功' : '操作失败';
|
||||
|
||||
// 设置图标
|
||||
if (success) {
|
||||
iconElement.innerHTML = '<i class="fas fa-check-circle text-success-500"></i>';
|
||||
iconElement.className = 'text-5xl mb-3 text-success-500';
|
||||
} else {
|
||||
iconElement.innerHTML = '<i class="fas fa-times-circle"></i>';
|
||||
iconElement.className = 'text-5xl mb-3 text-danger-500';
|
||||
}
|
||||
|
||||
// 设置消息
|
||||
messageElement.textContent = message;
|
||||
|
||||
// 设置确认按钮点击事件
|
||||
confirmButton.onclick = () => closeResultModal(autoReload);
|
||||
|
||||
// 显示模态框
|
||||
modalElement.classList.remove('hidden');
|
||||
}
|
||||
|
||||
async function executeResetAll(type) {
|
||||
try {
|
||||
// 关闭确认模态框
|
||||
closeResetModal();
|
||||
|
||||
// 使用data-reset-type属性直接找到对应的重置按钮
|
||||
const resetButton = document.querySelector(`button[data-reset-type="${type}"]`);
|
||||
|
||||
if (!resetButton) {
|
||||
// 如果找不到按钮,显示错误并返回
|
||||
showResultModal(false, `找不到${type === 'valid' ? '有效' : '无效'}密钥区域的批量重置按钮`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用按钮并显示加载状态
|
||||
resetButton.disabled = true;
|
||||
const originalHtml = resetButton.innerHTML;
|
||||
resetButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 重置中';
|
||||
|
||||
try {
|
||||
// 调用API,传递类型参数
|
||||
const response = await fetch(`/gemini/v1beta/reset-all-fail-counts?key_type=${type}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`服务器返回错误: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 根据重置结果显示模态框
|
||||
if (data.success) {
|
||||
const message = data.reset_count ?
|
||||
`成功重置${data.reset_count}个${type === 'valid' ? '有效' : '无效'}密钥的失败次数` :
|
||||
'所有失败次数重置成功';
|
||||
showResultModal(true, message);
|
||||
} else {
|
||||
const errorMsg = data.message || '批量重置失败';
|
||||
showResultModal(false, '批量重置失败: ' + errorMsg);
|
||||
}
|
||||
} catch (fetchError) {
|
||||
console.error('API请求失败:', fetchError);
|
||||
showResultModal(false, '批量重置请求失败: ' + fetchError.message);
|
||||
} finally {
|
||||
// 恢复按钮原始状态
|
||||
setTimeout(() => {
|
||||
resetButton.innerHTML = originalHtml;
|
||||
resetButton.disabled = false;
|
||||
}, 500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量重置失败:', error);
|
||||
showResultModal(false, '批量重置处理失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToTop() {
|
||||
const container = document.querySelector('.container');
|
||||
container.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
const container = document.querySelector('.container');
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// 移除这个函数,因为它可能正在干扰按钮的显示
|
||||
// HTML中已经设置了滚动按钮为flex显示,不需要JavaScript额外控制
|
||||
function updateScrollButtons() {
|
||||
const container = document.querySelector('.container');
|
||||
const scrollButtons = document.querySelector('.scroll-buttons');
|
||||
if (container.scrollHeight > container.clientHeight) {
|
||||
scrollButtons.style.display = 'flex';
|
||||
} else {
|
||||
scrollButtons.style.display = 'none';
|
||||
}
|
||||
// 不执行任何操作
|
||||
}
|
||||
|
||||
function refreshPage(button) {
|
||||
@@ -142,22 +323,50 @@ function toggleSection(header, sectionId) {
|
||||
content.classList.toggle('collapsed');
|
||||
}
|
||||
|
||||
// 筛选有效密钥(根据失败次数阈值)
|
||||
function filterValidKeys() {
|
||||
const thresholdInput = document.getElementById('failCountThreshold');
|
||||
const validKeyItems = document.querySelectorAll('#validKeys li');
|
||||
// 读取阈值,如果输入无效或为空,则默认为0(不过滤)
|
||||
const threshold = parseInt(thresholdInput.value, 10);
|
||||
const filterThreshold = isNaN(threshold) || threshold < 0 ? 0 : threshold;
|
||||
|
||||
validKeyItems.forEach(item => {
|
||||
const failCount = parseInt(item.dataset.failCount, 10);
|
||||
// 如果失败次数大于等于阈值,则显示,否则隐藏
|
||||
if (failCount >= filterThreshold) {
|
||||
item.style.display = ''; // 显示
|
||||
} else {
|
||||
item.style.display = 'none'; // 隐藏
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 检查滚动按钮
|
||||
updateScrollButtons();
|
||||
|
||||
// 移除对滚动按钮显示的控制,让它们由HTML/CSS控制
|
||||
|
||||
// 监听展开/折叠事件
|
||||
document.querySelectorAll('.key-list h2').forEach(header => {
|
||||
header.addEventListener('click', () => {
|
||||
setTimeout(updateScrollButtons, 300);
|
||||
// 不再调用updateScrollButtons
|
||||
});
|
||||
});
|
||||
|
||||
// 更新版权年份
|
||||
const copyrightYear = document.querySelector('.copyright script');
|
||||
if (copyrightYear) {
|
||||
copyrightYear.textContent = new Date().getFullYear();
|
||||
const copyrightYearElement = document.querySelector('.copyright script');
|
||||
if (copyrightYearElement && copyrightYearElement.parentNode.classList.contains('copyright')) {
|
||||
// 确保只更新版权部分的年份
|
||||
copyrightYearElement.textContent = new Date().getFullYear();
|
||||
}
|
||||
|
||||
// 添加筛选输入框事件监听
|
||||
const thresholdInput = document.getElementById('failCountThreshold');
|
||||
if (thresholdInput) {
|
||||
// 使用 'input' 事件实时响应输入变化
|
||||
thresholdInput.addEventListener('input', filterValidKeys);
|
||||
// 初始加载时应用一次筛选(基于默认值1)
|
||||
filterValidKeys();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -174,8 +383,8 @@ if ('serviceWorker' in navigator) {
|
||||
});
|
||||
}
|
||||
function toggleKeyVisibility(button) {
|
||||
const keyInfoDiv = button.closest('.key-info');
|
||||
const keyTextSpan = keyInfoDiv.querySelector('.key-text');
|
||||
const keyContainer = button.closest('.flex.items-center.gap-1');
|
||||
const keyTextSpan = keyContainer.querySelector('.key-text');
|
||||
const eyeIcon = button.querySelector('i');
|
||||
const fullKey = keyTextSpan.dataset.fullKey;
|
||||
const maskedKey = fullKey.substring(0, 4) + '...' + fullKey.substring(fullKey.length - 4);
|
||||
|
||||
Reference in New Issue
Block a user