mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-07-08 08:11:36 +08:00
refactor:Enhances key verification and management UI
Refactors bulk key verification for improved error handling and reporting. The UI is updated to use checkboxes for key selection and batch actions. Adds detailed verification results modal to display success and failure details. Improves key filtering, selection and actions for both valid and invalid keys. Fixes visual glitches with section collapsing/expanding animations.
This commit is contained in:
@@ -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
|
||||
})
|
||||
@@ -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 = '<i class="fas fa-redo-alt"></i> 重置';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示重置确认模态框 (基于选中的密钥)
|
||||
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 = '<i class="fas fa-check-circle text-success-500"></i>';
|
||||
iconElement.className = 'text-5xl mb-3 text-success-500';
|
||||
iconElement.className = 'text-6xl mb-3 text-success-500'; // 稍微增大图标
|
||||
} else {
|
||||
iconElement.innerHTML = '<i class="fas fa-times-circle"></i>';
|
||||
iconElement.className = 'text-5xl mb-3 text-danger-500';
|
||||
iconElement.innerHTML = '<i class="fas fa-times-circle text-danger-500"></i>';
|
||||
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 = '<i class="fas fa-check-double text-success-500"></i>';
|
||||
iconElement.className = 'text-6xl mb-3 text-success-500';
|
||||
} else if (invalidCount > 0 && validCount > 0) {
|
||||
iconElement.innerHTML = '<i class="fas fa-exclamation-triangle text-warning-500"></i>';
|
||||
iconElement.className = 'text-6xl mb-3 text-warning-500';
|
||||
} else if (invalidCount > 0 && validCount === 0) {
|
||||
iconElement.innerHTML = '<i class="fas fa-times-circle text-danger-500"></i>';
|
||||
iconElement.className = 'text-6xl mb-3 text-danger-500';
|
||||
} else { // 都为 0 或其他情况
|
||||
iconElement.innerHTML = '<i class="fas fa-info-circle text-gray-500"></i>';
|
||||
iconElement.className = 'text-6xl mb-3 text-gray-500';
|
||||
}
|
||||
|
||||
// 构建详细内容
|
||||
messageElement.innerHTML = ''; // 清空
|
||||
|
||||
const summaryDiv = document.createElement('div');
|
||||
summaryDiv.className = 'text-center mb-4 text-lg';
|
||||
summaryDiv.innerHTML = `验证完成:<span class="font-semibold text-success-600">${validCount}</span> 个成功,<span class="font-semibold text-danger-600">${invalidCount}</span> 个失败。`;
|
||||
messageElement.appendChild(summaryDiv);
|
||||
|
||||
// 成功列表
|
||||
if (successfulKeys.length > 0) {
|
||||
const successDiv = document.createElement('div');
|
||||
successDiv.className = 'mb-3';
|
||||
const successHeader = document.createElement('div');
|
||||
successHeader.className = 'flex justify-between items-center mb-1';
|
||||
successHeader.innerHTML = `<h4 class="font-semibold text-success-700">成功密钥 (${successfulKeys.length}):</h4>`;
|
||||
|
||||
const copySuccessBtn = document.createElement('button');
|
||||
copySuccessBtn.className = 'px-2 py-0.5 bg-green-100 hover:bg-green-200 text-green-700 text-xs rounded transition-colors';
|
||||
copySuccessBtn.innerHTML = '<i class="fas fa-copy mr-1"></i>复制全部';
|
||||
copySuccessBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
copyToClipboard(successfulKeys.join('\n'))
|
||||
.then(() => showNotification(`已复制 ${successfulKeys.length} 个成功密钥`, 'success'))
|
||||
.catch(() => showNotification('复制失败', 'error'));
|
||||
};
|
||||
successHeader.appendChild(copySuccessBtn);
|
||||
successDiv.appendChild(successHeader);
|
||||
|
||||
const successList = document.createElement('ul');
|
||||
successList.className = 'list-disc list-inside text-sm text-gray-600 max-h-20 overflow-y-auto bg-gray-50 p-2 rounded border border-gray-200';
|
||||
successfulKeys.forEach(key => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'font-mono';
|
||||
// Store full key in dataset for potential future use, display masked
|
||||
li.dataset.fullKey = key;
|
||||
li.textContent = key.substring(0, 4) + '...' + key.substring(key.length - 4);
|
||||
successList.appendChild(li);
|
||||
});
|
||||
successDiv.appendChild(successList);
|
||||
messageElement.appendChild(successDiv);
|
||||
}
|
||||
|
||||
// 失败列表
|
||||
if (Object.keys(failedKeys).length > 0) {
|
||||
const failDiv = document.createElement('div');
|
||||
failDiv.className = 'mb-1'; // 减少底部边距
|
||||
const failHeader = document.createElement('div');
|
||||
failHeader.className = 'flex justify-between items-center mb-1';
|
||||
failHeader.innerHTML = `<h4 class="font-semibold text-danger-700">失败密钥 (${Object.keys(failedKeys).length}):</h4>`;
|
||||
|
||||
const copyFailBtn = document.createElement('button');
|
||||
copyFailBtn.className = 'px-2 py-0.5 bg-red-100 hover:bg-red-200 text-red-700 text-xs rounded transition-colors';
|
||||
copyFailBtn.innerHTML = '<i class="fas fa-copy mr-1"></i>复制全部';
|
||||
const failedKeysArray = Object.keys(failedKeys); // Get array of failed keys
|
||||
copyFailBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
copyToClipboard(failedKeysArray.join('\n'))
|
||||
.then(() => showNotification(`已复制 ${failedKeysArray.length} 个失败密钥`, 'success'))
|
||||
.catch(() => showNotification('复制失败', 'error'));
|
||||
};
|
||||
failHeader.appendChild(copyFailBtn);
|
||||
failDiv.appendChild(failHeader);
|
||||
|
||||
const 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 = '<i class="fas fa-info-circle mr-1"></i>详情';
|
||||
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 = '<i class="fas fa-info-circle mr-1"></i>详情'; // 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 = '<i class="fas fa-chevron-up mr-1"></i>收起'; // 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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@
|
||||
<!-- 有效密钥区域 -->
|
||||
<div class="stats-card mb-6 animate-fade-in" style="animation-delay: 0.2s">
|
||||
<div class="stats-card-header cursor-pointer" onclick="toggleSection(this, 'validKeys')">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-3 flex-grow"> <!-- Added flex-grow -->
|
||||
<i class="fas fa-chevron-down toggle-icon text-primary-600"></i>
|
||||
<i class="fas fa-check-circle text-success-500"></i>
|
||||
<h2 class="text-lg font-semibold">有效密钥列表 ({{ valid_key_count }})</h2>
|
||||
@@ -323,140 +323,164 @@
|
||||
<label for="failCountThreshold" class="text-sm text-gray-600 select-none">失败次数≥</label>
|
||||
<input type="number" id="failCountThreshold" value="0" min="0" class="form-input h-7 w-16 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-primary-500 focus:border-primary-500" onclick="event.stopPropagation();">
|
||||
</div>
|
||||
<!-- 全选复选框 -->
|
||||
<div class="flex items-center gap-1 ml-4" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="selectAllValid" class="form-checkbox h-4 w-4 text-primary-600 border-gray-300 rounded focus:ring-primary-500" onchange="toggleSelectAll('valid', this.checked)">
|
||||
<label for="selectAllValid" class="text-sm text-gray-600 select-none">全选</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="flex items-center gap-2 bg-teal-500 hover:bg-teal-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); showVerifyModal('valid', event)"> <!-- 新增批量验证按钮 -->
|
||||
<i class="fas fa-check-double"></i>
|
||||
批量验证
|
||||
</button>
|
||||
<button class="flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); resetAllKeysFailCount('valid', event)" data-reset-type="valid">
|
||||
<i class="fas fa-redo-alt"></i>
|
||||
批量重置
|
||||
</button>
|
||||
<button class="flex items-center gap-2 bg-primary-600 hover:bg-primary-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); copyKeys('valid')">
|
||||
<i class="fas fa-copy"></i>
|
||||
批量复制
|
||||
</button>
|
||||
</div>
|
||||
<!-- 原始批量操作按钮(可选保留或移除) -->
|
||||
<!-- <div class="flex gap-2"> ... </div> -->
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 批量操作按钮组 (仅在选中时显示) -->
|
||||
<div id="validBatchActions" class="p-3 bg-gray-50 border-t border-gray-200 hidden flex items-center gap-3"> <!-- Changed to flex layout -->
|
||||
<span class="text-sm font-medium text-gray-700">已选择 <span id="validSelectedCount">0</span> 项</span>
|
||||
<button class="flex items-center gap-2 bg-teal-500 hover:bg-teal-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" onclick="event.stopPropagation(); showVerifyModal('valid', event)" disabled>
|
||||
<i class="fas fa-check-double"></i> 批量验证
|
||||
</button>
|
||||
<button class="flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" onclick="event.stopPropagation(); resetAllKeysFailCount('valid', event)" data-reset-type="valid" disabled>
|
||||
<i class="fas fa-redo-alt"></i> 批量重置
|
||||
</button>
|
||||
<button class="flex items-center gap-2 bg-primary-600 hover:bg-primary-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" onclick="event.stopPropagation(); copySelectedKeys('valid')" disabled>
|
||||
<i class="fas fa-copy"></i> 批量复制
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="key-content p-4 bg-white bg-opacity-40">
|
||||
<ul id="validKeys" class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{% if valid_keys %}
|
||||
{% for key, fail_count in valid_keys.items() %}
|
||||
<li class="bg-white rounded-lg p-3 shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100 hover:border-success-300 transform hover:-translate-y-1" data-fail-count="{{ fail_count }}">
|
||||
<div class="flex flex-col justify-between h-full gap-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-success-50 text-success-600">
|
||||
<i class="fas fa-check mr-1"></i> 有效
|
||||
</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="key-text font-mono text-gray-700" data-full-key="{{ key }}">{{ key[:4] + '...' + key[-4:] }}</span>
|
||||
<button class="text-gray-500 hover:text-primary-600 transition-colors" onclick="toggleKeyVisibility(this)">
|
||||
<i class="fas fa-eye"></i>
|
||||
<li class="bg-white rounded-lg p-3 shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100 hover:border-success-300 transform hover:-translate-y-1 flex items-start gap-3" data-fail-count="{{ fail_count }}" data-key="{{ key }}"> <!-- Added flex, items-start, gap, data-key -->
|
||||
<!-- Checkbox -->
|
||||
<input type="checkbox" class="form-checkbox h-5 w-5 text-primary-600 border-gray-300 rounded focus:ring-primary-500 mt-1 key-checkbox" data-key-type="valid" value="{{ key }}" onchange="updateBatchActions('valid')">
|
||||
<!-- Key Info -->
|
||||
<div class="flex-grow"> <!-- Added flex-grow -->
|
||||
<div class="flex flex-col justify-between h-full gap-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-success-50 text-success-600">
|
||||
<i class="fas fa-check mr-1"></i> 有效
|
||||
</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="key-text font-mono text-gray-700" data-full-key="{{ key }}">{{ key[:4] + '...' + key[-4:] }}</span>
|
||||
<button class="text-gray-500 hover:text-primary-600 transition-colors" onclick="toggleKeyVisibility(this)">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-50 text-amber-600">
|
||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
||||
失败: {{ fail_count }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button class="flex items-center gap-1 bg-success-500 hover:bg-success-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="verifyKey('{{ key }}', this)">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
验证
|
||||
</button>
|
||||
<button class="flex items-center gap-1 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="resetKeyFailCount('{{ key }}', this)">
|
||||
<i class="fas fa-redo-alt"></i>
|
||||
重置
|
||||
</button>
|
||||
<button class="flex items-center gap-1 bg-gray-500 hover:bg-gray-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="copyKey('{{ key }}')">
|
||||
<i class="fas fa-copy"></i>
|
||||
复制
|
||||
</button>
|
||||
<button class="flex items-center gap-1 bg-purple-500 hover:bg-purple-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="showKeyUsageDetails('{{ key }}')">
|
||||
<i class="fas fa-chart-pie"></i>
|
||||
详情
|
||||
</button>
|
||||
</div>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-50 text-amber-600">
|
||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
||||
失败: {{ fail_count }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button class="flex items-center gap-1 bg-success-500 hover:bg-success-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="verifyKey('{{ key }}', this)">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
验证
|
||||
</button>
|
||||
<button class="flex items-center gap-1 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="resetKeyFailCount('{{ key }}', this)">
|
||||
<i class="fas fa-redo-alt"></i>
|
||||
重置
|
||||
</button>
|
||||
<button class="flex items-center gap-1 bg-gray-500 hover:bg-gray-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="copyKey('{{ key }}')">
|
||||
<i class="fas fa-copy"></i>
|
||||
复制
|
||||
</button>
|
||||
<button class="flex items-center gap-1 bg-purple-500 hover:bg-purple-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="showKeyUsageDetails('{{ key }}')">
|
||||
<i class="fas fa-chart-pie"></i>
|
||||
详情
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li class="text-center text-gray-500 py-4">暂无有效密钥</li>
|
||||
<li class="text-center text-gray-500 py-4 col-span-full">暂无有效密钥</li> <!-- Added col-span-full -->
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 无效密钥区域 -->
|
||||
<div class="stats-card mb-6 animate-fade-in" style="animation-delay: 0.4s">
|
||||
<div class="stats-card-header cursor-pointer" onclick="toggleSection(this, 'invalidKeys')">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-3 flex-grow"> <!-- Added flex-grow -->
|
||||
<i class="fas fa-chevron-down toggle-icon text-primary-600"></i>
|
||||
<i class="fas fa-times-circle text-danger-500"></i>
|
||||
<h2 class="text-lg font-semibold">无效密钥列表 ({{ invalid_key_count }})</h2>
|
||||
<!-- 全选复选框 -->
|
||||
<div class="flex items-center gap-1 ml-4" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="selectAllInvalid" class="form-checkbox h-4 w-4 text-primary-600 border-gray-300 rounded focus:ring-primary-500" onchange="toggleSelectAll('invalid', this.checked)">
|
||||
<label for="selectAllInvalid" class="text-sm text-gray-600 select-none">全选</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="flex items-center gap-2 bg-teal-500 hover:bg-teal-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); showVerifyModal('invalid', event)"> <!-- 新增批量验证按钮 -->
|
||||
<i class="fas fa-check-double"></i>
|
||||
批量验证
|
||||
</button>
|
||||
<button class="flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); resetAllKeysFailCount('invalid', event)" data-reset-type="invalid">
|
||||
<i class="fas fa-redo-alt"></i>
|
||||
批量重置
|
||||
</button>
|
||||
<button class="flex items-center gap-2 bg-primary-600 hover:bg-primary-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); copyKeys('invalid')">
|
||||
<i class="fas fa-copy"></i>
|
||||
批量复制
|
||||
</button>
|
||||
</div>
|
||||
<!-- 原始批量操作按钮(可选保留或移除) -->
|
||||
<!-- <div class="flex gap-2"> ... </div> -->
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 批量操作按钮组 (仅在选中时显示) -->
|
||||
<div id="invalidBatchActions" class="p-3 bg-gray-50 border-t border-gray-200 hidden flex items-center gap-3"> <!-- Changed to flex layout -->
|
||||
<span class="text-sm font-medium text-gray-700">已选择 <span id="invalidSelectedCount">0</span> 项</span>
|
||||
<button class="flex items-center gap-2 bg-teal-500 hover:bg-teal-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" onclick="event.stopPropagation(); showVerifyModal('invalid', event)" disabled>
|
||||
<i class="fas fa-check-double"></i> 批量验证
|
||||
</button>
|
||||
<button class="flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" onclick="event.stopPropagation(); resetAllKeysFailCount('invalid', event)" data-reset-type="invalid" disabled>
|
||||
<i class="fas fa-redo-alt"></i> 批量重置
|
||||
</button>
|
||||
<button class="flex items-center gap-2 bg-primary-600 hover:bg-primary-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" onclick="event.stopPropagation(); copySelectedKeys('invalid')" disabled>
|
||||
<i class="fas fa-copy"></i> 批量复制
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="key-content p-4 bg-white bg-opacity-40">
|
||||
<ul id="invalidKeys" class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{% if invalid_keys %}
|
||||
{% for key, fail_count in invalid_keys.items() %}
|
||||
<li class="bg-white rounded-lg p-3 shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100 hover:border-danger-300 transform hover:-translate-y-1">
|
||||
<div class="flex flex-col justify-between h-full gap-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-danger-50 text-danger-600">
|
||||
<i class="fas fa-times mr-1"></i> 无效
|
||||
</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="key-text font-mono text-gray-700" data-full-key="{{ key }}">{{ key[:4] + '...' + key[-4:] }}</span>
|
||||
<button class="text-gray-500 hover:text-primary-600 transition-colors" onclick="toggleKeyVisibility(this)">
|
||||
<i class="fas fa-eye"></i>
|
||||
<li class="bg-white rounded-lg p-3 shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100 hover:border-danger-300 transform hover:-translate-y-1 flex items-start gap-3" data-key="{{ key }}"> <!-- Added flex, items-start, gap, data-key -->
|
||||
<!-- Checkbox -->
|
||||
<input type="checkbox" class="form-checkbox h-5 w-5 text-primary-600 border-gray-300 rounded focus:ring-primary-500 mt-1 key-checkbox" data-key-type="invalid" value="{{ key }}" onchange="updateBatchActions('invalid')">
|
||||
<!-- Key Info -->
|
||||
<div class="flex-grow"> <!-- Added flex-grow -->
|
||||
<div class="flex flex-col justify-between h-full gap-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-danger-50 text-danger-600">
|
||||
<i class="fas fa-times mr-1"></i> 无效
|
||||
</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="key-text font-mono text-gray-700" data-full-key="{{ key }}">{{ key[:4] + '...' + key[-4:] }}</span>
|
||||
<button class="text-gray-500 hover:text-primary-600 transition-colors" onclick="toggleKeyVisibility(this)">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-50 text-amber-600">
|
||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
||||
失败: {{ fail_count }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button class="flex items-center gap-1 bg-success-500 hover:bg-success-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="verifyKey('{{ key }}', this)">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
验证
|
||||
</button>
|
||||
<button class="flex items-center gap-1 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="resetKeyFailCount('{{ key }}', this)">
|
||||
<i class="fas fa-redo-alt"></i>
|
||||
重置
|
||||
</button>
|
||||
<button class="flex items-center gap-1 bg-gray-500 hover:bg-gray-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="copyKey('{{ key }}')">
|
||||
<i class="fas fa-copy"></i>
|
||||
复制
|
||||
</button>
|
||||
<button class="flex items-center gap-1 bg-purple-500 hover:bg-purple-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="showKeyUsageDetails('{{ key }}')">
|
||||
<i class="fas fa-chart-pie"></i>
|
||||
详情
|
||||
</button>
|
||||
</div>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-50 text-amber-600">
|
||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
||||
失败: {{ fail_count }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button class="flex items-center gap-1 bg-success-500 hover:bg-success-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="verifyKey('{{ key }}', this)">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
验证
|
||||
</button>
|
||||
<button class="flex items-center gap-1 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="resetKeyFailCount('{{ key }}', this)">
|
||||
<i class="fas fa-redo-alt"></i>
|
||||
重置
|
||||
</button>
|
||||
<button class="flex items-center gap-1 bg-gray-500 hover:bg-gray-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="copyKey('{{ key }}')">
|
||||
<i class="fas fa-copy"></i>
|
||||
复制
|
||||
</button>
|
||||
<button class="flex items-center gap-1 bg-purple-500 hover:bg-purple-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="showKeyUsageDetails('{{ key }}')">
|
||||
<i class="fas fa-chart-pie"></i>
|
||||
详情
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li class="text-center text-gray-500 py-4">暂无无效密钥</li>
|
||||
<li class="text-center text-gray-500 py-4 col-span-full">暂无无效密钥</li> <!-- Added col-span-full -->
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -537,8 +561,9 @@
|
||||
</div>
|
||||
<div class="px-8 pb-2 w-full">
|
||||
<div id="resultModalMessage"
|
||||
class="text-gray-700 text-base leading-relaxed break-all whitespace-pre-line max-h-60 overflow-y-auto border border-gray-100 rounded-lg bg-gray-50 p-4 shadow-inner"
|
||||
class="text-gray-700 text-base leading-relaxed break-words whitespace-pre-line max-h-80 overflow-y-auto border border-gray-100 rounded-lg bg-gray-50 p-4 shadow-inner" /* Increased max-h-60 to max-h-80 and changed break-all to break-words */
|
||||
style="font-family: 'JetBrains Mono', 'Fira Mono', 'Consolas', 'monospace';">
|
||||
<!-- Content is dynamically generated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center px-8 pb-6 pt-2">
|
||||
@@ -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
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user