Files
gemini-banlancer/frontend/js/pages/keys_status.js
2025-11-20 12:24:05 +08:00

2190 lines
82 KiB
JavaScript
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.
// 统计数据可视化交互效果
function copyToClipboard(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text);
} else {
return new Promise((resolve, reject) => {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand("copy");
document.body.removeChild(textArea);
if (successful) {
resolve();
} else {
reject(new Error("复制失败"));
}
} catch (err) {
document.body.removeChild(textArea);
reject(err);
}
});
}
}
// 添加统计项动画效果
function initStatItemAnimations() {
const statItems = document.querySelectorAll(".stat-item");
statItems.forEach((item) => {
item.addEventListener("mouseenter", () => {
item.style.transform = "scale(1.05)";
const icon = item.querySelector(".stat-icon");
if (icon) {
icon.style.opacity = "0.2";
icon.style.transform = "scale(1.1) rotate(0deg)";
}
});
item.addEventListener("mouseleave", () => {
item.style.transform = "";
const icon = item.querySelector(".stat-icon");
if (icon) {
icon.style.opacity = "";
icon.style.transform = "";
}
});
});
}
// 获取指定类型区域内选中的密钥
function getSelectedKeys(type) {
const checkboxes = document.querySelectorAll(
`#${type}Keys .key-checkbox:checked`
);
return Array.from(checkboxes).map((cb) => cb.value);
}
// 更新指定类型区域的批量操作按钮状态和计数
function updateBatchActions(type) {
const selectedKeys = getSelectedKeys(type);
const count = selectedKeys.length;
const batchActionsDiv = document.getElementById(`${type}BatchActions`);
const selectedCountSpan = document.getElementById(`${type}SelectedCount`);
const buttons = batchActionsDiv.querySelectorAll("button");
if (count > 0) {
batchActionsDiv.classList.remove("hidden");
selectedCountSpan.textContent = count;
buttons.forEach((button) => (button.disabled = false));
} else {
batchActionsDiv.classList.add("hidden");
selectedCountSpan.textContent = "0";
buttons.forEach((button) => (button.disabled = true));
}
// 更新全选复选框状态
const selectAllCheckbox = document.getElementById(
`selectAll${type.charAt(0).toUpperCase() + type.slice(1)}`
);
const allCheckboxes = document.querySelectorAll(`#${type}Keys .key-checkbox`);
// 只有在有可见的 key 时才考虑全选状态
const visibleCheckboxes = document.querySelectorAll(
`#${type}Keys li:not([style*="display: none"]) .key-checkbox`
);
if (selectAllCheckbox && visibleCheckboxes.length > 0) {
selectAllCheckbox.checked = count === visibleCheckboxes.length;
selectAllCheckbox.indeterminate =
count > 0 && count < visibleCheckboxes.length;
} else if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
}
}
// 全选/取消全选指定类型的密钥
function toggleSelectAll(type, isChecked) {
const listElement = document.getElementById(`${type}Keys`);
// Select checkboxes within LI elements that are NOT styled with display:none
// This targets currently visible items based on filtering.
const visibleCheckboxes = listElement.querySelectorAll(
`li:not([style*="display: none"]) .key-checkbox`
);
visibleCheckboxes.forEach((checkbox) => {
checkbox.checked = isChecked;
const listItem = checkbox.closest("li[data-key]"); // Get the LI from the current DOM
if (listItem) {
listItem.classList.toggle("selected", isChecked);
// Sync with master array
const key = listItem.dataset.key;
const masterList = type === "valid" ? allValidKeys : allInvalidKeys;
if (masterList) {
// Ensure masterList is defined
const masterListItem = masterList.find((li) => li.dataset.key === key);
if (masterListItem) {
const masterCheckbox = masterListItem.querySelector(".key-checkbox");
if (masterCheckbox) {
masterCheckbox.checked = isChecked;
}
}
}
}
});
updateBatchActions(type);
}
// 复制选中的密钥
function copySelectedKeys(type) {
const selectedKeys = getSelectedKeys(type);
if (selectedKeys.length === 0) {
showNotification("没有选中的密钥可复制", "warning");
return;
}
const keysText = selectedKeys.join("\n");
copyToClipboard(keysText)
.then(() => {
showNotification(
`已成功复制 ${selectedKeys.length} 个选中的${
type === "valid" ? "有效" : "无效"
}密钥`
);
})
.catch((err) => {
console.error("无法复制文本: ", err);
showNotification("复制失败,请重试", "error");
});
}
// 单个复制保持不变
function copyKey(key) {
copyToClipboard(key)
.then(() => {
showNotification(`已成功复制密钥`);
})
.catch((err) => {
console.error("无法复制文本: ", err);
showNotification("复制失败,请重试", "error");
});
}
// showCopyStatus 函数已废弃。
async function verifyKey(key, button) {
try {
// 禁用按钮并显示加载状态
button.disabled = true;
const originalHtml = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 验证中';
try {
const data = await fetchAPI(`/gemini/v1beta/verify-key/${key}`, {
method: "POST",
});
// 根据验证结果更新UI并显示模态提示框
if (data && (data.success || data.status === "valid")) {
// 验证成功,显示成功结果
button.style.backgroundColor = "#27ae60";
// 使用结果模态框显示成功消息
showResultModal(true, "密钥验证成功");
// 模态框关闭时会自动刷新页面
} else {
// 验证失败,显示失败结果
const errorMsg = data.error || "密钥无效";
button.style.backgroundColor = "#e74c3c";
// 使用结果模态框显示失败消息改为true以在关闭时刷新
showResultModal(false, "密钥验证失败: " + errorMsg, true);
}
} catch (apiError) {
console.error("密钥验证 API 请求失败:", apiError);
showResultModal(false, `验证请求失败: ${apiError.message}`, true);
} finally {
// 1秒后恢复按钮原始状态 (如果页面不刷新)
// 由于现在成功和失败都会刷新,这部分逻辑可以简化或移除
// 但为了防止未来修改刷新逻辑,暂时保留,但可能不会执行
setTimeout(() => {
if (
!document.getElementById("resultModal") ||
document.getElementById("resultModal").classList.contains("hidden")
) {
button.innerHTML = originalHtml;
button.disabled = false;
button.style.backgroundColor = "";
}
}, 1000);
}
} catch (error) {
console.error("验证失败:", error);
// 确保在捕获到错误时恢复按钮状态 (如果页面不刷新)
// button.disabled = false; // 由 finally 处理或因刷新而无需处理
// 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 data = await fetchAPI(`/gemini/v1beta/reset-fail-count/${key}`, {
method: "POST",
});
// 根据重置结果更新UI
if (data.success) {
showNotification("失败计数重置成功");
// 成功时保留绿色背景一会儿
button.style.backgroundColor = "#27ae60";
// 稍后刷新页面
setTimeout(() => location.reload(), 1000);
} else {
const errorMsg = data.message || "重置失败";
showNotification("重置失败: " + errorMsg, "error");
// 失败时保留红色背景一会儿
button.style.backgroundColor = "#e74c3c";
// 如果失败1秒后恢复按钮
setTimeout(() => {
button.innerHTML = originalHtml;
button.disabled = false;
button.style.backgroundColor = "";
}, 1000);
}
// 恢复按钮状态逻辑已移至成功/失败分支内
} catch (apiError) {
console.error("重置失败:", apiError);
showNotification(`重置请求失败: ${apiError.message}`, "error");
// 确保在捕获到错误时恢复按钮状态
button.disabled = false;
button.innerHTML = '<i class="fas fa-redo-alt"></i> 重置'; // 恢复原始图标和文本
button.style.backgroundColor = ""; // 清除可能设置的背景色
}
}
// 显示重置确认模态框 (基于选中的密钥)
function showResetModal(type) {
const modalElement = document.getElementById("resetModal");
const titleElement = document.getElementById("resetModalTitle");
const messageElement = document.getElementById("resetModalMessage");
const confirmButton = document.getElementById("confirmResetBtn");
const selectedKeys = getSelectedKeys(type);
const count = selectedKeys.length;
// 设置标题和消息
titleElement.textContent = "批量重置失败次数";
if (count > 0) {
messageElement.textContent = `确定要批量重置选中的 ${count}${
type === "valid" ? "有效" : "无效"
}密钥的失败次数吗?`;
confirmButton.disabled = false; // 确保按钮可用
} else {
// 这个情况理论上不会发生,因为按钮在未选中时是禁用的
messageElement.textContent = `请先选择要重置的${
type === "valid" ? "有效" : "无效"
}密钥。`;
confirmButton.disabled = true;
}
// 设置确认按钮事件
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-6xl mb-3 text-success-500"; // 稍微增大图标
} else {
iconElement.innerHTML =
'<i class="fas fa-times-circle text-danger-500"></i>';
iconElement.className = "text-6xl mb-3 text-danger-500"; // 稍微增大图标
}
// 清空现有内容并设置新消息
messageElement.innerHTML = ""; // 清空
if (typeof message === "string") {
// 对于普通字符串消息,保持原有逻辑
const messageDiv = document.createElement("div");
messageDiv.innerText = message; // 使用 innerText 防止 XSS
messageElement.appendChild(messageDiv);
} else if (message instanceof Node) {
// 如果传入的是 DOM 节点,直接添加
messageElement.appendChild(message);
} else {
// 其他类型转为字符串
const messageDiv = document.createElement("div");
messageDiv.innerText = String(message);
messageElement.appendChild(messageDiv);
}
// 设置确认按钮点击事件
confirmButton.onclick = () => closeResultModal(autoReload);
// 显示模态框
modalElement.classList.remove("hidden");
}
// 显示批量验证结果的专用模态框
function showVerificationResultModal(data) {
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");
const successfulKeys = data.successful_keys || [];
const failedKeys = data.failed_keys || {};
const validCount = data.valid_count || 0;
const invalidCount = data.invalid_count || 0;
// 设置标题和图标
titleElement.textContent = "批量验证结果";
if (invalidCount === 0 && validCount > 0) {
iconElement.innerHTML =
'<i class="fas fa-check-double text-success-500"></i>';
iconElement.className = "text-6xl mb-3 text-success-500";
} else if (invalidCount > 0 && validCount > 0) {
iconElement.innerHTML =
'<i class="fas fa-exclamation-triangle text-warning-500"></i>';
iconElement.className = "text-6xl mb-3 text-warning-500";
} else if (invalidCount > 0 && validCount === 0) {
iconElement.innerHTML =
'<i class="fas fa-times-circle text-danger-500"></i>';
iconElement.className = "text-6xl mb-3 text-danger-500";
} else {
// 都为 0 或其他情况
iconElement.innerHTML = '<i class="fas fa-info-circle text-gray-500"></i>';
iconElement.className = "text-6xl mb-3 text-gray-500";
}
// 构建详细内容
messageElement.innerHTML = ""; // 清空
const summaryDiv = document.createElement("div");
summaryDiv.className = "text-center mb-4 text-lg";
summaryDiv.innerHTML = `验证完成:<span class="font-semibold text-success-600">${validCount}</span> 个成功,<span class="font-semibold text-danger-600">${invalidCount}</span> 个失败。`;
messageElement.appendChild(summaryDiv);
// 成功列表
if (successfulKeys.length > 0) {
const successDiv = document.createElement("div");
successDiv.className = "mb-3";
const successHeader = document.createElement("div");
successHeader.className = "flex justify-between items-center mb-1";
successHeader.innerHTML = `<h4 class="font-semibold text-success-700">成功密钥 (${successfulKeys.length}):</h4>`;
const copySuccessBtn = document.createElement("button");
copySuccessBtn.className =
"px-2 py-0.5 bg-green-100 hover:bg-green-200 text-green-700 text-xs rounded transition-colors";
copySuccessBtn.innerHTML = '<i class="fas fa-copy mr-1"></i>复制全部';
copySuccessBtn.onclick = (e) => {
e.stopPropagation();
copyToClipboard(successfulKeys.join("\n"))
.then(() =>
showNotification(
`已复制 ${successfulKeys.length} 个成功密钥`,
"success"
)
)
.catch(() => showNotification("复制失败", "error"));
};
successHeader.appendChild(copySuccessBtn);
successDiv.appendChild(successHeader);
const successList = document.createElement("ul");
successList.className =
"list-disc list-inside text-sm text-gray-600 max-h-20 overflow-y-auto bg-gray-50 p-2 rounded border border-gray-200";
successfulKeys.forEach((key) => {
const li = document.createElement("li");
li.className = "font-mono";
// Store full key in dataset for potential future use, display masked
li.dataset.fullKey = key;
li.textContent =
key.substring(0, 4) + "..." + key.substring(key.length - 4);
successList.appendChild(li);
});
successDiv.appendChild(successList);
messageElement.appendChild(successDiv);
}
// 失败列表 - 按错误码分组展示
if (Object.keys(failedKeys).length > 0) {
const failDiv = document.createElement("div");
failDiv.className = "mb-1"; // 减少底部边距
const failHeader = document.createElement("div");
failHeader.className = "flex justify-between items-center mb-1";
failHeader.innerHTML = `<h4 class="font-semibold text-danger-700">失败密钥 (${
Object.keys(failedKeys).length
}):</h4>`;
const copyFailBtn = document.createElement("button");
copyFailBtn.className =
"px-2 py-0.5 bg-red-100 hover:bg-red-200 text-red-700 text-xs rounded transition-colors";
copyFailBtn.innerHTML = '<i class="fas fa-copy mr-1"></i>复制全部';
const failedKeysArray = Object.keys(failedKeys); // Get array of failed keys
copyFailBtn.onclick = (e) => {
e.stopPropagation();
copyToClipboard(failedKeysArray.join("\n"))
.then(() =>
showNotification(
`已复制 ${failedKeysArray.length} 个失败密钥`,
"success"
)
)
.catch(() => showNotification("复制失败", "error"));
};
failHeader.appendChild(copyFailBtn);
failDiv.appendChild(failHeader);
// 按错误码分组失败的密钥
const errorGroups = {};
Object.entries(failedKeys).forEach(([key, error]) => {
// 提取错误码或使用完整错误信息作为分组键
let errorCode = error;
// 尝试提取常见的错误码模式
const errorCodePatterns = [
/status code (\d+)/,
];
for (const pattern of errorCodePatterns) {
const match = error.match(pattern);
if (match) {
errorCode = match[1] || match[0];
break;
}
}
// 如果没有匹配到特定模式使用500
if (errorCode === error) {
errorCode = 500;
}
if (!errorGroups[errorCode]) {
errorGroups[errorCode] = [];
}
errorGroups[errorCode].push({ key, error });
});
// 创建分组展示容器
const groupsContainer = document.createElement("div");
groupsContainer.className = "space-y-3 max-h-64 overflow-y-auto bg-red-50 p-2 rounded border border-red-200";
// 按错误码分组展示
Object.entries(errorGroups).forEach(([errorCode, keyErrorPairs]) => {
const groupDiv = document.createElement("div");
groupDiv.className = "border border-red-300 rounded-lg bg-white p-2";
// 错误码标题
const groupHeader = document.createElement("div");
groupHeader.className = "flex justify-between items-center mb-2 cursor-pointer";
groupHeader.innerHTML = `
<div class="flex items-center gap-2">
<i class="fas fa-chevron-down group-toggle-icon text-red-600 transition-transform duration-200"></i>
<h5 class="font-semibold text-red-700 text-sm">错误码: ${errorCode}</h5>
<span class="bg-red-100 text-red-600 px-2 py-0.5 rounded-full text-xs font-medium">${keyErrorPairs.length} 个密钥</span>
</div>
<button class="px-2 py-0.5 bg-red-200 hover:bg-red-300 text-red-700 text-xs rounded transition-colors group-copy-btn">
<i class="fas fa-copy mr-1"></i>复制组内密钥
</button>
`;
// 复制组内密钥功能
const groupCopyBtn = groupHeader.querySelector('.group-copy-btn');
groupCopyBtn.onclick = (e) => {
e.stopPropagation();
const groupKeys = keyErrorPairs.map(pair => pair.key);
copyToClipboard(groupKeys.join("\n"))
.then(() =>
showNotification(
`已复制 ${groupKeys.length} 个密钥 (错误码: ${errorCode})`,
"success"
)
)
.catch(() => showNotification("复制失败", "error"));
};
// 密钥列表容器
const keysList = document.createElement("div");
keysList.className = "group-keys-list space-y-1";
keyErrorPairs.forEach(({ key, error }) => {
const keyItem = document.createElement("div");
keyItem.className = "flex flex-col items-start bg-gray-50 p-2 rounded border";
const keySpanContainer = document.createElement("div");
keySpanContainer.className = "flex justify-between items-center w-full";
const keySpan = document.createElement("span");
keySpan.className = "font-mono text-sm";
keySpan.dataset.fullKey = key;
keySpan.textContent = key.substring(0, 4) + "..." + key.substring(key.length - 4);
const detailsButton = document.createElement("button");
detailsButton.className = "ml-2 px-2 py-0.5 bg-red-200 hover:bg-red-300 text-red-700 text-xs rounded transition-colors";
detailsButton.innerHTML = '<i class="fas fa-info-circle mr-1"></i>详情';
detailsButton.dataset.error = error;
detailsButton.onclick = (e) => {
e.stopPropagation();
const button = e.currentTarget;
const keyItem = button.closest(".bg-gray-50");
const errorMsg = button.dataset.error;
const errorDetailsId = `error-details-${key.replace(/[^a-zA-Z0-9]/g, "")}`;
let errorDiv = keyItem.querySelector(`#${errorDetailsId}`);
if (errorDiv) {
errorDiv.remove();
button.innerHTML = '<i class="fas fa-info-circle mr-1"></i>详情';
} else {
errorDiv = document.createElement("div");
errorDiv.id = errorDetailsId;
errorDiv.className = "w-full mt-2 text-xs text-red-600 bg-red-50 p-2 rounded border border-red-100 whitespace-pre-wrap break-words";
errorDiv.textContent = errorMsg;
keyItem.appendChild(errorDiv);
button.innerHTML = '<i class="fas fa-chevron-up mr-1"></i>收起';
}
};
keySpanContainer.appendChild(keySpan);
keySpanContainer.appendChild(detailsButton);
keyItem.appendChild(keySpanContainer);
keysList.appendChild(keyItem);
});
// 分组折叠/展开功能
groupHeader.onclick = (e) => {
if (e.target.closest('.group-copy-btn')) return; // 避免复制按钮触发折叠
const toggleIcon = groupHeader.querySelector('.group-toggle-icon');
const isCollapsed = keysList.style.display === 'none';
if (isCollapsed) {
keysList.style.display = 'block';
toggleIcon.style.transform = 'rotate(0deg)';
} else {
keysList.style.display = 'none';
toggleIcon.style.transform = 'rotate(-90deg)';
}
};
groupDiv.appendChild(groupHeader);
groupDiv.appendChild(keysList);
groupsContainer.appendChild(groupDiv);
});
failDiv.appendChild(groupsContainer);
messageElement.appendChild(failDiv);
}
// 设置确认按钮点击事件 - 总是自动刷新
confirmButton.onclick = () => closeResultModal(true); // Always reload
// 显示模态框
modalElement.classList.remove("hidden");
}
async function executeResetAll(type) {
closeResetModal();
const keysToReset = getSelectedKeys(type);
if (keysToReset.length === 0) {
showNotification("没有选中的密钥可重置", "warning");
return;
}
showProgressModal(`批量重置 ${keysToReset.length} 个密钥的失败计数`);
let successCount = 0;
let failCount = 0;
for (let i = 0; i < keysToReset.length; i++) {
const key = keysToReset[i];
const keyDisplay = `${key.substring(0, 4)}...${key.substring(
key.length - 4
)}`;
updateProgress(i, keysToReset.length, `正在重置: ${keyDisplay}`);
try {
const data = await fetchAPI(`/gemini/v1beta/reset-fail-count/${key}`, {
method: "POST",
});
if (data.success) {
successCount++;
addProgressLog(`${keyDisplay}: 重置成功`);
} else {
failCount++;
addProgressLog(
`${keyDisplay}: 重置失败 - ${data.message || "未知错误"}`,
true
);
}
} catch (apiError) {
failCount++;
addProgressLog(`${keyDisplay}: 请求失败 - ${apiError.message}`, true);
}
}
updateProgress(
keysToReset.length,
keysToReset.length,
`重置完成!成功: ${successCount}, 失败: ${failCount}`
);
}
function scrollToTop() {
window.scrollTo({ top: 0, behavior: "smooth" });
}
function scrollToBottom() {
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
}
// 移除这个函数,因为它可能正在干扰按钮的显示
// HTML中已经设置了滚动按钮为flex显示不需要JavaScript额外控制
// function updateScrollButtons() {
// // 不执行任何操作
// }
function refreshPage(button) {
button.classList.add("loading"); // Maybe add a loading class for visual feedback
button.disabled = true;
const icon = button.querySelector("i");
if (icon) icon.classList.add("fa-spin"); // Add spin animation
setTimeout(() => {
window.location.reload();
// No need to remove loading/spin as page reloads
}, 300);
}
// 展开/收起区块内容的函数,带有平滑动画效果。
// @param {HTMLElement} header - 被点击的区块头部元素。
// @param {string} sectionId - (当前未使用,但可用于更精确的目标定位) 关联内容区块的ID。
function toggleSection(header, sectionId) {
const toggleIcon = header.querySelector(".toggle-icon");
// 内容元素是卡片内的 .key-content div
const card = header.closest(".stats-card");
const content = card ? card.querySelector(".key-content") : null;
// 批量操作栏和分页控件也可能影响内容区域的动画高度计算
const batchActions = card ? card.querySelector('[id$="BatchActions"]') : null;
const pagination = card
? card.querySelector('[id$="PaginationControls"]')
: null;
if (!toggleIcon || !content) {
console.error(
"Toggle section failed: Icon or content element not found. Header:",
header,
"SectionId:",
sectionId
);
return;
}
const isCollapsed = content.classList.contains("collapsed");
toggleIcon.classList.toggle("collapsed", !isCollapsed); // 更新箭头图标方向
if (isCollapsed) {
// --- 准备展开动画 ---
content.classList.remove("collapsed"); // 移除 collapsed 类以应用展开的样式
// 步骤 1: 重置内联样式让CSS控制初始的"隐藏"状态 (通常是 maxHeight: 0, opacity: 0)。
// 同时,确保 overflow 在动画开始前是 hidden。
content.style.maxHeight = ""; // 清除可能存在的内联 maxHeight
content.style.opacity = ""; // 清除可能存在的内联 opacity
content.style.paddingTop = ""; // 清除内联 padding
content.style.paddingBottom = "";
content.style.overflow = "hidden"; // 动画过程中隐藏溢出内容
// 步骤 2: 使用 requestAnimationFrame (rAF) 确保浏览器在计算 scrollHeight 之前
// 已经应用了上一步的样式重置特别是如果CSS中有过渡效果
requestAnimationFrame(() => {
// 步骤 3: 计算内容区的目标高度。
// 这包括内容本身的 scrollHeight以及任何可见的批量操作栏和分页控件的高度。
let targetHeight = content.scrollHeight;
if (batchActions && !batchActions.classList.contains("hidden")) {
targetHeight += batchActions.offsetHeight;
}
if (pagination && pagination.offsetHeight > 0) {
// 尝试获取分页控件的 margin-top以获得更精确的高度
const paginationStyle = getComputedStyle(pagination);
const paginationMarginTop = parseFloat(paginationStyle.marginTop) || 0;
targetHeight += pagination.offsetHeight + paginationMarginTop;
}
// 步骤 4: 设置 maxHeight 和 opacity 以触发CSS过渡到展开状态。
content.style.maxHeight = targetHeight + "px";
content.style.opacity = "1";
// 假设展开后的 padding 为 1rem (p-4 in Tailwind). 根据实际情况调整。
content.style.paddingTop = "1rem";
content.style.paddingBottom = "1rem";
// 步骤 5: 监听 transitionend 事件。动画结束后,移除 maxHeight 以允许内容动态调整,
// 并将 overflow 设置为 visible以防内容变化后被裁剪。
content.addEventListener(
"transitionend",
function onExpansionEnd() {
content.removeEventListener("transitionend", onExpansionEnd); // 清理监听器
// 再次检查确保是在展开状态 (避免在快速连续点击时出错)
if (!content.classList.contains("collapsed")) {
content.style.maxHeight = ""; // 允许内容自适应高度
content.style.overflow = "visible"; // 允许内容溢出(如果需要)
}
},
{ once: true }
); // 确保监听器只执行一次
});
} else {
// --- 准备收起动画 ---
// 步骤 1: 获取当前内容区的可见高度。
// 这对于从当前渲染高度平滑过渡到0是必要的。
let currentVisibleHeight = content.scrollHeight; // scrollHeight 应该已经是包括padding的内部高度
if (batchActions && !batchActions.classList.contains("hidden")) {
currentVisibleHeight += batchActions.offsetHeight;
}
if (pagination && pagination.offsetHeight > 0) {
const paginationStyle = getComputedStyle(pagination);
const paginationMarginTop = parseFloat(paginationStyle.marginTop) || 0;
currentVisibleHeight += pagination.offsetHeight + paginationMarginTop;
}
// 步骤 2: 将 maxHeight 设置为当前计算的可见高度,以确保过渡从当前高度开始。
// 同时,确保 overflow 在动画开始前是 hidden。
content.style.maxHeight = currentVisibleHeight + "px";
content.style.overflow = "hidden";
// 步骤 3: 使用 requestAnimationFrame (rAF) 确保浏览器应用了上述 maxHeight。
requestAnimationFrame(() => {
// 步骤 4: 过渡到目标状态 (收起): maxHeight 和 padding 设为0opacity 设为0。
content.style.maxHeight = "0px";
content.style.opacity = "0";
content.style.paddingTop = "0";
content.style.paddingBottom = "0";
// 在动画开始(或即将开始)后添加 collapsed 类以便CSS可以应用最终的折叠样式。
content.classList.add("collapsed");
});
}
}
// filterValidKeys 函数已被 filterAndSearchValidKeys 替代,此函数保留为空或可移除
function filterValidKeys() {
// This function is now handled by filterAndSearchValidKeys
// Kept for now to avoid breaking any potential legacy calls, but should be removed later.
filterAndSearchValidKeys();
}
// --- Initialization Helper Functions ---
function initializePageAnimationsAndEffects() {
initStatItemAnimations(); // Already an external function
const animateCounters = () => {
const statValues = document.querySelectorAll(".stat-value");
statValues.forEach((valueElement) => {
const finalValue = parseInt(valueElement.textContent, 10);
if (!isNaN(finalValue)) {
if (!valueElement.dataset.originalValue) {
valueElement.dataset.originalValue = valueElement.textContent;
}
let startValue = 0;
const duration = 1500;
const startTime = performance.now();
const updateCounter = (currentTime) => {
const elapsedTime = currentTime - startTime;
if (elapsedTime < duration) {
const progress = elapsedTime / duration;
const easeOutValue = 1 - Math.pow(1 - progress, 3);
const currentValue = Math.floor(easeOutValue * finalValue);
valueElement.textContent = currentValue;
requestAnimationFrame(updateCounter);
} else {
valueElement.textContent = valueElement.dataset.originalValue;
}
};
requestAnimationFrame(updateCounter);
}
});
};
setTimeout(animateCounters, 300);
document.querySelectorAll(".stats-card").forEach((card) => {
card.addEventListener("mouseenter", () => {
card.classList.add("shadow-lg");
card.style.transform = "translateY(-2px)";
});
card.addEventListener("mouseleave", () => {
card.classList.remove("shadow-lg");
card.style.transform = "";
});
});
}
function initializeSectionToggleListeners() {
document.querySelectorAll(".stats-card-header").forEach((header) => {
if (header.querySelector(".toggle-icon")) {
header.addEventListener("click", (event) => {
if (event.target.closest("input, label, button, select")) {
return;
}
const card = header.closest(".stats-card");
const content = card ? card.querySelector(".key-content") : null;
const sectionId = content ? content.id : null;
if (sectionId) {
toggleSection(header, sectionId);
} else {
console.warn("Could not determine sectionId for toggle.");
}
});
}
});
}
function initializeKeyFilterControls() {
const thresholdInput = document.getElementById("failCountThreshold");
if (thresholdInput) {
thresholdInput.addEventListener("input", filterValidKeys);
}
// 为无效密钥添加筛选控件监听器
const invalidThresholdInput = document.getElementById("invalidFailCountThreshold");
if (invalidThresholdInput) {
invalidThresholdInput.addEventListener("input", () => fetchAndDisplayKeys('invalid', 1));
}
}
function initializeGlobalBatchVerificationHandlers() {
window.showVerifyModal = function (type, event) {
if (event) {
event.stopPropagation();
}
const modalElement = document.getElementById("verifyModal");
const titleElement = document.getElementById("verifyModalTitle");
const messageElement = document.getElementById("verifyModalMessage");
const confirmButton = document.getElementById("confirmVerifyBtn");
const selectedKeys = getSelectedKeys(type);
const count = selectedKeys.length;
titleElement.textContent = "批量验证密钥";
if (count > 0) {
messageElement.textContent = `确定要批量验证选中的 ${count}${
type === "valid" ? "有效" : "无效"
}密钥吗?此操作可能需要一些时间。`;
confirmButton.disabled = false;
} else {
messageElement.textContent = `请先选择要验证的${
type === "valid" ? "有效" : "无效"
}密钥。`;
confirmButton.disabled = true;
}
confirmButton.onclick = () => executeVerifyAll(type);
modalElement.classList.remove("hidden");
};
window.closeVerifyModal = function () {
document.getElementById("verifyModal").classList.add("hidden");
};
// executeVerifyAll 变为 initializeGlobalBatchVerificationHandlers 的局部函数
async function executeVerifyAll(type) {
closeVerifyModal();
const keysToVerify = getSelectedKeys(type);
if (keysToVerify.length === 0) {
showNotification("没有选中的密钥可验证", "warning");
return;
}
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)
// 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() {
const setupEventListenersForList = (listId, keyType) => {
const listElement = document.getElementById(listId);
if (!listElement) return;
// Event delegation for clicks on list items to toggle checkbox
listElement.addEventListener("click", (event) => {
const listItem = event.target.closest("li[data-key]");
if (!listItem) return;
// Do not toggle if a button, a link, or any element explicitly designed for interaction within the li was clicked
if (
event.target.closest(
"button, a, input[type='button'], input[type='submit']"
)
) {
let currentTarget = event.target;
let isInteractiveElementClick = false;
while (currentTarget && currentTarget !== listItem) {
if (
currentTarget.tagName === "BUTTON" ||
currentTarget.tagName === "A" ||
(currentTarget.tagName === "INPUT" &&
["button", "submit"].includes(currentTarget.type))
) {
isInteractiveElementClick = true;
break;
}
currentTarget = currentTarget.parentElement;
}
if (isInteractiveElementClick) return;
}
const checkbox = listItem.querySelector(".key-checkbox");
if (checkbox) {
checkbox.checked = !checkbox.checked;
checkbox.dispatchEvent(new Event("change", { bubbles: true }));
}
});
// Event delegation for 'change' event on checkboxes within the list
listElement.addEventListener("change", (event) => {
if (event.target.classList.contains("key-checkbox")) {
const checkbox = event.target; // This is the checkbox in the DOM
const listItem = checkbox.closest("li[data-key]"); // This is the LI in the DOM
if (listItem) {
listItem.classList.toggle("selected", checkbox.checked);
// Sync with master array
const key = listItem.dataset.key;
const masterList =
keyType === "valid" ? allValidKeys : allInvalidKeys;
if (masterList) {
// Ensure masterList is defined
const masterListItem = masterList.find(
(li) => li.dataset.key === key
);
if (masterListItem) {
const masterCheckbox =
masterListItem.querySelector(".key-checkbox");
if (masterCheckbox) {
masterCheckbox.checked = checkbox.checked;
}
}
}
}
updateBatchActions(keyType);
}
});
};
setupEventListenersForList("validKeys", "valid");
setupEventListenersForList("invalidKeys", "invalid");
}
function initializeAutoRefreshControls() {
const autoRefreshToggle = document.getElementById("autoRefreshToggle");
const autoRefreshIntervalTime = 60000; // 60秒
let autoRefreshTimer = null;
function startAutoRefresh() {
if (autoRefreshTimer) return;
console.log("启动自动刷新...");
showNotification("自动刷新已启动", "info", 2000);
autoRefreshTimer = setInterval(() => {
console.log("自动刷新 keys_status 页面...");
location.reload();
}, autoRefreshIntervalTime);
}
function stopAutoRefresh() {
if (autoRefreshTimer) {
console.log("停止自动刷新...");
showNotification("自动刷新已停止", "info", 2000);
clearInterval(autoRefreshTimer);
autoRefreshTimer = null;
}
}
if (autoRefreshToggle) {
const isAutoRefreshEnabled =
localStorage.getItem("autoRefreshEnabled") === "true";
autoRefreshToggle.checked = isAutoRefreshEnabled;
if (isAutoRefreshEnabled) {
startAutoRefresh();
}
autoRefreshToggle.addEventListener("change", () => {
if (autoRefreshToggle.checked) {
localStorage.setItem("autoRefreshEnabled", "true");
startAutoRefresh();
} else {
localStorage.setItem("autoRefreshEnabled", "false");
stopAutoRefresh();
}
});
}
}
// Debounce function
function debounce(func, delay) {
let timeout;
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), delay);
};
}
// --- Key List Display & Pagination ---
/**
* Fetches and displays keys.
* @param {string} type 'valid' or 'invalid'
* @param {number} page Page number (1-based)
*/
async function fetchAndDisplayKeys(type, page = 1) {
const listElement = document.getElementById(`${type}Keys`);
const paginationControls = document.getElementById(`${type}PaginationControls`);
if (!listElement || !paginationControls) return;
// Show loading indicator
listElement.innerHTML = `<li><div class="text-center py-4 col-span-full"><i class="fas fa-spinner fa-spin"></i> Loading...</div></li>`;
// 根据类型选择对应的控件
const itemsPerPageSelect = document.getElementById(type === 'valid' ? "itemsPerPageSelect" : "invalidItemsPerPageSelect");
const limit = itemsPerPageSelect ? parseInt(itemsPerPageSelect.value, 10) : 10;
const searchInput = document.getElementById(type === 'valid' ? "keySearchInput" : "invalidKeySearchInput");
const searchTerm = searchInput ? searchInput.value : '';
const thresholdInput = document.getElementById(type === 'valid' ? "failCountThreshold" : "invalidFailCountThreshold");
const failCountThreshold = thresholdInput ? (thresholdInput.value === '' ? null : parseInt(thresholdInput.value, 10)) : null;
try {
const params = new URLSearchParams({
page: page,
limit: limit,
status: type,
});
if (searchTerm) {
params.append('search', searchTerm);
}
if (failCountThreshold !== null) {
params.append('fail_count_threshold', failCountThreshold);
}
const data = await fetchAPI(`/api/keys?${params.toString()}`);
listElement.innerHTML = ""; // Clear loading indicator
const keys = data.keys || {};
if (Object.keys(keys).length > 0) {
Object.entries(keys).forEach(([key, fail_count]) => {
const listItem = createKeyListItem(key, fail_count, type);
listElement.appendChild(listItem);
});
} else {
listElement.innerHTML = `<li><div class="text-center py-4 col-span-full">No keys found.</div></li>`;
}
setupPaginationControls(type, data.current_page, data.total_pages);
updateBatchActions(type);
} catch (error) {
console.error(`Error fetching ${type} keys:`, error);
listElement.innerHTML = `<li><div class="text-center py-4 text-red-500 col-span-full">Error loading keys.</div></li>`;
}
}
/**
* Creates a single key list item element.
* @param {string} key The API key.
* @param {number} fail_count The failure count for the key.
* @param {string} type 'valid' or 'invalid'.
* @returns {HTMLElement} The created list item element.
*/
function createKeyListItem(key, fail_count, type) {
const li = document.createElement("li");
li.className = `bg-white rounded-lg p-3 shadow-sm hover:shadow-md transition-all duration-300 border ${type === 'valid' ? 'hover:border-success-300' : 'hover:border-danger-300'} transform hover:-translate-y-1`;
li.dataset.key = key;
li.dataset.failCount = fail_count;
const statusBadge = type === 'valid'
? `<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-success-50 text-success-600"><i class="fas fa-check mr-1"></i> 有效</span>`
: `<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-danger-50 text-danger-600"><i class="fas fa-times mr-1"></i> 无效</span>`;
li.innerHTML = `
<input type="checkbox" class="form-checkbox h-5 w-5 text-primary-600 border-gray-300 rounded focus:ring-primary-500 mt-1 key-checkbox" data-key-type="${type}" value="${key}">
<div class="flex-grow">
<div class="flex flex-col justify-between h-full gap-3">
<div class="flex flex-wrap items-center gap-2">
${statusBadge}
<div class="flex items-center gap-1">
<span class="key-text font-mono" data-full-key="${key}">${key.substring(0, 4)}...${key.substring(key.length - 4)}</span>
<button class="text-gray-500 hover:text-primary-600 transition-colors" onclick="toggleKeyVisibility(this)" title="Show/Hide Key">
<i class="fas fa-eye"></i>
</button>
</div>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-50 text-amber-600">
<i class="fas fa-exclamation-triangle mr-1"></i>
失败: ${fail_count}
</span>
</div>
<div class="flex flex-wrap items-center gap-2">
<button class="flex items-center gap-1 bg-success-600 hover:bg-success-700 text-white px-2.5 py-1 rounded-lg text-xs font-medium transition-all duration-200" onclick="verifyKey('${key}', this)"><i class="fas fa-check-circle"></i> 验证</button>
<button class="flex items-center gap-1 bg-gray-500 hover:bg-gray-600 text-white px-2.5 py-1 rounded-lg text-xs font-medium transition-all duration-200" onclick="resetKeyFailCount('${key}', this)"><i class="fas fa-redo-alt"></i> 重置</button>
<button class="flex items-center gap-1 bg-blue-500 hover:bg-blue-600 text-white px-2.5 py-1 rounded-lg text-xs font-medium transition-all duration-200" onclick="copyKey('${key}')"><i class="fas fa-copy"></i> 复制</button>
<button class="flex items-center gap-1 bg-blue-600 hover:bg-blue-700 text-white px-2.5 py-1 rounded-lg text-xs font-medium transition-all duration-200" onclick="showKeyUsageDetails('${key}')"><i class="fas fa-chart-pie"></i> 详情</button>
<button class="flex items-center gap-1 bg-red-800 hover:bg-red-900 text-white px-2.5 py-1 rounded-lg text-xs font-medium transition-all duration-200" onclick="showSingleKeyDeleteConfirmModal('${key}', this)"><i class="fas fa-trash-alt"></i> 删除</button>
</div>
</div>
</div>
`;
return li;
}
/**
* Sets up pagination controls.
* @param {string} type 'valid' or 'invalid'
* @param {number} currentPage Current page number
* @param {number} totalPages Total number of pages
*/
function setupPaginationControls(type, currentPage, totalPages) {
const controlsContainer = document.getElementById(`${type}PaginationControls`);
if (!controlsContainer) return;
controlsContainer.innerHTML = "";
if (totalPages <= 1) return;
// Previous Button
const prevButton = document.createElement("button");
prevButton.innerHTML = '<i class="fas fa-chevron-left"></i>';
prevButton.className = `pagination-button px-3 py-1 rounded text-sm transition-colors duration-150 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed`;
prevButton.disabled = currentPage === 1;
prevButton.onclick = () => fetchAndDisplayKeys(type, currentPage - 1);
controlsContainer.appendChild(prevButton);
// Page Number Buttons
for (let i = 1; i <= totalPages; i++) {
// Simple pagination for now, can be improved with ellipsis for many pages
const pageButton = document.createElement("button");
pageButton.textContent = i;
pageButton.className = `pagination-button px-3 py-1 rounded text-sm transition-colors duration-150 ease-in-out ${i === currentPage ? 'active font-semibold' : ''}`;
pageButton.onclick = () => fetchAndDisplayKeys(type, i);
controlsContainer.appendChild(pageButton);
}
// Next Button
const nextButton = document.createElement("button");
nextButton.innerHTML = '<i class="fas fa-chevron-right"></i>';
nextButton.className = `pagination-button px-3 py-1 rounded text-sm transition-colors duration-150 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed`;
nextButton.disabled = currentPage === totalPages;
nextButton.onclick = () => fetchAndDisplayKeys(type, currentPage + 1);
controlsContainer.appendChild(nextButton);
}
let allValidKeys = [];
let allInvalidKeys = [];
let filteredValidKeys = [];
let itemsPerPage = 10; // Default
let validCurrentPage = 1; // Also used by displayPage
let invalidCurrentPage = 1; // Also used by displayPage
function initializeKeyPaginationAndSearch() {
const debouncedFetchValidKeys = debounce(() => fetchAndDisplayKeys('valid', 1), 300);
const debouncedFetchInvalidKeys = debounce(() => fetchAndDisplayKeys('invalid', 1), 300);
// 有效密钥的搜索和筛选控件
const searchInput = document.getElementById("keySearchInput");
if (searchInput) {
searchInput.addEventListener("input", debouncedFetchValidKeys);
}
const thresholdInput = document.getElementById("failCountThreshold");
if (thresholdInput) {
thresholdInput.addEventListener("input", debouncedFetchValidKeys);
}
const itemsPerPageSelect = document.getElementById("itemsPerPageSelect");
if (itemsPerPageSelect) {
itemsPerPageSelect.addEventListener("change", () => {
fetchAndDisplayKeys('valid', 1);
});
}
// 无效密钥的搜索和筛选控件
const invalidSearchInput = document.getElementById("invalidKeySearchInput");
if (invalidSearchInput) {
invalidSearchInput.addEventListener("input", debouncedFetchInvalidKeys);
}
const invalidThresholdInput = document.getElementById("invalidFailCountThreshold");
if (invalidThresholdInput) {
invalidThresholdInput.addEventListener("input", debouncedFetchInvalidKeys);
}
const invalidItemsPerPageSelect = document.getElementById("invalidItemsPerPageSelect");
if (invalidItemsPerPageSelect) {
invalidItemsPerPageSelect.addEventListener("change", () => {
fetchAndDisplayKeys('invalid', 1);
});
}
// Initial fetch
fetchAndDisplayKeys('valid');
fetchAndDisplayKeys('invalid');
}
function registerServiceWorker() {
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker
.register("/static/service-worker.js")
.then((registration) => {
console.log("ServiceWorker注册成功:", registration.scope);
})
.catch((error) => {
console.log("ServiceWorker注册失败:", error);
});
});
}
}
// 初始化下拉菜单
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", () => {
initializePageAnimationsAndEffects();
initializeSectionToggleListeners();
initializeKeyFilterControls();
initializeGlobalBatchVerificationHandlers();
initializeKeySelectionListeners();
initializeAutoRefreshControls();
initializeKeyPaginationAndSearch(); // This will also handle initial display
registerServiceWorker();
initializeDropdownMenu(); // 初始化下拉菜单
// Initial batch actions update might be needed if not covered by displayPage
// updateBatchActions('valid');
// updateBatchActions('invalid');
});
// --- 新增:删除密钥相关功能 ---
// 新版:显示单个密钥删除确认模态框
function showSingleKeyDeleteConfirmModal(key, button) {
const modalElement = document.getElementById("singleKeyDeleteConfirmModal");
const titleElement = document.getElementById(
"singleKeyDeleteConfirmModalTitle"
);
const messageElement = document.getElementById(
"singleKeyDeleteConfirmModalMessage"
);
const confirmButton = document.getElementById("confirmSingleKeyDeleteBtn");
const keyDisplay =
key.substring(0, 4) + "..." + key.substring(key.length - 4);
titleElement.textContent = "确认删除密钥";
messageElement.innerHTML = `确定要删除密钥 <span class="font-mono text-amber-300 font-semibold">${keyDisplay}</span> 吗?<br>此操作无法撤销。`;
// 移除旧的监听器并重新附加,以确保 key 和 button 参数是最新的
const newConfirmButton = confirmButton.cloneNode(true);
confirmButton.parentNode.replaceChild(newConfirmButton, confirmButton);
newConfirmButton.onclick = () => executeSingleKeyDelete(key, button);
modalElement.classList.remove("hidden");
}
// 新版:关闭单个密钥删除确认模态框
function closeSingleKeyDeleteConfirmModal() {
document
.getElementById("singleKeyDeleteConfirmModal")
.classList.add("hidden");
}
// 新版:执行单个密钥删除
async function executeSingleKeyDelete(key, button) {
closeSingleKeyDeleteConfirmModal();
button.disabled = true;
const originalHtml = button.innerHTML;
// 使用字体图标,确保一致性
button.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i>删除中';
try {
const response = await fetchAPI(`/api/config/keys/${key}`, {
method: "DELETE",
});
if (response.success) {
// 使用 resultModal 并确保刷新
showResultModal(true, response.message || "密钥删除成功", true);
} else {
// 使用 resultModal失败时不刷新以便用户看到错误信息
showResultModal(false, response.message || "密钥删除失败", false);
button.innerHTML = originalHtml;
button.disabled = false;
}
} catch (error) {
console.error("删除密钥 API 请求失败:", error);
showResultModal(false, `删除密钥请求失败: ${error.message}`, false);
button.innerHTML = originalHtml;
button.disabled = false;
}
}
// 显示批量删除确认模态框
function showDeleteConfirmationModal(type, event) {
if (event) {
event.stopPropagation();
}
const modalElement = document.getElementById("deleteConfirmModal");
const titleElement = document.getElementById("deleteConfirmModalTitle");
const messageElement = document.getElementById("deleteConfirmModalMessage");
const confirmButton = document.getElementById("confirmDeleteBtn");
const selectedKeys = getSelectedKeys(type);
const count = selectedKeys.length;
titleElement.textContent = "确认批量删除";
if (count > 0) {
messageElement.textContent = `确定要批量删除选中的 ${count}${
type === "valid" ? "有效" : "无效"
}密钥吗?此操作无法撤销。`;
confirmButton.disabled = false;
} else {
// 此情况理论上不应发生,因为批量删除按钮在未选中时是禁用的
messageElement.textContent = `请先选择要删除的${
type === "valid" ? "有效" : "无效"
}密钥。`;
confirmButton.disabled = true;
}
confirmButton.onclick = () => executeDeleteSelectedKeys(type);
modalElement.classList.remove("hidden");
}
// 关闭批量删除确认模态框
function closeDeleteConfirmationModal() {
document.getElementById("deleteConfirmModal").classList.add("hidden");
}
// 执行批量删除
async function executeDeleteSelectedKeys(type) {
closeDeleteConfirmationModal();
const selectedKeys = getSelectedKeys(type);
if (selectedKeys.length === 0) {
showNotification("没有选中的密钥可删除", "warning");
return;
}
// 找到批量删除按钮并显示加载状态 (假设它在对应类型的 batchActions 中是最后一个按钮)
const batchActionsDiv = document.getElementById(`${type}BatchActions`);
const deleteButton = batchActionsDiv
? batchActionsDiv.querySelector("button.bg-red-600")
: null;
let originalDeleteBtnHtml = "";
if (deleteButton) {
originalDeleteBtnHtml = deleteButton.innerHTML;
deleteButton.disabled = true;
deleteButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 删除中';
}
try {
const response = await fetchAPI("/api/config/keys/delete-selected", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ keys: selectedKeys }),
});
if (response.success) {
// 使用 resultModal 显示更详细的结果
const message =
response.message ||
`成功删除 ${response.deleted_count || selectedKeys.length} 个密钥。`;
showResultModal(true, message, true); // true 表示成功messagetrue 表示关闭后刷新
} else {
showResultModal(false, response.message || "批量删除密钥失败", true); // false 表示失败messagetrue 表示关闭后刷新
}
} catch (error) {
console.error("批量删除 API 请求失败:", error);
showResultModal(false, `批量删除请求失败: ${error.message}`, true);
} finally {
// resultModal 关闭时会刷新页面,所以通常不需要在这里恢复按钮状态。
// 如果不刷新,则需要恢复按钮状态:
// if (deleteButton && (!document.getElementById("resultModal") || document.getElementById("resultModal").classList.contains("hidden") || document.getElementById("resultModalTitle").textContent.includes("失败"))) {
// deleteButton.innerHTML = originalDeleteBtnHtml;
// // 按钮的 disabled 状态会在 updateBatchActions 中处理,或者因页面刷新而重置
// }
}
}
// --- 结束:删除密钥相关功能 ---
function toggleKeyVisibility(button) {
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);
if (keyTextSpan.textContent === maskedKey) {
keyTextSpan.textContent = fullKey;
eyeIcon.classList.remove("fa-eye");
eyeIcon.classList.add("fa-eye-slash");
button.title = "隐藏密钥";
} else {
keyTextSpan.textContent = maskedKey;
eyeIcon.classList.remove("fa-eye-slash");
eyeIcon.classList.add("fa-eye");
button.title = "显示密钥";
}
}
// --- API 调用详情模态框逻辑 ---
// 显示 API 调用详情模态框
async function showApiCallDetails(
period,
totalCalls,
successCalls,
failureCalls
) {
const modal = document.getElementById("apiCallDetailsModal");
const contentArea = document.getElementById("apiCallDetailsContent");
const titleElement = document.getElementById("apiCallDetailsModalTitle");
if (!modal || !contentArea || !titleElement) {
console.error("无法找到 API 调用详情模态框元素");
showNotification("无法显示详情,页面元素缺失", "error");
return;
}
// 设置标题
let periodText = "";
switch (period) {
case "1m":
periodText = "最近 1 分钟";
break;
case "1h":
periodText = "最近 1 小时";
break;
case "24h":
periodText = "最近 24 小时";
break;
default:
periodText = "指定时间段";
}
titleElement.textContent = `${periodText} API 调用详情`;
// 显示模态框并设置加载状态
modal.classList.remove("hidden");
contentArea.innerHTML = `
<div class="text-center py-10">
<i class="fas fa-spinner fa-spin text-primary-600 text-3xl"></i>
<p class="text-gray-500 mt-2">加载中...</p>
</div>`;
try {
const data = await fetchAPI(`/api/stats/details?period=${period}`);
if (data) {
renderApiCallDetails(
data,
contentArea,
totalCalls,
successCalls,
failureCalls
);
} else {
renderApiCallDetails(
[],
contentArea,
totalCalls,
successCalls,
failureCalls
); // Show empty state if no data
}
} catch (apiError) {
console.error("获取 API 调用详情失败:", apiError);
contentArea.innerHTML = `
<div class="text-center py-10 text-danger-500">
<i class="fas fa-exclamation-triangle text-3xl"></i>
<p class="mt-2">加载失败: ${apiError.message}</p>
</div>`;
}
}
// 关闭 API 调用详情模态框
function closeApiCallDetailsModal() {
const modal = document.getElementById("apiCallDetailsModal");
if (modal) {
modal.classList.add("hidden");
}
}
// 渲染 API 调用详情到模态框
function renderApiCallDetails(
data,
container,
totalCalls,
successCalls,
failureCalls
) {
let summaryHtml = "";
// 只有在提供了这些统计数据时才显示概览
if (
totalCalls !== undefined &&
successCalls !== undefined &&
failureCalls !== undefined
) {
summaryHtml = `
<div class="mb-4 p-3 bg-white dark:bg-gray-700 rounded-lg">
<h4 class="font-semibold text-gray-700 dark:text-gray-200 mb-2 text-md border-b pb-1.5 dark:border-gray-600">期间调用概览:</h4>
<div class="grid grid-cols-3 gap-2 text-center">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">总计</p>
<p class="text-lg font-bold text-primary-600 dark:text-primary-400">${totalCalls}</p>
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">成功</p>
<p class="text-lg font-bold text-success-600 dark:text-success-400">${successCalls}</p>
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">失败</p>
<p class="text-lg font-bold text-danger-600 dark:text-danger-400">${failureCalls}</p>
</div>
</div>
</div>
`;
}
if (!data || data.length === 0) {
container.innerHTML =
summaryHtml +
`
<div class="text-center py-10 text-gray-500 dark:text-gray-400">
<i class="fas fa-info-circle text-3xl"></i>
<p class="mt-2">该时间段内没有 API 调用记录。</p>
</div>`;
return;
}
// 创建表格
let tableHtml = `
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-700/50">
<tr>
<th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">时间</th>
<th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">密钥 (部分)</th>
<th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">模型</th>
<th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">状态</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
`;
// 填充表格行
data.forEach((call) => {
const timestamp = new Date(call.timestamp).toLocaleString();
const keyDisplay = call.key
? `${call.key.substring(0, 4)}...${call.key.substring(
call.key.length - 4
)}`
: "N/A";
const statusClass =
call.status === "success"
? "text-success-600 dark:text-success-400"
: "text-danger-600 dark:text-danger-400";
const statusIcon =
call.status === "success" ? "fa-check-circle" : "fa-times-circle";
tableHtml += `
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">${timestamp}</td>
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 font-mono">${keyDisplay}</td>
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">${
call.model || "N/A"
}</td>
<td class="px-4 py-2 whitespace-nowrap text-sm ${statusClass}">
<i class="fas ${statusIcon} mr-1"></i>
${call.status}
</td>
</tr>
`;
});
tableHtml += `
</tbody>
</table>
`;
container.innerHTML = summaryHtml + tableHtml; // Prepend summary
}
// --- 密钥使用详情模态框逻辑 ---
// 显示密钥使用详情模态框
window.showKeyUsageDetails = async function (key) {
const modal = document.getElementById("keyUsageDetailsModal");
const contentArea = document.getElementById("keyUsageDetailsContent");
const titleElement = document.getElementById("keyUsageDetailsModalTitle");
const keyDisplay =
key.substring(0, 4) + "..." + key.substring(key.length - 4);
if (!modal || !contentArea || !titleElement) {
console.error("无法找到密钥使用详情模态框元素");
showNotification("无法显示详情,页面元素缺失", "error");
return;
}
// renderKeyUsageDetails 变为 showKeyUsageDetails 的局部函数
function renderKeyUsageDetails(data, container) {
if (!data || Object.keys(data).length === 0) {
container.innerHTML = `
<div class="text-center py-10 text-gray-500">
<i class="fas fa-info-circle text-3xl"></i>
<p class="mt-2">该密钥在最近24小时内没有调用记录。</p>
</div>`;
return;
}
let tableHtml = `
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">模型名称</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">调用次数 (24h)</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">`;
const sortedModels = Object.entries(data).sort(
([, countA], [, countB]) => countB - countA
);
sortedModels.forEach(([model, count]) => {
tableHtml += `
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${model}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${count}</td>
</tr>`;
});
tableHtml += `
</tbody>
</table>`;
container.innerHTML = tableHtml;
}
// 设置标题
titleElement.textContent = `密钥 ${keyDisplay} - 最近24小时请求详情`;
// 显示模态框并设置加载状态
modal.classList.remove("hidden");
contentArea.innerHTML = `
<div class="text-center py-10">
<i class="fas fa-spinner fa-spin text-primary-600 text-3xl"></i>
<p class="text-gray-500 mt-2">加载中...</p>
</div>`;
try {
const data = await fetchAPI(`/api/key-usage-details/${key}`);
if (data) {
renderKeyUsageDetails(data, contentArea);
} else {
renderKeyUsageDetails({}, contentArea); // Show empty state if no data
}
} catch (apiError) {
console.error("获取密钥使用详情失败:", apiError);
contentArea.innerHTML = `
<div class="text-center py-10 text-danger-500">
<i class="fas fa-exclamation-triangle text-3xl"></i>
<p class="mt-2">加载失败: ${apiError.message}</p>
</div>`;
}
};
// 关闭密钥使用详情模态框
window.closeKeyUsageDetailsModal = function () {
const modal = document.getElementById("keyUsageDetailsModal");
if (modal) {
modal.classList.add("hidden");
}
};
// window.renderKeyUsageDetails 函数已被移入 showKeyUsageDetails 内部, 此处残留代码已删除。
// --- Key List Display & Pagination ---
/**
* Displays key list items for a specific type and page.
* @param {string} type 'valid' or 'invalid'
* @param {number} page Page number (1-based)
* @param {Array} keyItemsArray The array of li elements to paginate (e.g., filteredValidKeys, allInvalidKeys)
*/
function displayPage(type, page, keyItemsArray) {
const listElement = document.getElementById(`${type}Keys`);
const paginationControls = document.getElementById(
`${type}PaginationControls`
);
if (!listElement || !paginationControls) return;
// This function is now mostly handled by fetchAndDisplayKeys.
// We can simplify this or remove it if all display logic is in fetchAndDisplayKeys.
// For now, let's keep it for rendering the pagination controls as a separate step.
setupPaginationControls(type, page, totalPages);
updateBatchActions(type); // Update batch actions based on the currently displayed page
}
/**
* Sets up pagination controls.
* @param {string} type 'valid' or 'invalid'
* @param {number} currentPage Current page number
* @param {number} totalPages Total number of pages
* @param {Array} keyItemsArray The array of li elements being paginated
*/
function setupPaginationControls(type, currentPage, totalPages) {
const controlsContainer = document.getElementById(
`${type}PaginationControls`
);
if (!controlsContainer) return;
controlsContainer.innerHTML = "";
if (totalPages <= 1) {
return; // No controls needed for single/no page
}
// Base classes for all buttons (Tailwind for layout, custom for consistent styling)
const baseButtonClasses =
"pagination-button px-3 py-1 rounded text-sm transition-colors duration-150 ease-in-out";
// Define hover classes that work with the custom background by adjusting opacity or a border effect.
// Since .pagination-button defines a background, a hover effect might be a subtle border change or brightness.
// For simplicity, we can rely on CSS for hover effects on .pagination-button:hover
// const hoverClasses = "hover:border-purple-400"; // Example if you want JS to add specific hover behavior
// Previous Button
const prevButton = document.createElement("button");
prevButton.innerHTML = '<i class="fas fa-chevron-left"></i>';
prevButton.className = `${baseButtonClasses} disabled:opacity-50 disabled:cursor-not-allowed`;
prevButton.disabled = currentPage === 1;
prevButton.onclick = () => fetchAndDisplayKeys(type, currentPage - 1);
controlsContainer.appendChild(prevButton);
// Page Number Buttons (Logic for ellipsis)
const maxPageButtons = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxPageButtons / 2));
let endPage = Math.min(totalPages, startPage + maxPageButtons - 1);
if (endPage - startPage + 1 < maxPageButtons) {
startPage = Math.max(1, endPage - maxPageButtons + 1);
}
// First Page Button & Ellipsis
if (startPage > 1) {
const firstPageButton = document.createElement("button");
firstPageButton.textContent = "1";
firstPageButton.className = `${baseButtonClasses}`;
firstPageButton.onclick = () => fetchAndDisplayKeys(type, 1);
controlsContainer.appendChild(firstPageButton);
if (startPage > 2) {
const ellipsis = document.createElement("span");
ellipsis.textContent = "...";
ellipsis.className = "px-3 py-1 text-gray-300 text-sm"; // Adjusted color for dark theme
controlsContainer.appendChild(ellipsis);
}
}
// Middle Page Buttons
for (let i = startPage; i <= endPage; i++) {
const pageButton = document.createElement("button");
pageButton.textContent = i;
pageButton.className = `${baseButtonClasses} ${
i === currentPage
? "active font-semibold" // Relies on .pagination-button.active CSS for styling
: "" // Non-active buttons just use .pagination-button style
}`;
pageButton.onclick = () => fetchAndDisplayKeys(type, i);
controlsContainer.appendChild(pageButton);
}
// Ellipsis & Last Page Button
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
const ellipsis = document.createElement("span");
ellipsis.textContent = "...";
ellipsis.className = "px-3 py-1 text-gray-300 text-sm"; // Adjusted color
controlsContainer.appendChild(ellipsis);
}
const lastPageButton = document.createElement("button");
lastPageButton.textContent = totalPages;
lastPageButton.className = `${baseButtonClasses}`;
lastPageButton.onclick = () => fetchAndDisplayKeys(type, totalPages);
controlsContainer.appendChild(lastPageButton);
}
// Next Button
const nextButton = document.createElement("button");
nextButton.innerHTML = '<i class="fas fa-chevron-right"></i>';
nextButton.className = `${baseButtonClasses} disabled:opacity-50 disabled:cursor-not-allowed`;
nextButton.disabled = currentPage === totalPages;
nextButton.onclick = () => fetchAndDisplayKeys(type, currentPage + 1);
controlsContainer.appendChild(nextButton);
}
// --- Filtering & Searching (Valid Keys Only) ---
/**
* Filters and searches the valid keys based on threshold and search term.
* Updates the `filteredValidKeys` array and redisplays the first page.
*/
function filterAndSearchValidKeys() {
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
});
}