mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-09-07 08:46:37 +08:00
feat: 添加下拉菜单和批量操作功能
- 新增 /api/keys/all 端点获取所有密钥 - 添加下拉菜单界面,提供复制全部密钥和验证所有密钥选项 - 重构批量重置逻辑,改为逐个处理以提供更好的进度反馈 - 新增批量操作进度模态框,实时显示操作状态和日志 - 在验证模态框中添加批次大小配置选项 - 优化用户体验,提供更直观的批量操作界面
This commit is contained in:
@@ -61,3 +61,23 @@ async def get_keys_paginated(
|
|||||||
"total_pages": (total_items + limit - 1) // limit,
|
"total_pages": (total_items + limit - 1) // limit,
|
||||||
"current_page": page,
|
"current_page": page,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@router.get("/api/keys/all")
|
||||||
|
async def get_all_keys(
|
||||||
|
request: Request,
|
||||||
|
key_manager: KeyManager = Depends(get_key_manager_instance),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get all keys (both valid and invalid) for bulk operations.
|
||||||
|
"""
|
||||||
|
auth_token = request.cookies.get("auth_token")
|
||||||
|
if not auth_token or not verify_auth_token(auth_token):
|
||||||
|
return JSONResponse(status_code=401, content={"detail": "Unauthorized"})
|
||||||
|
|
||||||
|
all_keys_with_status = await key_manager.get_all_keys_with_fail_count()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"valid_keys": list(all_keys_with_status["valid_keys"].keys()),
|
||||||
|
"invalid_keys": list(all_keys_with_status["invalid_keys"].keys()),
|
||||||
|
"total_count": len(all_keys_with_status["valid_keys"]) + len(all_keys_with_status["invalid_keys"])
|
||||||
|
}
|
||||||
|
|||||||
+354
-110
@@ -602,82 +602,50 @@ function showVerificationResultModal(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function executeResetAll(type) {
|
async function executeResetAll(type) {
|
||||||
try {
|
closeResetModal();
|
||||||
// 关闭确认模态框
|
const keysToReset = getSelectedKeys(type);
|
||||||
closeResetModal();
|
if (keysToReset.length === 0) {
|
||||||
|
showNotification("没有选中的密钥可重置", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 找到对应的重置按钮以显示加载状态
|
showProgressModal(`批量重置 ${keysToReset.length} 个密钥的失败计数`);
|
||||||
const resetButton = document.querySelector(
|
|
||||||
`button[data-reset-type="${type}"]`
|
|
||||||
);
|
|
||||||
if (!resetButton) {
|
|
||||||
showResultModal(
|
|
||||||
false,
|
|
||||||
`找不到${type === "valid" ? "有效" : "无效"}密钥区域的批量重置按钮`,
|
|
||||||
false
|
|
||||||
); // Don't reload if button not found
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取选中的密钥
|
let successCount = 0;
|
||||||
const keysToReset = getSelectedKeys(type);
|
let failCount = 0;
|
||||||
|
|
||||||
if (keysToReset.length === 0) {
|
for (let i = 0; i < keysToReset.length; i++) {
|
||||||
showNotification(
|
const key = keysToReset[i];
|
||||||
`没有选中的${type === "valid" ? "有效" : "无效"}密钥可重置`,
|
const keyDisplay = `${key.substring(0, 4)}...${key.substring(
|
||||||
"warning"
|
key.length - 4
|
||||||
);
|
)}`;
|
||||||
return;
|
updateProgress(i, keysToReset.length, `正在重置: ${keyDisplay}`);
|
||||||
}
|
|
||||||
|
|
||||||
// 禁用按钮并显示加载状态
|
|
||||||
resetButton.disabled = true;
|
|
||||||
const originalHtml = resetButton.innerHTML;
|
|
||||||
resetButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 重置中';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const options = {
|
const data = await fetchAPI(`/gemini/v1beta/reset-fail-count/${key}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
});
|
||||||
body: JSON.stringify({ keys: keysToReset, key_type: type }),
|
|
||||||
};
|
|
||||||
const data = await fetchAPI(
|
|
||||||
`/gemini/v1beta/reset-selected-fail-counts`,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
|
|
||||||
// 根据重置结果显示模态框
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
const message =
|
successCount++;
|
||||||
data.reset_count !== undefined
|
addProgressLog(`✅ ${keyDisplay}: 重置成功`);
|
||||||
? `成功重置 ${data.reset_count} 个选中的${
|
|
||||||
type === "valid" ? "有效" : "无效"
|
|
||||||
}密钥的失败次数`
|
|
||||||
: `成功重置 ${keysToReset.length} 个选中的密钥`;
|
|
||||||
showResultModal(true, message); // 成功后刷新页面
|
|
||||||
} else {
|
} else {
|
||||||
const errorMsg = data.message || "批量重置失败";
|
failCount++;
|
||||||
// 失败后不自动刷新页面,让用户看到错误信息
|
addProgressLog(
|
||||||
showResultModal(false, "批量重置失败: " + errorMsg, false);
|
`❌ ${keyDisplay}: 重置失败 - ${data.message || "未知错误"}`,
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (apiError) {
|
} catch (apiError) {
|
||||||
console.error("批量重置 API 请求失败:", apiError);
|
failCount++;
|
||||||
showResultModal(false, `批量重置请求失败: ${apiError.message}`, false);
|
addProgressLog(`❌ ${keyDisplay}: 请求失败 - ${apiError.message}`, true);
|
||||||
} finally {
|
|
||||||
// 恢复按钮状态 (仅在不刷新的情况下)
|
|
||||||
if (
|
|
||||||
!document.getElementById("resultModal") ||
|
|
||||||
document.getElementById("resultModal").classList.contains("hidden") ||
|
|
||||||
document.getElementById("resultModalTitle").textContent.includes("失败")
|
|
||||||
) {
|
|
||||||
resetButton.innerHTML = originalHtml;
|
|
||||||
resetButton.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error("批量重置处理失败:", error);
|
|
||||||
showResultModal(false, "批量重置处理失败: " + error.message, false); // 失败后不自动刷新
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateProgress(
|
||||||
|
keysToReset.length,
|
||||||
|
keysToReset.length,
|
||||||
|
`重置完成!成功: ${successCount}, 失败: ${failCount}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToTop() {
|
function scrollToTop() {
|
||||||
@@ -929,58 +897,138 @@ function initializeGlobalBatchVerificationHandlers() {
|
|||||||
|
|
||||||
// executeVerifyAll 变为 initializeGlobalBatchVerificationHandlers 的局部函数
|
// executeVerifyAll 变为 initializeGlobalBatchVerificationHandlers 的局部函数
|
||||||
async function executeVerifyAll(type) {
|
async function executeVerifyAll(type) {
|
||||||
// Removed window.
|
closeVerifyModal();
|
||||||
try {
|
const keysToVerify = getSelectedKeys(type);
|
||||||
window.closeVerifyModal(); // Calls the global close function, which is fine.
|
if (keysToVerify.length === 0) {
|
||||||
const verifyButton = document.querySelector(
|
showNotification("没有选中的密钥可验证", "warning");
|
||||||
`#${type}BatchActions button:nth-child(1)`
|
return;
|
||||||
); // Assuming verify is the first button
|
|
||||||
let originalVerifyHtml = "";
|
|
||||||
if (verifyButton) {
|
|
||||||
originalVerifyHtml = verifyButton.innerHTML;
|
|
||||||
verifyButton.disabled = true;
|
|
||||||
verifyButton.innerHTML =
|
|
||||||
'<i class="fas fa-spinner fa-spin"></i> 验证中';
|
|
||||||
}
|
|
||||||
const keysToVerify = getSelectedKeys(type);
|
|
||||||
if (keysToVerify.length === 0) {
|
|
||||||
showNotification(
|
|
||||||
`没有选中的${type === "valid" ? "有效" : "无效"}密钥可验证`,
|
|
||||||
"warning"
|
|
||||||
);
|
|
||||||
if (verifyButton) {
|
|
||||||
// Restore button if no keys selected
|
|
||||||
verifyButton.innerHTML = originalVerifyHtml;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showNotification("开始批量验证,请稍候...", "info");
|
|
||||||
const options = {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ keys: keysToVerify }),
|
|
||||||
};
|
|
||||||
const data = await fetchAPI(
|
|
||||||
`/gemini/v1beta/verify-selected-keys`,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
if (data) {
|
|
||||||
showVerificationResultModal(data);
|
|
||||||
} else {
|
|
||||||
throw new Error("API did not return verification data.");
|
|
||||||
}
|
|
||||||
} catch (apiError) {
|
|
||||||
console.error("批量验证处理失败:", apiError);
|
|
||||||
showResultModal(false, `批量验证处理失败: ${apiError.message}`, true);
|
|
||||||
} finally {
|
|
||||||
console.log("Bulk verification process finished.");
|
|
||||||
// Button state will be reset on page reload or by updateBatchActions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const batchSizeInput = document.getElementById("batchSize");
|
||||||
|
const batchSize = parseInt(batchSizeInput.value, 10) || 10;
|
||||||
|
|
||||||
|
showProgressModal(`批量验证 ${keysToVerify.length} 个密钥`);
|
||||||
|
|
||||||
|
let allSuccessfulKeys = [];
|
||||||
|
let allFailedKeys = {};
|
||||||
|
let processedCount = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < keysToVerify.length; i += batchSize) {
|
||||||
|
const batch = keysToVerify.slice(i, i + batchSize);
|
||||||
|
const progressText = `正在验证批次 ${Math.floor(i / batchSize) + 1} / ${Math.ceil(keysToVerify.length / batchSize)} (密钥 ${i + 1}-${Math.min(i + batchSize, keysToVerify.length)})`;
|
||||||
|
|
||||||
|
updateProgress(i, keysToVerify.length, progressText);
|
||||||
|
addProgressLog(`处理批次: ${batch.length}个密钥...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ keys: batch }),
|
||||||
|
};
|
||||||
|
const data = await fetchAPI(`/gemini/v1beta/verify-selected-keys`, options);
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
if (data.successful_keys && data.successful_keys.length > 0) {
|
||||||
|
allSuccessfulKeys = allSuccessfulKeys.concat(data.successful_keys);
|
||||||
|
addProgressLog(`✅ 批次成功: ${data.successful_keys.length} 个`);
|
||||||
|
}
|
||||||
|
if (data.failed_keys && Object.keys(data.failed_keys).length > 0) {
|
||||||
|
Object.assign(allFailedKeys, data.failed_keys);
|
||||||
|
addProgressLog(`❌ 批次失败: ${Object.keys(data.failed_keys).length} 个`, true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addProgressLog(`- 批次返回空数据`, true);
|
||||||
|
}
|
||||||
|
} catch (apiError) {
|
||||||
|
addProgressLog(`❌ 批次请求失败: ${apiError.message}`, true);
|
||||||
|
// Mark all keys in this batch as failed due to API error
|
||||||
|
batch.forEach(key => {
|
||||||
|
allFailedKeys[key] = apiError.message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
processedCount += batch.length;
|
||||||
|
updateProgress(processedCount, keysToVerify.length, progressText);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(
|
||||||
|
keysToVerify.length,
|
||||||
|
keysToVerify.length,
|
||||||
|
`所有批次验证完成!`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Close progress modal and show final results
|
||||||
|
closeProgressModal(false); // Don't reload yet
|
||||||
|
showVerificationResultModal({
|
||||||
|
successful_keys: allSuccessfulKeys,
|
||||||
|
failed_keys: allFailedKeys,
|
||||||
|
valid_count: allSuccessfulKeys.length,
|
||||||
|
invalid_count: Object.keys(allFailedKeys).length
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// The confirmButton.onclick in showVerifyModal (defined earlier in initializeGlobalBatchVerificationHandlers)
|
// The confirmButton.onclick in showVerifyModal (defined earlier in initializeGlobalBatchVerificationHandlers)
|
||||||
// will correctly reference this local executeVerifyAll due to closure.
|
// will correctly reference this local executeVerifyAll due to closure.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 进度条模态框函数 ---
|
||||||
|
function showProgressModal(title) {
|
||||||
|
const modal = document.getElementById("progressModal");
|
||||||
|
const titleElement = document.getElementById("progressModalTitle");
|
||||||
|
const statusText = document.getElementById("progressStatusText");
|
||||||
|
const progressBar = document.getElementById("progressBar");
|
||||||
|
const progressPercentage = document.getElementById("progressPercentage");
|
||||||
|
const progressLog = document.getElementById("progressLog");
|
||||||
|
const closeButton = document.getElementById("progressModalCloseBtn");
|
||||||
|
const closeIcon = document.getElementById("closeProgressModalBtn");
|
||||||
|
|
||||||
|
titleElement.textContent = title;
|
||||||
|
statusText.textContent = "准备开始...";
|
||||||
|
progressBar.style.width = "0%";
|
||||||
|
progressPercentage.textContent = "0%";
|
||||||
|
progressLog.innerHTML = "";
|
||||||
|
closeButton.disabled = true;
|
||||||
|
closeIcon.disabled = true;
|
||||||
|
|
||||||
|
modal.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgress(processed, total, status) {
|
||||||
|
const progressBar = document.getElementById("progressBar");
|
||||||
|
const progressPercentage = document.getElementById("progressPercentage");
|
||||||
|
const statusText = document.getElementById("progressStatusText");
|
||||||
|
const closeButton = document.getElementById("progressModalCloseBtn");
|
||||||
|
const closeIcon = document.getElementById("closeProgressModalBtn");
|
||||||
|
|
||||||
|
const percentage = total > 0 ? Math.round((processed / total) * 100) : 0;
|
||||||
|
progressBar.style.width = `${percentage}%`;
|
||||||
|
progressPercentage.textContent = `${percentage}%`;
|
||||||
|
statusText.textContent = status;
|
||||||
|
|
||||||
|
if (processed === total) {
|
||||||
|
closeButton.disabled = false;
|
||||||
|
closeIcon.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addProgressLog(message, isError = false) {
|
||||||
|
const progressLog = document.getElementById("progressLog");
|
||||||
|
const logEntry = document.createElement("div");
|
||||||
|
logEntry.textContent = message;
|
||||||
|
logEntry.className = isError
|
||||||
|
? "text-danger-600"
|
||||||
|
: "text-gray-700";
|
||||||
|
progressLog.appendChild(logEntry);
|
||||||
|
progressLog.scrollTop = progressLog.scrollHeight; // Auto-scroll to bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeProgressModal(reload = false) {
|
||||||
|
const modal = document.getElementById("progressModal");
|
||||||
|
modal.classList.add("hidden");
|
||||||
|
if (reload) {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function initializeKeySelectionListeners() {
|
function initializeKeySelectionListeners() {
|
||||||
const setupEventListenersForList = (listId, keyType) => {
|
const setupEventListenersForList = (listId, keyType) => {
|
||||||
const listElement = document.getElementById(listId);
|
const listElement = document.getElementById(listId);
|
||||||
@@ -1309,6 +1357,25 @@ function registerServiceWorker() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化下拉菜单
|
||||||
|
function initializeDropdownMenu() {
|
||||||
|
// 阻止下拉菜单按钮的点击事件冒泡
|
||||||
|
const dropdownButton = document.getElementById('dropdownMenuButton');
|
||||||
|
if (dropdownButton) {
|
||||||
|
dropdownButton.addEventListener('click', (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 阻止下拉菜单内部点击事件冒泡
|
||||||
|
const dropdownMenu = document.getElementById('dropdownMenu');
|
||||||
|
if (dropdownMenu) {
|
||||||
|
dropdownMenu.addEventListener('click', (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
initializePageAnimationsAndEffects();
|
initializePageAnimationsAndEffects();
|
||||||
@@ -1319,6 +1386,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
initializeAutoRefreshControls();
|
initializeAutoRefreshControls();
|
||||||
initializeKeyPaginationAndSearch(); // This will also handle initial display
|
initializeKeyPaginationAndSearch(); // This will also handle initial display
|
||||||
registerServiceWorker();
|
registerServiceWorker();
|
||||||
|
initializeDropdownMenu(); // 初始化下拉菜单
|
||||||
|
|
||||||
// Initial batch actions update might be needed if not covered by displayPage
|
// Initial batch actions update might be needed if not covered by displayPage
|
||||||
// updateBatchActions('valid');
|
// updateBatchActions('valid');
|
||||||
@@ -1897,3 +1965,179 @@ function setupPaginationControls(type, currentPage, totalPages) {
|
|||||||
function filterAndSearchValidKeys() {
|
function filterAndSearchValidKeys() {
|
||||||
fetchAndDisplayKeys('valid', 1);
|
fetchAndDisplayKeys('valid', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 下拉菜单功能 ---
|
||||||
|
|
||||||
|
// 切换下拉菜单显示/隐藏
|
||||||
|
window.toggleDropdownMenu = function() {
|
||||||
|
const dropdownMenu = document.getElementById('dropdownMenu');
|
||||||
|
const isVisible = dropdownMenu.classList.contains('show');
|
||||||
|
|
||||||
|
if (isVisible) {
|
||||||
|
hideDropdownMenu();
|
||||||
|
} else {
|
||||||
|
showDropdownMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示下拉菜单
|
||||||
|
function showDropdownMenu() {
|
||||||
|
const dropdownMenu = document.getElementById('dropdownMenu');
|
||||||
|
dropdownMenu.classList.add('show');
|
||||||
|
|
||||||
|
// 点击其他地方时隐藏菜单
|
||||||
|
document.addEventListener('click', handleOutsideClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏下拉菜单
|
||||||
|
function hideDropdownMenu() {
|
||||||
|
const dropdownMenu = document.getElementById('dropdownMenu');
|
||||||
|
dropdownMenu.classList.remove('show');
|
||||||
|
|
||||||
|
// 移除事件监听器
|
||||||
|
document.removeEventListener('click', handleOutsideClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理点击菜单外部区域
|
||||||
|
function handleOutsideClick(event) {
|
||||||
|
const dropdownToggle = document.querySelector('.dropdown-toggle');
|
||||||
|
if (!dropdownToggle.contains(event.target)) {
|
||||||
|
hideDropdownMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制全部密钥
|
||||||
|
async function copyAllKeys() {
|
||||||
|
hideDropdownMenu();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取所有密钥(有效和无效)
|
||||||
|
const response = await fetchAPI('/api/keys/all');
|
||||||
|
|
||||||
|
const allKeys = [...response.valid_keys, ...response.invalid_keys];
|
||||||
|
|
||||||
|
if (allKeys.length === 0) {
|
||||||
|
showNotification("没有找到任何密钥", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keysText = allKeys.join('\n');
|
||||||
|
await copyToClipboard(keysText);
|
||||||
|
showNotification(`已成功复制 ${allKeys.length} 个密钥到剪贴板`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('复制全部密钥失败:', error);
|
||||||
|
showNotification(`复制失败: ${error.message}`, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证所有密钥
|
||||||
|
window.verifyAllKeys = async function() {
|
||||||
|
hideDropdownMenu();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取所有密钥(有效和无效)
|
||||||
|
const response = await fetchAPI('/api/keys/all');
|
||||||
|
|
||||||
|
const allKeys = [...response.valid_keys, ...response.invalid_keys];
|
||||||
|
|
||||||
|
if (allKeys.length === 0) {
|
||||||
|
showNotification("没有找到任何密钥可验证", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用验证模态框显示确认对话框
|
||||||
|
showVerifyModalForAllKeys(allKeys);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取所有密钥失败:', error);
|
||||||
|
showNotification(`获取密钥失败: ${error.message}`, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示验证所有密钥的模态框
|
||||||
|
function showVerifyModalForAllKeys(allKeys) {
|
||||||
|
const modalElement = document.getElementById("verifyModal");
|
||||||
|
const titleElement = document.getElementById("verifyModalTitle");
|
||||||
|
const messageElement = document.getElementById("verifyModalMessage");
|
||||||
|
const confirmButton = document.getElementById("confirmVerifyBtn");
|
||||||
|
|
||||||
|
titleElement.textContent = "批量验证所有密钥";
|
||||||
|
messageElement.textContent = `确定要验证所有 ${allKeys.length} 个密钥吗?此操作可能需要较长时间。`;
|
||||||
|
confirmButton.disabled = false;
|
||||||
|
|
||||||
|
// 设置确认按钮事件
|
||||||
|
confirmButton.onclick = () => executeVerifyAllKeys(allKeys);
|
||||||
|
|
||||||
|
// 显示模态框
|
||||||
|
modalElement.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行验证所有密钥
|
||||||
|
async function executeVerifyAllKeys(allKeys) {
|
||||||
|
closeVerifyModal();
|
||||||
|
|
||||||
|
// 获取批次大小
|
||||||
|
const batchSizeInput = document.getElementById("batchSize");
|
||||||
|
const batchSize = parseInt(batchSizeInput.value, 10) || 10;
|
||||||
|
|
||||||
|
// 开始批量验证
|
||||||
|
showProgressModal(`批量验证所有 ${allKeys.length} 个密钥`);
|
||||||
|
|
||||||
|
let allSuccessfulKeys = [];
|
||||||
|
let allFailedKeys = {};
|
||||||
|
let processedCount = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < allKeys.length; i += batchSize) {
|
||||||
|
const batch = allKeys.slice(i, i + batchSize);
|
||||||
|
const progressText = `正在验证批次 ${Math.floor(i / batchSize) + 1} / ${Math.ceil(allKeys.length / batchSize)} (密钥 ${i + 1}-${Math.min(i + batchSize, allKeys.length)})`;
|
||||||
|
|
||||||
|
updateProgress(i, allKeys.length, progressText);
|
||||||
|
addProgressLog(`处理批次: ${batch.length}个密钥...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ keys: batch }),
|
||||||
|
};
|
||||||
|
const data = await fetchAPI(`/gemini/v1beta/verify-selected-keys`, options);
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
if (data.successful_keys && data.successful_keys.length > 0) {
|
||||||
|
allSuccessfulKeys = allSuccessfulKeys.concat(data.successful_keys);
|
||||||
|
addProgressLog(`✅ 批次成功: ${data.successful_keys.length} 个`);
|
||||||
|
}
|
||||||
|
if (data.failed_keys && Object.keys(data.failed_keys).length > 0) {
|
||||||
|
Object.assign(allFailedKeys, data.failed_keys);
|
||||||
|
addProgressLog(`❌ 批次失败: ${Object.keys(data.failed_keys).length} 个`, true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addProgressLog(`- 批次返回空数据`, true);
|
||||||
|
}
|
||||||
|
} catch (apiError) {
|
||||||
|
addProgressLog(`❌ 批次请求失败: ${apiError.message}`, true);
|
||||||
|
// 将此批次的所有密钥标记为失败
|
||||||
|
batch.forEach(key => {
|
||||||
|
allFailedKeys[key] = apiError.message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
processedCount += batch.length;
|
||||||
|
updateProgress(processedCount, allKeys.length, progressText);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(
|
||||||
|
allKeys.length,
|
||||||
|
allKeys.length,
|
||||||
|
`所有批次验证完成!`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 关闭进度模态框并显示最终结果
|
||||||
|
closeProgressModal(false);
|
||||||
|
showVerificationResultModal({
|
||||||
|
successful_keys: allSuccessfulKeys,
|
||||||
|
failed_keys: allFailedKeys,
|
||||||
|
valid_count: allSuccessfulKeys.length,
|
||||||
|
invalid_count: Object.keys(allFailedKeys).length
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1026,6 +1026,69 @@ endblock %} {% block head_extra_styles %}
|
|||||||
color: #fca5b3 !important;
|
color: #fca5b3 !important;
|
||||||
}
|
}
|
||||||
/* End of API Call Details Modal Specific Styling Adjustments */
|
/* End of API Call Details Modal Specific Styling Adjustments */
|
||||||
|
|
||||||
|
/* 下拉菜单样式 */
|
||||||
|
.dropdown-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
background-color: rgba(255, 255, 255, 0.98);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
min-width: 200px;
|
||||||
|
z-index: 1000;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu.show {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
color: #374151;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background-color: rgba(59, 130, 246, 0.1);
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:first-child {
|
||||||
|
border-top-left-radius: 0.5rem;
|
||||||
|
border-top-right-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:last-child {
|
||||||
|
border-bottom-left-radius: 0.5rem;
|
||||||
|
border-bottom-right-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item i {
|
||||||
|
width: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-toggle {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %} {% block head_extra_scripts %}
|
{% endblock %} {% block head_extra_scripts %}
|
||||||
<!-- keys_status.js needs to be loaded in head because it might be used by inline scripts -->
|
<!-- keys_status.js needs to be loaded in head because it might be used by inline scripts -->
|
||||||
@@ -1061,6 +1124,28 @@ endblock %} {% block head_extra_styles %}
|
|||||||
>
|
>
|
||||||
<i class="fas fa-sync-alt"></i>
|
<i class="fas fa-sync-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- 下拉菜单按钮 -->
|
||||||
|
<div class="dropdown-toggle relative">
|
||||||
|
<button
|
||||||
|
id="dropdownMenuButton"
|
||||||
|
class="bg-white bg-opacity-20 hover:bg-opacity-30 rounded-full w-8 h-8 flex items-center justify-center text-primary-600 transition-all duration-300"
|
||||||
|
onclick="toggleDropdownMenu()"
|
||||||
|
title="更多操作"
|
||||||
|
>
|
||||||
|
<i class="fas fa-ellipsis-v"></i>
|
||||||
|
</button>
|
||||||
|
<!-- 下拉菜单 -->
|
||||||
|
<div id="dropdownMenu" class="dropdown-menu">
|
||||||
|
<button class="dropdown-item" onclick="copyAllKeys()">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
<span>复制全部密钥</span>
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" onclick="verifyAllKeys()">
|
||||||
|
<i class="fas fa-check-double"></i>
|
||||||
|
<span>验证所有密钥</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1
|
<h1
|
||||||
@@ -1520,7 +1605,11 @@ endblock %} {% block head_extra_styles %}
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<p style="color: #374151" id="verifyModalMessage"></p>
|
<p style="color: #374151" id="verifyModalMessage" class="mb-4"></p>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<label for="batchSize" class="text-sm font-medium" style="color: #374151;">每批次验证数量:</label>
|
||||||
|
<input type="number" id="batchSize" value="10" min="1" class="form-input h-8 w-20 px-2 py-1 text-sm border rounded focus:ring-primary-500 focus:border-primary-500">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end gap-3">
|
<div class="flex justify-end gap-3">
|
||||||
<button
|
<button
|
||||||
@@ -1644,6 +1733,78 @@ endblock %} {% block head_extra_styles %}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 批量操作进度模态框 -->
|
||||||
|
<div
|
||||||
|
id="progressModal"
|
||||||
|
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-white rounded-lg p-6 shadow-xl max-w-2xl w-full animate-fade-in"
|
||||||
|
style="
|
||||||
|
background-color: rgba(255, 255, 255, 0.98);
|
||||||
|
color: #374151;
|
||||||
|
border-color: rgba(0, 0, 0, 0.08);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3
|
||||||
|
class="text-lg font-semibold text-gray-800"
|
||||||
|
id="progressModalTitle"
|
||||||
|
style="color: #1f2937; font-weight: 600"
|
||||||
|
>
|
||||||
|
批量操作进度
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onclick="closeProgressModal()"
|
||||||
|
id="closeProgressModalBtn"
|
||||||
|
class="text-gray-500 hover:text-gray-700 focus:outline-none"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<p id="progressStatusText" class="text-sm text-gray-600 mb-2">
|
||||||
|
准备开始...
|
||||||
|
</p>
|
||||||
|
<div class="w-full bg-gray-200 rounded-full h-4 dark:bg-gray-700">
|
||||||
|
<div
|
||||||
|
id="progressBar"
|
||||||
|
class="bg-primary-600 h-4 rounded-full transition-all duration-300"
|
||||||
|
style="width: 0%"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
id="progressPercentage"
|
||||||
|
class="text-center text-sm font-semibold mt-1"
|
||||||
|
style="color: #1f2937"
|
||||||
|
>
|
||||||
|
0%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="progressLog"
|
||||||
|
class="text-xs max-h-60 overflow-y-auto bg-gray-50 p-3 rounded border border-gray-200 space-y-1 font-mono"
|
||||||
|
style="
|
||||||
|
background-color: rgba(249, 250, 251, 0.95);
|
||||||
|
border-color: rgba(0, 0, 0, 0.08);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- Log entries will be added here -->
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-3 mt-6">
|
||||||
|
<button
|
||||||
|
id="progressModalCloseBtn"
|
||||||
|
onclick="closeProgressModal(true)"
|
||||||
|
class="px-4 py-1.5 text-sm font-medium bg-primary-700 hover:bg-primary-800 text-white rounded-lg transition-colors"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
完成并刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 操作结果模态框 -->
|
<!-- 操作结果模态框 -->
|
||||||
<div
|
<div
|
||||||
id="resultModal"
|
id="resultModal"
|
||||||
|
|||||||
Reference in New Issue
Block a user