// 统计数据可视化交互效果 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 = ' 验证中'; 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 = ' 验证'; showResultModal(false, "验证处理失败: " + error.message, true); // 改为true以在关闭时刷新 } } async function resetKeyFailCount(key, button) { try { // 禁用按钮并显示加载状态 button.disabled = true; const originalHtml = button.innerHTML; button.innerHTML = ' 重置中'; 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 = ' 重置'; // 恢复原始图标和文本 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 = ''; iconElement.className = "text-6xl mb-3 text-success-500"; // 稍微增大图标 } else { iconElement.innerHTML = ''; 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 = ''; iconElement.className = "text-6xl mb-3 text-success-500"; } else if (invalidCount > 0 && validCount > 0) { iconElement.innerHTML = ''; iconElement.className = "text-6xl mb-3 text-warning-500"; } else if (invalidCount > 0 && validCount === 0) { iconElement.innerHTML = ''; iconElement.className = "text-6xl mb-3 text-danger-500"; } else { // 都为 0 或其他情况 iconElement.innerHTML = ''; 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 = `验证完成:${validCount} 个成功,${invalidCount} 个失败。`; 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 = `

成功密钥 (${successfulKeys.length}):

`; 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 = '复制全部'; 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 = `

失败密钥 (${ Object.keys(failedKeys).length }):

`; 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 = '复制全部'; 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 = `
错误码: ${errorCode}
${keyErrorPairs.length} 个密钥
`; // 复制组内密钥功能 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 = '详情'; 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 = '详情'; } 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 = '收起'; } }; 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 设为0,opacity 设为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 = `
  • Loading...
  • `; // 根据类型选择对应的控件 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 = `
  • No keys found.
  • `; } setupPaginationControls(type, data.current_page, data.total_pages); updateBatchActions(type); } catch (error) { console.error(`Error fetching ${type} keys:`, error); listElement.innerHTML = `
  • Error loading keys.
  • `; } } /** * 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' ? ` 有效` : ` 无效`; li.innerHTML = `
    ${statusBadge}
    ${key.substring(0, 4)}...${key.substring(key.length - 4)}
    失败: ${fail_count}
    `; 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 = ''; 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 = ''; 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 = `确定要删除密钥 ${keyDisplay} 吗?
    此操作无法撤销。`; // 移除旧的监听器并重新附加,以确保 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 = '删除中'; 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 = ' 删除中'; } 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 表示成功,message,true 表示关闭后刷新 } else { showResultModal(false, response.message || "批量删除密钥失败", true); // false 表示失败,message,true 表示关闭后刷新 } } 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 = `

    加载中...

    `; 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 = `

    加载失败: ${apiError.message}

    `; } } // 关闭 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 = `

    期间调用概览:

    总计

    ${totalCalls}

    成功

    ${successCalls}

    失败

    ${failureCalls}

    `; } if (!data || data.length === 0) { container.innerHTML = summaryHtml + `

    该时间段内没有 API 调用记录。

    `; return; } // 创建表格 let tableHtml = ` `; // 填充表格行 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 += ` `; }); tableHtml += `
    时间 密钥 (部分) 模型 状态
    ${timestamp} ${keyDisplay} ${ call.model || "N/A" } ${call.status}
    `; 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 = `

    该密钥在最近24小时内没有调用记录。

    `; return; } let tableHtml = ` `; const sortedModels = Object.entries(data).sort( ([, countA], [, countB]) => countB - countA ); sortedModels.forEach(([model, count]) => { tableHtml += ` `; }); tableHtml += `
    模型名称 调用次数 (24h)
    ${model} ${count}
    `; container.innerHTML = tableHtml; } // 设置标题 titleElement.textContent = `密钥 ${keyDisplay} - 最近24小时请求详情`; // 显示模态框并设置加载状态 modal.classList.remove("hidden"); contentArea.innerHTML = `

    加载中...

    `; 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 = `

    加载失败: ${apiError.message}

    `; } }; // 关闭密钥使用详情模态框 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 = ''; 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 = ''; 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 }); }