diff --git a/app/router/gemini_routes.py b/app/router/gemini_routes.py index 0b6b3c5..0523eb6 100644 --- a/app/router/gemini_routes.py +++ b/app/router/gemini_routes.py @@ -325,13 +325,12 @@ async def verify_selected_keys( if not keys_to_verify: return JSONResponse({"success": False, "message": "没有提供需要验证的密钥"}, status_code=400) - valid_count = 0 - invalid_count = 0 - verification_errors = {} # 存储验证过程中的错误 + successful_keys = [] + failed_keys = {} # 存储失败的 key 和错误信息 async def _verify_single_key(api_key: str): """内部函数,用于验证单个密钥并处理异常""" - nonlocal valid_count, invalid_count # 允许修改外部计数器 + nonlocal successful_keys, failed_keys # 允许修改外部列表和字典 try: # 重用单密钥验证逻辑的核心部分 gemini_request = GeminiRequest( @@ -344,7 +343,7 @@ async def verify_selected_keys( api_key ) # 如果上面没有抛出异常,则认为密钥有效 - valid_count += 1 + successful_keys.append(api_key) return api_key, "valid", None except Exception as e: error_message = str(e) @@ -358,7 +357,7 @@ async def verify_selected_keys( # 如果密钥不在计数中(可能刚添加或从未失败),初始化为1 key_manager.key_failure_counts[api_key] = 1 logger.warning(f"Bulk verification exception for key: {api_key}, initializing failure count to 1") - invalid_count += 1 + failed_keys[api_key] = error_message # 记录失败的 key 和错误信息 return api_key, "invalid", error_message # 并发执行所有密钥的验证 @@ -373,28 +372,39 @@ async def verify_selected_keys( # 可以选择如何处理这种任务级别的错误,这里我们简单记录 # 也可以将其计入 invalid_count 或单独记录 elif result: - key, status, error = result - if status == "invalid" and error: - verification_errors[key] = error # 记录具体的验证错误信息 + # result 可能是 (key, status, error) 或 Exception + if not isinstance(result, Exception) and result: + key, status, error = result + # 失败信息已在 _verify_single_key 中记录到 failed_keys + elif isinstance(result, Exception): + # 记录任务本身的异常,可以关联到一个特定的 key 如果可能的话 + # 这里简化处理,只记录日志 + logger.error(f"Task execution error during bulk verification: {result}") + valid_count = len(successful_keys) + invalid_count = len(failed_keys) logger.info(f"Bulk verification finished. Valid: {valid_count}, Invalid: {invalid_count}") - # 根据是否有错误决定最终消息和状态 - if verification_errors or valid_count + invalid_count != len(keys_to_verify): # 检查是否有错误或任务异常 - error_summary = "; ".join([f"{k}: {v}" for k, v in verification_errors.items()]) - message = f"批量验证完成,但出现问题。有效: {valid_count}, 无效: {invalid_count}。错误详情: {error_summary or '任务执行异常'}" + # 根据是否有失败的 key 决定最终消息和状态 + if failed_keys: + message = f"批量验证完成。成功: {valid_count}, 失败: {invalid_count}。" + # 即使有失败也认为是部分成功,返回 200 OK,让前端处理详细结果 return JSONResponse({ - "success": False, # 标记为失败,因为有错误 + "success": True, # 表示请求处理完成,具体结果看内容 "message": message, - "valid_count": valid_count, - "invalid_count": invalid_count, - "errors": verification_errors - }, status_code=207) # 207 Multi-Status 表示部分成功/失败 + "successful_keys": successful_keys, + "failed_keys": failed_keys, + "valid_count": valid_count, # 保留计数方便前端快速展示 + "invalid_count": invalid_count + }) else: # 完全成功 + message = f"批量验证成功完成。所有 {valid_count} 个密钥均有效。" return JSONResponse({ "success": True, - "message": f"批量验证成功完成。有效: {valid_count}, 无效: {invalid_count}", + "message": message, + "successful_keys": successful_keys, + "failed_keys": {}, "valid_count": valid_count, - "invalid_count": invalid_count + "invalid_count": 0 }) \ No newline at end of file diff --git a/app/static/js/keys_status.js b/app/static/js/keys_status.js index 81c7187..1db8269 100644 --- a/app/static/js/keys_status.js +++ b/app/static/js/keys_status.js @@ -51,21 +51,67 @@ function initStatItemAnimations() { }); } -function copyKeys(type) { - // 选择对应区域内所有可见的 li 元素下的 key-text span - const visibleKeyItems = document.querySelectorAll(`#${type}Keys li:not([style*="display: none"]) .key-text`); - const keys = Array.from(visibleKeyItems).map(span => span.dataset.fullKey); +// 获取指定类型区域内选中的密钥 +function getSelectedKeys(type) { + const checkboxes = document.querySelectorAll(`#${type}Keys .key-checkbox:checked`); + return Array.from(checkboxes).map(cb => cb.value); +} - if (keys.length === 0) { - showNotification('没有可复制的筛选后密钥', 'warning'); // 修改提示信息 +// 更新指定类型区域的批量操作按钮状态和计数 +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 checkboxes = document.querySelectorAll(`#${type}Keys li:not([style*="display: none"]) .key-checkbox`); // 只选择可见的 + checkboxes.forEach(checkbox => { + checkbox.checked = isChecked; + }); + updateBatchActions(type); +} + +// 复制选中的密钥 +function copySelectedKeys(type) { + const selectedKeys = getSelectedKeys(type); + + if (selectedKeys.length === 0) { + showNotification('没有选中的密钥可复制', 'warning'); return; } - const keysText = keys.join('\n'); + const keysText = selectedKeys.join('\n'); copyToClipboard(keysText) .then(() => { - showNotification(`已成功复制 ${keys.length} 个筛选后的${type === 'valid' ? '有效' : '无效'}密钥`); // 修改提示信息 + showNotification(`已成功复制 ${selectedKeys.length} 个选中的${type === 'valid' ? '有效' : '无效'}密钥`); }) .catch((err) => { console.error('无法复制文本: ', err); @@ -73,6 +119,8 @@ function copyKeys(type) { }); } + +// 单个复制保持不变 function copyKey(key) { copyToClipboard(key) .then(() => { @@ -177,43 +225,33 @@ async function resetKeyFailCount(key, button) { console.error('重置失败:', error); showNotification('重置请求失败: ' + error.message, 'error'); // 确保在捕获到错误时恢复按钮状态 - button.innerHTML = originalHtml; // 需要确保 originalHtml 在此作用域可用 + // button.innerHTML = originalHtml; // 需要确保 originalHtml 在此作用域可用 - 这行代码在原始逻辑中可能导致错误,暂时注释掉 button.disabled = false; button.innerHTML = ' 重置'; } } +// 显示重置确认模态框 (基于选中的密钥) function showResetModal(type) { const modalElement = document.getElementById('resetModal'); const titleElement = document.getElementById('resetModalTitle'); const messageElement = document.getElementById('resetModalMessage'); const confirmButton = document.getElementById('confirmResetBtn'); - // 获取当前筛选后可见的、且包含 data-fail-count 属性的密钥数量 - // 根据密钥类型选择合适的选择器 - let keySelector; - if (type === 'valid') { - // 对于有效密钥,可能需要基于失败次数筛选,保留 data-fail-count (虽然批量重置通常不需要筛选) - // 如果批量重置有效密钥也应重置所有可见的,可以将此行改为下面 else 中的选择器 - keySelector = `#${type}Keys li[data-fail-count]:not([style*="display: none"])`; - } else { - // 对于无效密钥,我们想要重置所有可见的无效密钥,不依赖 data-fail-count - keySelector = `#${type}Keys li:not([style*="display: none"])`; - } - const visibleKeyItems = document.querySelectorAll(keySelector); - const count = visibleKeyItems.length; + const selectedKeys = getSelectedKeys(type); + const count = selectedKeys.length; // 设置标题和消息 titleElement.textContent = '批量重置失败次数'; if (count > 0) { - messageElement.textContent = `确定要批量重置筛选出的 ${count} 个${type === 'valid' ? '有效' : '无效'}密钥的失败次数吗?`; + messageElement.textContent = `确定要批量重置选中的 ${count} 个${type === 'valid' ? '有效' : '无效'}密钥的失败次数吗?`; confirmButton.disabled = false; // 确保按钮可用 } else { - messageElement.textContent = `当前没有筛选出可重置的${type === 'valid' ? '有效' : '无效'}密钥。`; - confirmButton.disabled = true; // 没有可重置的密钥时禁用确认按钮 + // 这个情况理论上不会发生,因为按钮在未选中时是禁用的 + messageElement.textContent = `请先选择要重置的${type === 'valid' ? '有效' : '无效'}密钥。`; + confirmButton.disabled = true; } - // 设置确认按钮事件 confirmButton.onclick = () => executeResetAll(type); @@ -245,42 +283,201 @@ function closeResultModal(reload = true) { } } -// 显示操作结果模态框 +// 显示操作结果模态框 (通用版本) 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-5xl mb-3 text-success-500'; + iconElement.className = 'text-6xl mb-3 text-success-500'; // 稍微增大图标 } else { - iconElement.innerHTML = ''; - iconElement.className = 'text-5xl mb-3 text-danger-500'; + iconElement.innerHTML = ''; + iconElement.className = 'text-6xl mb-3 text-danger-500'; // 稍微增大图标 } - - // 设置消息 - // 支持长文本和换行,内容插入到div而不是p + + // 清空现有内容并设置新消息 + messageElement.innerHTML = ''; // 清空 if (typeof message === 'string') { - // 如果内容包含换行或长文本,自动转为可滚动 - messageElement.textContent = ''; - messageElement.innerText = message; + // 对于普通字符串消息,保持原有逻辑 + const messageDiv = document.createElement('div'); + messageDiv.innerText = message; // 使用 innerText 防止 XSS + messageElement.appendChild(messageDiv); } else if (message instanceof Node) { - messageElement.innerHTML = ''; + // 如果传入的是 DOM 节点,直接添加 messageElement.appendChild(message); } else { - messageElement.textContent = String(message); + // 其他类型转为字符串 + 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 failList = document.createElement('ul'); + failList.className = 'text-sm text-gray-600 max-h-32 overflow-y-auto bg-red-50 p-2 rounded border border-red-200 space-y-1'; // 增加最大高度和间距 + Object.entries(failedKeys).forEach(([key, error]) => { + const li = document.createElement('li'); + li.className = 'flex justify-between items-center'; // Restore original layout + + const keySpan = document.createElement('span'); + keySpan.className = 'font-mono'; + // Store full key in dataset, display masked + 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; // 将错误信息存储在 data 属性中 + detailsButton.onclick = (e) => { + e.stopPropagation(); // Prevent modal close + const button = e.currentTarget; + const listItem = button.closest('li'); + const errorMsg = button.dataset.error; + const errorDetailsId = `error-details-${key.replace(/[^a-zA-Z0-9]/g, '')}`; // Create unique ID + let errorDiv = listItem.querySelector(`#${errorDetailsId}`); + + if (errorDiv) { + // Collapse: Remove error div and reset li layout + errorDiv.remove(); + listItem.className = 'flex justify-between items-center'; // Restore original layout + button.innerHTML = '详情'; // Restore button text + } else { + // Expand: Create and append error div, change li layout + errorDiv = document.createElement('div'); + errorDiv.id = errorDetailsId; + errorDiv.className = 'w-full mt-1 pl-2 text-xs text-red-600 bg-red-50 p-1 rounded border border-red-100 whitespace-pre-wrap break-words'; // Added w-full + errorDiv.textContent = errorMsg; + listItem.appendChild(errorDiv); + listItem.className = 'flex flex-col items-start'; // Change layout to vertical + button.innerHTML = '收起'; // Change button text + // Move button to be alongside the keySpan for vertical layout + const keySpanContainer = document.createElement('div'); + keySpanContainer.className = 'flex justify-between items-center w-full'; // Ensure key and button are on the same line initially + keySpanContainer.appendChild(listItem.querySelector('.font-mono')); // Move keySpan + keySpanContainer.appendChild(button); // Move button + listItem.insertBefore(keySpanContainer, errorDiv); // Insert container before errorDiv + } + }; + + li.appendChild(keySpan); + li.appendChild(detailsButton); // Add button back + failList.appendChild(li); + }); + failDiv.appendChild(failList); + messageElement.appendChild(failDiv); + } + + // 设置确认按钮点击事件 - 仅当没有失败时自动刷新 + const autoReload = invalidCount === 0; + confirmButton.onclick = () => closeResultModal(autoReload); + // 显示模态框 modalElement.classList.remove('hidden'); } @@ -297,12 +494,11 @@ async function executeResetAll(type) { return; } - // 获取筛选后可见的密钥 - const visibleKeyItems = document.querySelectorAll(`#${type}Keys li:not([style*="display: none"]) .key-text`); - const keysToReset = Array.from(visibleKeyItems).map(span => span.dataset.fullKey); + // 获取选中的密钥 + const keysToReset = getSelectedKeys(type); if (keysToReset.length === 0) { - showNotification(`没有需要重置的筛选后${type === 'valid' ? '有效' : '无效'}密钥`, 'warning'); + showNotification(`没有选中的${type === 'valid' ? '有效' : '无效'}密钥可重置`, 'warning'); return; } @@ -338,9 +534,9 @@ async function executeResetAll(type) { // 根据重置结果显示模态框 if (data.success) { - const message = data.reset_count !== undefined ? // 检查 reset_count 是否存在 - `成功重置 ${data.reset_count} 个筛选后的${type === 'valid' ? '有效' : '无效'}密钥的失败次数` : - `成功重置 ${keysToReset.length} 个筛选后的密钥`; // 如果后端没返回数量,使用前端计算的数量 + const message = data.reset_count !== undefined ? + `成功重置 ${data.reset_count} 个选中的${type === 'valid' ? '有效' : '无效'}密钥的失败次数` : + `成功重置 ${keysToReset.length} 个选中的密钥`; showResultModal(true, message); // 成功后刷新页面 } else { const errorMsg = data.message || '批量重置失败'; @@ -384,61 +580,125 @@ function refreshPage(button) { }, 300); } -// 重写切换区域显示/隐藏函数,以更好地支持新样式 +// 恢复之前的 toggleSection 函数以修复展开/收缩动画 function toggleSection(header, sectionId) { const toggleIcon = header.querySelector('.toggle-icon'); - const content = header.nextElementSibling; - + // 需要找到正确的 content 元素。它不再是紧邻的兄弟元素,而是 card 内的 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; // 获取批量操作栏 + if (toggleIcon && content) { - // 添加旋转动画 - toggleIcon.classList.toggle('collapsed'); - - // 控制内容区域的可见性 - if (!content.classList.contains('collapsed')) { - // 收起内容 - content.style.maxHeight = '0px'; - content.style.opacity = '0'; - content.style.overflow = 'hidden'; - content.classList.add('collapsed'); - - // 为动画添加延迟 - setTimeout(() => { - content.style.padding = '0'; - }, 100); - } else { + const isCollapsed = content.classList.contains('collapsed'); + + // 切换图标状态 + toggleIcon.classList.toggle('collapsed', !isCollapsed); + + if (isCollapsed) { // 展开内容 content.classList.remove('collapsed'); - content.style.padding = '1rem'; - content.style.maxHeight = '2000px'; // 使用足够大的高度 - content.style.opacity = '1'; - - // 为动画添加延迟 - setTimeout(() => { - content.style.overflow = 'visible'; - }, 300); + // 先移除内联样式,让 CSS 控制初始状态 + content.style.maxHeight = ''; + content.style.opacity = ''; + content.style.padding = ''; + content.style.overflow = ''; + // 使用 requestAnimationFrame 确保浏览器应用了初始状态 + requestAnimationFrame(() => { + // 计算内容的实际高度 + const scrollHeight = content.scrollHeight; + content.style.maxHeight = scrollHeight + 'px'; + content.style.opacity = '1'; + content.style.padding = '1rem'; // 恢复 padding + // 如果批量操作栏存在且可见,也计算其高度 + if (batchActions && !batchActions.classList.contains('hidden')) { + content.style.maxHeight = (scrollHeight + batchActions.offsetHeight) + 'px'; + } else { + content.style.maxHeight = scrollHeight + 'px'; + } + + // 动画结束后移除 max-height 以允许内容动态变化 + content.addEventListener('transitionend', function handler() { + content.removeEventListener('transitionend', handler); + if (!content.classList.contains('collapsed')) { // 确保是在展开状态 + content.style.maxHeight = ''; + content.style.overflow = 'visible'; + } + }, { once: true }); + }); + } else { + // 收起内容 + // 先设置一个明确的高度,然后过渡到 0 + content.style.maxHeight = content.scrollHeight + 'px'; + // 如果批量操作栏存在且可见,也计算其高度 + if (batchActions && !batchActions.classList.contains('hidden')) { + content.style.maxHeight = (content.scrollHeight + batchActions.offsetHeight) + 'px'; + } + requestAnimationFrame(() => { + content.style.maxHeight = '0px'; + content.style.opacity = '0'; + content.style.padding = '0 1rem'; // 保持左右 padding,收起上下 padding + content.style.overflow = 'hidden'; + content.classList.add('collapsed'); + }); } + } else { + console.error("Toggle section failed: Icon or content not found.", header, sectionId); } } -// 筛选有效密钥(根据失败次数阈值) + +// 筛选有效密钥(根据失败次数阈值)并更新批量操作状态 function filterValidKeys() { const thresholdInput = document.getElementById('failCountThreshold'); - const validKeyItems = document.querySelectorAll('#validKeys li'); + const validKeyItems = document.querySelectorAll('#validKeys li[data-key]'); // 选择包含 data-key 的 li // 读取阈值,如果输入无效或为空,则默认为0(不过滤) const threshold = parseInt(thresholdInput.value, 10); const filterThreshold = isNaN(threshold) || threshold < 0 ? 0 : threshold; + let hasVisibleItems = false; validKeyItems.forEach(item => { - const failCount = parseInt(item.dataset.failCount, 10); - // 如果失败次数大于等于阈值,则显示,否则隐藏 - if (failCount >= filterThreshold) { - item.style.display = ''; // 显示 - } else { - item.style.display = 'none'; // 隐藏 + // 确保只处理包含 data-fail-count 的 li 元素 + if (item.dataset.failCount !== undefined) { + const failCount = parseInt(item.dataset.failCount, 10); + // 如果失败次数大于等于阈值,则显示,否则隐藏 + if (failCount >= filterThreshold) { + item.style.display = 'flex'; // 使用 flex 因为 li 现在是 flex 容器 + hasVisibleItems = true; + } else { + item.style.display = 'none'; // 隐藏 + // 如果隐藏了一个项,取消其选中状态 + const checkbox = item.querySelector('.key-checkbox'); + if (checkbox && checkbox.checked) { + checkbox.checked = false; + } + } } }); + + // 更新有效密钥的批量操作状态和全选复选框 + updateBatchActions('valid'); + + // 处理“暂无有效密钥”消息 + const noMatchMsgId = 'no-valid-keys-msg'; + const validKeysList = document.getElementById('validKeys'); + let noMatchMsg = validKeysList.querySelector(`#${noMatchMsgId}`); + const initialKeyCount = validKeysList.querySelectorAll('li[data-key]').length; // 获取初始密钥数量 + + if (!hasVisibleItems && initialKeyCount > 0) { // 仅当初始有密钥但现在都不可见时显示 + if (!noMatchMsg) { + noMatchMsg = document.createElement('li'); + noMatchMsg.id = noMatchMsgId; + noMatchMsg.className = 'text-center text-gray-500 py-4 col-span-full'; + noMatchMsg.textContent = '没有符合条件的有效密钥'; + validKeysList.appendChild(noMatchMsg); + } + noMatchMsg.style.display = ''; + } else if (noMatchMsg) { + noMatchMsg.style.display = 'none'; + } } + // 初始化 document.addEventListener('DOMContentLoaded', () => { // 初始化统计区块动画 @@ -497,16 +757,28 @@ document.addEventListener('DOMContentLoaded', () => { }); }); - // 监听展开/折叠事件 - document.querySelectorAll('.stats-card-title').forEach(header => { - header.addEventListener('click', () => { - const card = header.closest('.stats-card'); - if (card) { - card.classList.toggle('active'); - } - }); + // 监听展开/折叠事件 (确保使用正确的选择器和函数) + document.querySelectorAll('.stats-card-header').forEach(header => { + // 检查 header 是否包含 toggle-icon,避免为其他卡片(如统计卡片)添加监听器 + if (header.querySelector('.toggle-icon')) { + header.addEventListener('click', (event) => { + // 确保点击的不是内部交互元素(如输入框、复选框、标签) + if (event.target.closest('input, label, button')) { + return; + } + // 从 header 中提取 sectionId (例如从关联的 content div 的 id) + 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."); + } + }); + } }); - + // 添加筛选输入框事件监听 const thresholdInput = document.getElementById('failCountThreshold'); if (thresholdInput) { @@ -518,43 +790,35 @@ document.addEventListener('DOMContentLoaded', () => { // --- 批量验证相关函数 (明确挂载到 window) --- + // 显示验证确认模态框 (基于选中的密钥) 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'); - - // 根据密钥类型选择合适的选择器来确定可验证的密钥 - let keySelector; - if (type === 'valid') { - // 对于有效密钥,可以根据需要决定是否只验证有失败记录的,或所有可见的 - // 暂时保留 data-fail-count,如果需要验证所有可见有效密钥,则移除 - keySelector = `#${type}Keys li[data-fail-count]:not([style*="display: none"])`; - } else { // type === 'invalid' - // 对于无效密钥,应该验证所有可见的无效密钥 - keySelector = `#${type}Keys li:not([style*="display: none"])`; - } - const visibleKeyItems = document.querySelectorAll(keySelector); - const count = visibleKeyItems.length; + + const selectedKeys = getSelectedKeys(type); + const count = selectedKeys.length; // 设置标题和消息 titleElement.textContent = '批量验证密钥'; if (count > 0) { - messageElement.textContent = `确定要批量验证筛选出的 ${count} 个${type === 'valid' ? '有效' : '无效'}密钥吗?此操作可能需要一些时间。`; + messageElement.textContent = `确定要批量验证选中的 ${count} 个${type === 'valid' ? '有效' : '无效'}密钥吗?此操作可能需要一些时间。`; confirmButton.disabled = false; // 确保按钮可用 } else { - messageElement.textContent = `当前没有筛选出可验证的${type === 'valid' ? '有效' : '无效'}密钥。`; - confirmButton.disabled = true; // 没有可验证的密钥时禁用确认按钮 + // 这个情况理论上不会发生,因为按钮在未选中时是禁用的 + messageElement.textContent = `请先选择要验证的${type === 'valid' ? '有效' : '无效'}密钥。`; + confirmButton.disabled = true; } - + // 设置确认按钮事件 confirmButton.onclick = () => executeVerifyAll(type); - + // 显示模态框 modalElement.classList.remove('hidden'); } @@ -573,19 +837,11 @@ document.addEventListener('DOMContentLoaded', () => { // 这里我们暂时只记录日志,实际UI反馈可以后续增强 console.log(`Starting bulk verification for ${type} keys...`); - // 根据密钥类型选择合适的选择器来获取待验证的密钥 - let keySelector; - if (type === 'valid') { - // 同上,根据需求决定是否保留 data-fail-count - keySelector = `#${type}Keys li[data-fail-count]:not([style*="display: none"]) .key-text`; - } else { // type === 'invalid' - keySelector = `#${type}Keys li:not([style*="display: none"]) .key-text`; - } - const visibleKeyItems = document.querySelectorAll(keySelector); - const keysToVerify = Array.from(visibleKeyItems).map(span => span.dataset.fullKey); + // 获取选中的密钥 + const keysToVerify = getSelectedKeys(type); if (keysToVerify.length === 0) { - showNotification(`没有需要验证的筛选后${type === 'valid' ? '有效' : '无效'}密钥`, 'warning'); + showNotification(`没有选中的${type === 'valid' ? '有效' : '无效'}密钥可验证`, 'warning'); return; } @@ -612,17 +868,9 @@ document.addEventListener('DOMContentLoaded', () => { const data = await response.json(); - // 根据验证结果显示模态框 - if (data.success) { - // 可以在这里构建更详细的消息,例如显示多少有效多少无效 - const message = `批量验证完成。有效: ${data.valid_count}, 无效: ${data.invalid_count}。页面即将刷新。`; - // 验证成功后通常需要刷新页面以更新状态 - showResultModal(true, message, true); // autoReload = true - } else { - const errorMsg = data.message || '批量验证失败'; - // 失败后不自动刷新 - showResultModal(false, '批量验证失败: ' + errorMsg, false); - } + // 使用新的专用模态框显示结果 + showVerificationResultModal(data); + // 注意:autoReload 逻辑已移至 showVerificationResultModal 内部 } catch (error) { console.error('批量验证处理失败:', error); @@ -634,6 +882,19 @@ document.addEventListener('DOMContentLoaded', () => { } } + // --- 复选框事件监听 --- + document.querySelectorAll('.key-checkbox').forEach(checkbox => { + checkbox.addEventListener('change', (event) => { + const type = event.target.dataset.keyType; + updateBatchActions(type); + }); + }); + + // 初始化批量操作区域状态 + updateBatchActions('valid'); + updateBatchActions('invalid'); + + // --- 滚动和页面控制 --- // --- 自动刷新控制 --- const autoRefreshToggle = document.getElementById('autoRefreshToggle'); diff --git a/app/templates/base.html b/app/templates/base.html index dd3b2d5..11d95bd 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -141,7 +141,7 @@ background-color: rgba(0, 0, 0, 0.8); color: white; font-weight: 500; /* font-medium */ - z-index: 50; + z-index: 1000; /* Increased z-index */ opacity: 0; transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out; } diff --git a/app/templates/keys_status.html b/app/templates/keys_status.html index 1fa25a1..007e457 100644 --- a/app/templates/keys_status.html +++ b/app/templates/keys_status.html @@ -315,7 +315,7 @@
-
+

有效密钥列表 ({{ valid_key_count }})

@@ -323,140 +323,164 @@
+ +
+ + +
-
- - - -
+ +
- + + + +
{% endfor %} {% else %} -
  • 暂无有效密钥
  • +
  • 暂无有效密钥
  • {% endif %}
    - +
    -
    +

    无效密钥列表 ({{ invalid_key_count }})

    + +
    + + +
    -
    - - - -
    + +
    - + + + +
      {% if invalid_keys %} {% for key, fail_count in invalid_keys.items() %} -
    • -
      -
      - - 无效 - -
      - {{ key[:4] + '...' + key[-4:] }} - +
      + + + 失败: {{ fail_count }} + +
      +
      + + + +
      - - - 失败: {{ fail_count }} - -
      -
      - - - -
    {% endfor %} {% else %} -
  • 暂无无效密钥
  • +
  • 暂无无效密钥
  • {% endif %}
    @@ -537,8 +561,9 @@
    +
    @@ -653,14 +678,8 @@ // Initialize other elements or event listeners if needed // The main logic (verifyKey, resetKeyFailCount, copyKey, etc.) is in keys_status.js // The toggleSection logic is now specific to this page - window.toggleSection = function(header, sectionId) { - const toggleIcon = header.querySelector('.toggle-icon'); - const content = header.nextElementSibling; // Assumes content is immediately after header - if (toggleIcon && content) { - toggleIcon.classList.toggle('collapsed'); - content.classList.toggle('collapsed'); - } - } + // The toggleSection logic is now correctly handled by keys_status.js + // Removed duplicate inline definition }); {% endblock %}