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 @@
-
+
+
+
+ 已选择 0 项
+
+ 批量验证
+
+
+ 批量重置
+
+
+ 批量复制
+
+
+
{% if valid_keys %}
{% for key, fail_count in valid_keys.items() %}
-
-
-
-
- 有效
-
-
-
{{ key[:4] + '...' + key[-4:] }}
-
-
+
+
+
+
+
+
+
+
+ 有效
+
+
+ {{ key[:4] + '...' + key[-4:] }}
+
+
+
+
+
+
+ 失败: {{ fail_count }}
+
+
+
+
+
+ 验证
+
+
+
+ 重置
+
+
+
+ 复制
+
+
+
+ 详情
-
-
- 失败: {{ fail_count }}
-
-
-
-
-
- 验证
-
-
-
- 重置
-
-
-
- 复制
-
-
-
- 详情
-
{% endfor %}
{% else %}
- 暂无有效密钥
+ 暂无有效密钥
{% endif %}
-
+