feat(keys): 实现密钥状态页面的客户端分页、搜索与筛选

- 在 keys_status.html 中:
  - 重新设计有效密钥列表头部,添加密钥搜索框、失败次数筛选器和每页显示数量选择器,并优化布局。
  - 为有效和无效密钥列表添加分页控件容器。
  - 更新 CSS 样式以支持新的筛选/分页控件、Grid 布局和改进的响应式设计。
  - 移除内联的 DOMContentLoaded 初始化脚本,相关逻辑已移至 keys_status.js。
  - 为显示/隐藏密钥按钮添加 `title` 属性以提升可访问性。
  - 调整批量操作栏布局,允许换行。
- 在 keys_status.js 中:
  - 修改 `verifyKey` 函数,在验证成功或失败后通过 `showResultModal` 关闭时强制刷新页面。
  - 调整 `verifyKey` 和 `resetKeyFailCount` 中的按钮状态恢复逻辑,以适应页面刷新行为。
  - 清理了部分冗余代码和空行。
This commit is contained in:
snaily
2025-04-25 23:56:48 +08:00
parent a5602c602e
commit cd54650431
2 changed files with 537 additions and 265 deletions

View File

@@ -39,7 +39,7 @@ function initStatItemAnimations() {
icon.style.transform = 'scale(1.1) rotate(0deg)';
}
});
item.addEventListener('mouseleave', () => {
item.style.transform = '';
const icon = item.querySelector('.stat-icon');
@@ -158,28 +158,34 @@ async function verifyKey(key, button) {
// 验证失败,显示失败结果
const errorMsg = data.error || '密钥无效';
button.style.backgroundColor = '#e74c3c';
// 使用结果模态框显示失败消息,但不自动刷新页面
showResultModal(false, '密钥验证失败: ' + errorMsg, true); // 改为true以在关闭时刷新
// 使用结果模态框显示失败消息,改为true以在关闭时刷新
showResultModal(false, '密钥验证失败: ' + errorMsg, true);
}
} catch (fetchError) {
console.error('API请求失败:', fetchError);
showResultModal(false, '验证请求失败: ' + fetchError.message, true); // 改为true以在关闭时刷新
} finally {
// 1秒后恢复按钮原始状态
// 1秒后恢复按钮原始状态 (如果页面不刷新)
// 由于现在成功和失败都会刷新,这部分逻辑可以简化或移除
// 但为了防止未来修改刷新逻辑,暂时保留,但可能不会执行
setTimeout(() => {
button.innerHTML = originalHtml;
button.disabled = false;
button.style.backgroundColor = '';
if (!document.getElementById('resultModal') || document.getElementById('resultModal').classList.contains('hidden')) {
button.innerHTML = originalHtml;
button.disabled = false;
button.style.backgroundColor = '';
}
}, 1000);
}
} catch (error) {
console.error('验证失败:', error);
button.disabled = false;
button.innerHTML = '<i class="fas fa-check-circle"></i> 验证';
// 确保在捕获到错误时恢复按钮状态 (如果页面不刷新)
// button.disabled = false; // 由 finally 处理或因刷新而无需处理
// button.innerHTML = '<i class="fas fa-check-circle"></i> 验证';
showResultModal(false, '验证处理失败: ' + error.message, true); // 改为true以在关闭时刷新
}
}
async function resetKeyFailCount(key, button) {
try {
// 禁用按钮并显示加载状态
@@ -204,33 +210,27 @@ async function resetKeyFailCount(key, button) {
showNotification('重置失败: ' + errorMsg, 'error');
// 失败时保留红色背景一会儿
button.style.backgroundColor = '#e74c3c';
}
// 立即恢复按钮状态,除非成功或失败时需要短暂显示颜色
if (!data.success) {
// 如果失败1秒后恢复按钮
setTimeout(() => {
button.innerHTML = originalHtml;
button.disabled = false;
button.style.backgroundColor = '';
}, 1000);
} else {
// 如果成功,在刷新前恢复按钮(虽然用户可能看不到)
button.innerHTML = originalHtml;
button.disabled = false;
// 背景色会在刷新时重置
}
// 恢复按钮状态逻辑已移至成功/失败分支内
} catch (error) {
console.error('重置失败:', error);
showNotification('重置请求失败: ' + error.message, 'error');
// 确保在捕获到错误时恢复按钮状态
// button.innerHTML = originalHtml; // 需要确保 originalHtml 在此作用域可用 - 这行代码在原始逻辑中可能导致错误,暂时注释掉
button.disabled = false;
button.innerHTML = '<i class="fas fa-redo-alt"></i> 重置';
button.innerHTML = '<i class="fas fa-redo-alt"></i> 重置'; // 恢复原始图标和文本
button.style.backgroundColor = ''; // 清除可能设置的背景色
}
}
// 显示重置确认模态框 (基于选中的密钥)
function showResetModal(type) {
const modalElement = document.getElementById('resetModal');
@@ -269,12 +269,11 @@ function resetAllKeysFailCount(type, event) {
if (event) {
event.stopPropagation();
}
// 显示模态确认框
showResetModal(type);
}
// 执行批量重置
// 关闭模态框并根据参数决定是否刷新页面
function closeResultModal(reload = true) {
document.getElementById('resultModal').classList.add('hidden');
@@ -423,7 +422,11 @@ function showVerificationResultModal(data) {
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
// li.className = 'flex justify-between items-center'; // Restore original layout
li.className = 'flex flex-col items-start'; // Start with 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
const keySpan = document.createElement('span');
keySpan.className = 'font-mono';
@@ -446,42 +449,39 @@ function showVerificationResultModal(data) {
if (errorDiv) {
// Collapse: Remove error div and reset li layout
errorDiv.remove();
listItem.className = 'flex justify-between items-center'; // Restore original layout
// listItem.className = 'flex justify-between items-center'; // Restore original layout
listItem.className = 'flex flex-col items-start'; // Keep vertical 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.className = 'w-full mt-1 pl-0 text-xs text-red-600 bg-red-50 p-1 rounded border border-red-100 whitespace-pre-wrap break-words'; // Adjusted padding
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
// Move button to be alongside the keySpan for vertical layout (already done)
}
};
li.appendChild(keySpan);
li.appendChild(detailsButton); // Add button back
keySpanContainer.appendChild(keySpan); // Add keySpan to container
keySpanContainer.appendChild(detailsButton); // Add button to container
li.appendChild(keySpanContainer); // Add container to list item
failList.appendChild(li);
});
failDiv.appendChild(failList);
messageElement.appendChild(failDiv);
}
// 设置确认按钮点击事件 - 仅当没有失败时自动刷新
const autoReload = invalidCount === 0;
confirmButton.onclick = () => closeResultModal(autoReload);
// 设置确认按钮点击事件 - 总是自动刷新
confirmButton.onclick = () => closeResultModal(true); // Always reload
// 显示模态框
modalElement.classList.remove('hidden');
}
async function executeResetAll(type) {
try {
// 关闭确认模态框
@@ -490,7 +490,7 @@ async function executeResetAll(type) {
// 找到对应的重置按钮以显示加载状态
const resetButton = document.querySelector(`button[data-reset-type="${type}"]`);
if (!resetButton) {
showResultModal(false, `找不到${type === 'valid' ? '有效' : '无效'}密钥区域的批量重置按钮`);
showResultModal(false, `找不到${type === 'valid' ? '有效' : '无效'}密钥区域的批量重置按钮`, false); // Don't reload if button not found
return;
}
@@ -526,7 +526,7 @@ async function executeResetAll(type) {
} catch (e) {
// 如果解析失败,使用原始错误信息
}
throw new Error(errorMsg);
}
@@ -547,9 +547,11 @@ async function executeResetAll(type) {
console.error('API请求失败:', fetchError);
showResultModal(false, '批量重置请求失败: ' + fetchError.message, false); // 失败后不自动刷新
} finally {
// 恢复按钮状态
resetButton.innerHTML = originalHtml;
resetButton.disabled = false;
// 恢复按钮状态 (仅在不刷新的情况下)
if (!document.getElementById('resultModal') || document.getElementById('resultModal').classList.contains('hidden') || document.getElementById('resultModalTitle').textContent.includes('失败')) {
resetButton.innerHTML = originalHtml;
resetButton.disabled = false;
}
}
} catch (error) {
console.error('批量重置处理失败:', error);
@@ -557,6 +559,7 @@ async function executeResetAll(type) {
}
}
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
@@ -567,19 +570,23 @@ function scrollToBottom() {
// 移除这个函数,因为它可能正在干扰按钮的显示
// HTML中已经设置了滚动按钮为flex显示不需要JavaScript额外控制
function updateScrollButtons() {
// 不执行任何操作
}
// function updateScrollButtons() {
// // 不执行任何操作
// }
function refreshPage(button) {
button.classList.add('loading');
button.classList.add('loading'); // Maybe add a loading class for visual feedback
button.disabled = true;
const icon = button.querySelector('i');
if (icon) icon.classList.add('fa-spin'); // Add spin animation
setTimeout(() => {
window.location.reload();
// No need to remove loading/spin as page reloads
}, 300);
}
// 恢复之前的 toggleSection 函数以修复展开/收缩动画
function toggleSection(header, sectionId) {
const toggleIcon = header.querySelector('.toggle-icon');
@@ -587,6 +594,7 @@ function toggleSection(header, sectionId) {
const card = header.closest('.stats-card');
const content = card ? card.querySelector('.key-content') : null;
const batchActions = card ? card.querySelector('[id$="BatchActions"]') : null; // 获取批量操作栏
const pagination = card ? card.querySelector('[id$="PaginationControls"]') : null; // 获取分页控件
if (toggleIcon && content) {
const isCollapsed = content.classList.contains('collapsed');
@@ -606,15 +614,22 @@ function toggleSection(header, sectionId) {
requestAnimationFrame(() => {
// 计算内容的实际高度
const scrollHeight = content.scrollHeight;
content.style.maxHeight = scrollHeight + 'px';
content.style.opacity = '1';
content.style.padding = '1rem'; // 恢复 padding
let totalHeight = scrollHeight;
// 如果批量操作栏存在且可见,也计算其高度
if (batchActions && !batchActions.classList.contains('hidden')) {
content.style.maxHeight = (scrollHeight + batchActions.offsetHeight) + 'px';
} else {
content.style.maxHeight = scrollHeight + 'px';
totalHeight += batchActions.offsetHeight;
}
// 如果分页控件存在且可见,也计算其高度和 margin-top
if (pagination && pagination.offsetHeight > 0) {
// Assuming mt-4 which is 1rem = 16px (adjust if needed)
totalHeight += pagination.offsetHeight + 16;
}
content.style.maxHeight = totalHeight + 'px';
content.style.opacity = '1';
content.style.padding = '1rem'; // 恢复 padding
content.style.overflow = 'hidden'; // Keep hidden during transition
// 动画结束后移除 max-height 以允许内容动态变化
content.addEventListener('transitionend', function handler() {
@@ -627,17 +642,21 @@ function toggleSection(header, sectionId) {
});
} else {
// 收起内容
// 先设置一个明确的高度,然后过渡到 0
content.style.maxHeight = content.scrollHeight + 'px';
// 如果批量操作栏存在且可见,也计算其高度
// 先计算当前总高度
let currentHeight = content.scrollHeight;
if (batchActions && !batchActions.classList.contains('hidden')) {
content.style.maxHeight = (content.scrollHeight + batchActions.offsetHeight) + 'px';
currentHeight += batchActions.offsetHeight;
}
if (pagination && pagination.offsetHeight > 0) {
currentHeight += pagination.offsetHeight + 16;
}
// 设置一个明确的高度,然后过渡到 0
content.style.maxHeight = currentHeight + 'px';
content.style.overflow = 'hidden'; // Ensure overflow is hidden before starting transition
requestAnimationFrame(() => {
content.style.maxHeight = '0px';
content.style.opacity = '0';
content.style.padding = '0 1rem'; // 保持左右 padding收起上下 padding
content.style.overflow = 'hidden';
content.classList.add('collapsed');
});
}
@@ -650,7 +669,10 @@ function toggleSection(header, sectionId) {
// 筛选有效密钥(根据失败次数阈值)并更新批量操作状态
function filterValidKeys() {
const thresholdInput = document.getElementById('failCountThreshold');
const validKeyItems = document.querySelectorAll('#validKeys li[data-key]'); // 选择包含 data-key 的 li
const validKeysList = document.getElementById('validKeys'); // Get the UL element
if (!validKeysList) return; // Exit if the list doesn't exist
const validKeyItems = validKeysList.querySelectorAll('li[data-key]'); // Select li elements within the list
// 读取阈值如果输入无效或为空则默认为0不过滤
const threshold = parseInt(thresholdInput.value, 10);
const filterThreshold = isNaN(threshold) || threshold < 0 ? 0 : threshold;
@@ -680,7 +702,6 @@ function filterValidKeys() {
// 处理“暂无有效密钥”消息
const noMatchMsgId = 'no-valid-keys-msg';
const validKeysList = document.getElementById('validKeys');
let noMatchMsg = validKeysList.querySelector(`#${noMatchMsgId}`);
const initialKeyCount = validKeysList.querySelectorAll('li[data-key]').length; // 获取初始密钥数量
@@ -703,7 +724,7 @@ function filterValidKeys() {
document.addEventListener('DOMContentLoaded', () => {
// 初始化统计区块动画
initStatItemAnimations();
// 添加数字滚动动画效果
const animateCounters = () => {
const statValues = document.querySelectorAll('.stat-value');
@@ -714,12 +735,12 @@ document.addEventListener('DOMContentLoaded', () => {
if (!valueElement.dataset.originalValue) {
valueElement.dataset.originalValue = valueElement.textContent;
}
// 数字滚动动画
let startValue = 0;
const duration = 1500;
const startTime = performance.now();
const updateCounter = (currentTime) => {
const elapsedTime = currentTime - startTime;
if (elapsedTime < duration) {
@@ -733,37 +754,37 @@ document.addEventListener('DOMContentLoaded', () => {
// 恢复为原始值,以确保准确性
valueElement.textContent = valueElement.dataset.originalValue;
}
};
requestAnimationFrame(updateCounter);
}
});
};
// 在页面加载后启动数字动画
setTimeout(animateCounters, 300);
// 添加卡片悬停效果
document.querySelectorAll('.stats-card').forEach(card => {
card.addEventListener('mouseenter', () => {
card.classList.add('shadow-lg');
card.style.transform = 'translateY(-2px)';
});
card.addEventListener('mouseleave', () => {
card.classList.remove('shadow-lg');
card.style.transform = '';
});
});
// 监听展开/折叠事件 (确保使用正确的选择器和函数)
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')) {
// 确保点击的不是内部交互元素(如输入框、复选框、标签、按钮、选择框
if (event.target.closest('input, label, button, select')) {
return;
}
// 从 header 中提取 sectionId (例如从关联的 content div 的 id)
@@ -784,12 +805,12 @@ document.addEventListener('DOMContentLoaded', () => {
if (thresholdInput) {
// 使用 'input' 事件实时响应输入变化
thresholdInput.addEventListener('input', filterValidKeys);
// 初始加载时应用一次筛选
filterValidKeys();
// 初始加载时应用一次筛选 (现在由 pagination/search 初始化处理)
// filterValidKeys();
}
// --- 批量验证相关函数 (明确挂载到 window) ---
// 显示验证确认模态框 (基于选中的密钥)
window.showVerifyModal = function(type, event) {
// 阻止事件冒泡(如果从按钮点击触发)
@@ -822,32 +843,41 @@ document.addEventListener('DOMContentLoaded', () => {
// 显示模态框
modalElement.classList.remove('hidden');
}
window.closeVerifyModal = function() {
document.getElementById('verifyModal').classList.add('hidden');
}
window.executeVerifyAll = async function(type) {
try {
// 关闭确认模态框
closeVerifyModal();
// 找到对应的验证按钮以显示加载状态 (需要给按钮添加 data-verify-type 属性)
// 或者,我们可以暂时禁用所有按钮或显示一个全局加载指示器
// 这里我们暂时只记录日志实际UI反馈可以后续增强
console.log(`Starting bulk verification for ${type} keys...`);
// 找到对应的验证按钮以显示加载状态
const verifyButton = document.querySelector(`#${type}BatchActions button:nth-child(1)`); // Assuming verify is the first button
let originalVerifyHtml = '';
if (verifyButton) {
originalVerifyHtml = verifyButton.innerHTML;
verifyButton.disabled = true;
verifyButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 验证中';
}
// 获取选中的密钥
const keysToVerify = getSelectedKeys(type);
if (keysToVerify.length === 0) {
showNotification(`没有选中的${type === 'valid' ? '有效' : '无效'}密钥可验证`, 'warning');
if (verifyButton) { // Restore button if no keys selected
verifyButton.innerHTML = originalVerifyHtml;
// Button disable state will be handled by updateBatchActions after reload or modal close
}
return;
}
// 显示一个通用的加载提示
showNotification('开始批量验证,请稍候...', 'info');
// 调用新的后端 API 来验证选定的密钥
const response = await fetch(`/gemini/v1beta/verify-selected-keys`, { // 假设的新 API 端点
method: 'POST',
@@ -856,7 +886,7 @@ document.addEventListener('DOMContentLoaded', () => {
},
body: JSON.stringify({ keys: keysToVerify }) // 只发送密钥列表
});
if (!response.ok) {
let errorMsg = `服务器返回错误: ${response.status}`;
try {
@@ -865,37 +895,44 @@ document.addEventListener('DOMContentLoaded', () => {
} catch (e) { /*忽略解析错误*/ }
throw new Error(errorMsg);
}
const data = await response.json();
// 使用新的专用模态框显示结果
showVerificationResultModal(data);
// 注意autoReload 逻辑已移至 showVerificationResultModal 内部
// 注意autoReload 逻辑已移至 showVerificationResultModal 内部 (现在总是刷新)
} catch (error) {
console.error('批量验证处理失败:', error);
// 失败后不自动刷新
showResultModal(false, '批量验证处理失败: ' + error.message, false);
// 失败后也刷新页面,让用户看到可能更新的状态
showResultModal(false, '批量验证处理失败: ' + error.message, true);
} finally {
// 可以在这里移除加载指示器
console.log("Bulk verification process finished.");
// Button state will be reset on page reload
}
}
// --- 复选框事件监听 ---
document.querySelectorAll('.key-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', (event) => {
const type = event.target.dataset.keyType;
updateBatchActions(type);
});
// Attach listeners dynamically after pagination renders content, or use event delegation
document.getElementById('validKeys').addEventListener('change', (event) => {
if (event.target.classList.contains('key-checkbox')) {
updateBatchActions('valid');
}
});
document.getElementById('invalidKeys').addEventListener('change', (event) => {
if (event.target.classList.contains('key-checkbox')) {
updateBatchActions('invalid');
}
});
// 初始化批量操作区域状态
updateBatchActions('valid');
updateBatchActions('invalid');
// 初始化批量操作区域状态 (在 pagination 初始化后进行)
// updateBatchActions('valid'); // Called by displayPage
// updateBatchActions('invalid'); // Called by displayPage
// --- 滚动和页面控制 ---
// --- 滚动和页面控制 --- (Scroll buttons handled by base.html)
// --- 自动刷新控制 ---
const autoRefreshToggle = document.getElementById('autoRefreshToggle');
const autoRefreshIntervalTime = 60000; // 60秒
@@ -904,6 +941,7 @@ document.addEventListener('DOMContentLoaded', () => {
function startAutoRefresh() {
if (autoRefreshTimer) return; // 防止重复启动
console.log('启动自动刷新...');
showNotification('自动刷新已启动', 'info', 2000);
autoRefreshTimer = setInterval(() => {
console.log('自动刷新 keys_status 页面...');
location.reload();
@@ -913,6 +951,7 @@ document.addEventListener('DOMContentLoaded', () => {
function stopAutoRefresh() {
if (autoRefreshTimer) {
console.log('停止自动刷新...');
showNotification('自动刷新已停止', 'info', 2000);
clearInterval(autoRefreshTimer);
autoRefreshTimer = null;
}
@@ -937,6 +976,60 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
}
// --- Pagination and Search Initialization ---
// This part needs to be integrated with the pagination logic from the provided file content
// Assuming the pagination/search related code from the file_content is now part of this script
// --- Get DOM Elements for Pagination/Search ---
const validKeysListElement = document.getElementById('validKeys');
const invalidKeysListElement = document.getElementById('invalidKeys');
// const thresholdInput = document.getElementById('failCountThreshold'); // Already defined
const searchInput = document.getElementById('keySearchInput');
const itemsPerPageSelect = document.getElementById('itemsPerPageSelect');
// --- Store Initial Key Data ---
if (validKeysListElement) {
allValidKeys = Array.from(validKeysListElement.querySelectorAll('li[data-key]'));
allValidKeys.forEach(li => {
const keyTextSpan = li.querySelector('.key-text');
if (keyTextSpan && keyTextSpan.dataset.fullKey) {
li.dataset.key = keyTextSpan.dataset.fullKey; // Ensure li has full key for search
}
});
filteredValidKeys = [...allValidKeys]; // Start with all keys
}
if (invalidKeysListElement) {
allInvalidKeys = Array.from(invalidKeysListElement.querySelectorAll('li[data-key]'));
allInvalidKeys.forEach(li => {
const keyTextSpan = li.querySelector('.key-text');
if (keyTextSpan && keyTextSpan.dataset.fullKey) {
li.dataset.key = keyTextSpan.dataset.fullKey;
}
});
}
// --- Initial Display ---
if (itemsPerPageSelect) {
itemsPerPage = parseInt(itemsPerPageSelect.value, 10);
}
filterAndSearchValidKeys(); // This applies initial filter/search and calls displayPage('valid', 1, ...)
displayPage('invalid', 1, allInvalidKeys); // Display first page of invalid keys
// --- Event Listeners for Pagination/Search ---
if (thresholdInput) {
thresholdInput.addEventListener('input', filterAndSearchValidKeys);
}
if (searchInput) {
searchInput.addEventListener('input', filterAndSearchValidKeys);
}
if (itemsPerPageSelect) {
itemsPerPageSelect.addEventListener('change', () => {
itemsPerPage = parseInt(itemsPerPageSelect.value, 10);
filterAndSearchValidKeys(); // Re-filter and display page 1
});
}
});
// Service Worker registration
@@ -1007,7 +1100,12 @@ async function showApiCallDetails(period) {
// 调用后端 API 获取数据
const response = await fetch(`/api/stats/details?period=${period}`);
if (!response.ok) {
throw new Error(`服务器错误: ${response.status}`);
let errorMsg = `服务器错误: ${response.status}`;
try {
const errorData = await response.json();
errorMsg = errorData.detail || errorMsg;
} catch (e) { /* 忽略解析错误 */ }
throw new Error(errorMsg);
}
const data = await response.json();
@@ -1190,3 +1288,195 @@ window.renderKeyUsageDetails = function(data, container) {
container.innerHTML = tableHtml;
}
// --- Global Variables for Pagination ---
let itemsPerPage = 10; // Default, will be updated from select
let validCurrentPage = 1;
let invalidCurrentPage = 1;
let allValidKeys = []; // Stores all original valid key li elements
let allInvalidKeys = []; // Stores all original invalid key li elements
let filteredValidKeys = []; // Stores filtered and searched valid key li elements
// --- Key List Display & Pagination ---
/**
* Displays key list items for a specific type and page.
* @param {string} type 'valid' or 'invalid'
* @param {number} page Page number (1-based)
* @param {Array} keyItemsArray The array of li elements to paginate (e.g., filteredValidKeys, allInvalidKeys)
*/
function displayPage(type, page, keyItemsArray) {
const listElement = document.getElementById(`${type}Keys`);
const paginationControls = document.getElementById(`${type}PaginationControls`);
if (!listElement || !paginationControls) return;
// Update current page based on type
if (type === 'valid') {
validCurrentPage = page;
// Read itemsPerPage from the select specifically for valid keys
const itemsPerPageSelect = document.getElementById('itemsPerPageSelect');
itemsPerPage = itemsPerPageSelect ? parseInt(itemsPerPageSelect.value, 10) : 10;
} else {
invalidCurrentPage = page;
// For invalid keys, use a fixed itemsPerPage or the same global one
// itemsPerPage = 10; // Or read from a different select if needed
}
const totalItems = keyItemsArray.length;
const totalPages = Math.ceil(totalItems / itemsPerPage);
page = Math.max(1, Math.min(page, totalPages || 1)); // Ensure page is valid
// Update current page variable again after validation
if (type === 'valid') {
validCurrentPage = page;
} else {
invalidCurrentPage = page;
}
const startIndex = (page - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
listElement.innerHTML = ''; // Clear current list content
const pageItems = keyItemsArray.slice(startIndex, endIndex);
if (pageItems.length > 0) {
pageItems.forEach(item => listElement.appendChild(item.cloneNode(true))); // Clone node to avoid issues if original array is modified elsewhere
} else if (totalItems === 0 && type === 'valid' && (document.getElementById('failCountThreshold').value !== '0' || document.getElementById('keySearchInput').value !== '')) {
// Handle empty state after filtering/searching for valid keys
const noMatchMsgId = 'no-valid-keys-msg';
let noMatchMsg = listElement.querySelector(`#${noMatchMsgId}`);
if (!noMatchMsg) {
noMatchMsg = document.createElement('li');
noMatchMsg.id = noMatchMsgId;
noMatchMsg.className = 'text-center text-gray-500 py-4 col-span-full';
noMatchMsg.textContent = '没有符合条件的有效密钥';
listElement.appendChild(noMatchMsg);
}
noMatchMsg.style.display = '';
} else if (totalItems === 0) {
// Handle empty state for initially empty lists
const emptyMsg = document.createElement('li');
emptyMsg.className = 'text-center text-gray-500 py-4 col-span-full';
emptyMsg.textContent = `暂无${type === 'valid' ? '有效' : '无效'}密钥`;
listElement.appendChild(emptyMsg);
}
setupPaginationControls(type, page, totalPages, keyItemsArray);
updateBatchActions(type); // Update batch actions based on the currently displayed page
// Re-attach event listeners for buttons inside the newly added list items if needed (using event delegation is better)
}
/**
* Sets up pagination controls.
* @param {string} type 'valid' or 'invalid'
* @param {number} currentPage Current page number
* @param {number} totalPages Total number of pages
* @param {Array} keyItemsArray The array of li elements being paginated
*/
function setupPaginationControls(type, currentPage, totalPages, keyItemsArray) {
const controlsContainer = document.getElementById(`${type}PaginationControls`);
if (!controlsContainer) return;
controlsContainer.innerHTML = '';
if (totalPages <= 1) {
return; // No controls needed for single/no page
}
// Previous Button
const prevButton = document.createElement('button');
prevButton.innerHTML = '<i class="fas fa-chevron-left"></i>';
prevButton.className = 'px-3 py-1 rounded bg-gray-200 hover:bg-gray-300 disabled:opacity-50 disabled:cursor-not-allowed text-sm';
prevButton.disabled = currentPage === 1;
prevButton.onclick = () => displayPage(type, currentPage - 1, keyItemsArray);
controlsContainer.appendChild(prevButton);
// Page Number Buttons (Logic for ellipsis)
const maxPageButtons = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxPageButtons / 2));
let endPage = Math.min(totalPages, startPage + maxPageButtons - 1);
if (endPage - startPage + 1 < maxPageButtons) {
startPage = Math.max(1, endPage - maxPageButtons + 1);
}
// First Page Button & Ellipsis
if (startPage > 1) {
const firstPageButton = document.createElement('button');
firstPageButton.textContent = '1';
firstPageButton.className = 'px-3 py-1 rounded bg-gray-200 hover:bg-gray-300 text-sm';
firstPageButton.onclick = () => displayPage(type, 1, keyItemsArray);
controlsContainer.appendChild(firstPageButton);
if (startPage > 2) {
const ellipsis = document.createElement('span');
ellipsis.textContent = '...';
ellipsis.className = 'px-3 py-1 text-gray-500 text-sm';
controlsContainer.appendChild(ellipsis);
}
}
// Middle Page Buttons
for (let i = startPage; i <= endPage; i++) {
const pageButton = document.createElement('button');
pageButton.textContent = i;
pageButton.className = `px-3 py-1 rounded text-sm ${i === currentPage ? 'bg-primary-600 text-white font-semibold' : 'bg-gray-200 hover:bg-gray-300'}`;
pageButton.onclick = () => displayPage(type, i, keyItemsArray);
controlsContainer.appendChild(pageButton);
}
// Ellipsis & Last Page Button
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
const ellipsis = document.createElement('span');
ellipsis.textContent = '...';
ellipsis.className = 'px-3 py-1 text-gray-500 text-sm';
controlsContainer.appendChild(ellipsis);
}
const lastPageButton = document.createElement('button');
lastPageButton.textContent = totalPages;
lastPageButton.className = 'px-3 py-1 rounded bg-gray-200 hover:bg-gray-300 text-sm';
lastPageButton.onclick = () => displayPage(type, totalPages, keyItemsArray);
controlsContainer.appendChild(lastPageButton);
}
// Next Button
const nextButton = document.createElement('button');
nextButton.innerHTML = '<i class="fas fa-chevron-right"></i>';
nextButton.className = 'px-3 py-1 rounded bg-gray-200 hover:bg-gray-300 disabled:opacity-50 disabled:cursor-not-allowed text-sm';
nextButton.disabled = currentPage === totalPages;
nextButton.onclick = () => displayPage(type, currentPage + 1, keyItemsArray);
controlsContainer.appendChild(nextButton);
}
// --- Filtering & Searching (Valid Keys Only) ---
/**
* Filters and searches the valid keys based on threshold and search term.
* Updates the `filteredValidKeys` array and redisplays the first page.
*/
function filterAndSearchValidKeys() {
const thresholdInput = document.getElementById('failCountThreshold');
const searchInput = document.getElementById('keySearchInput');
const threshold = parseInt(thresholdInput.value, 10);
const filterThreshold = isNaN(threshold) || threshold < 0 ? 0 : threshold;
const searchTerm = searchInput.value.trim().toLowerCase();
// Filter from the original full list (allValidKeys)
filteredValidKeys = allValidKeys.filter(item => {
const failCount = parseInt(item.dataset.failCount, 10);
const fullKey = item.dataset.key || ''; // Use data-key which should hold the full key
const failCountMatch = failCount >= filterThreshold;
const searchMatch = searchTerm === '' || fullKey.toLowerCase().includes(searchTerm);
return failCountMatch && searchMatch;
});
// Reset to the first page after filtering/searching
validCurrentPage = 1;
displayPage('valid', validCurrentPage, filteredValidKeys);
}

View File

@@ -6,12 +6,15 @@
<style>
/* keys_status.html specific styles */
.key-content {
transition: max-height 0.3s ease-in-out, opacity 0.3s ease-in-out;
transition: max-height 0.3s ease-in-out, opacity 0.3s ease-in-out, padding 0.3s ease-in-out; /* Added padding transition */
overflow: hidden; /* Keep hidden initially and during collapse */
}
.key-content.collapsed {
max-height: 0;
overflow: hidden;
max-height: 0 !important; /* Use important to override inline style during transition */
opacity: 0;
padding-top: 0 !important; /* Collapse padding */
padding-bottom: 0 !important; /* Collapse padding */
/* overflow: hidden; */ /* Already set above */
}
.toggle-icon {
transition: transform 0.3s ease;
@@ -30,13 +33,13 @@
position: relative;
z-index: 10;
}
@media (min-width: 768px) {
.stats-dashboard {
grid-template-columns: 1fr 1fr;
}
}
/* 统计卡片样式 */
.stats-card {
background-color: rgba(255, 255, 255, 0.8);
@@ -48,11 +51,11 @@
overflow: hidden;
transition: all 0.3s ease-in-out;
}
.stats-card:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.stats-card-header {
background-color: rgba(255, 255, 255, 0.3);
padding: 0.75rem 1rem;
@@ -60,8 +63,10 @@
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap; /* Allow wrapping for smaller screens */
gap: 0.5rem; /* Add gap between items */
}
.stats-card-title {
display: flex;
align-items: center;
@@ -69,19 +74,19 @@
font-weight: 500;
color: #374151;
}
.stats-card-title i {
margin-right: 0.5rem;
color: #4F46E5;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
padding: 0.75rem;
}
/* 统计项样式 */
.stat-item {
padding: 0.75rem;
@@ -95,7 +100,7 @@
position: relative;
overflow: hidden;
}
.stat-item::before {
content: '';
position: absolute;
@@ -105,15 +110,15 @@
z-index: 0;
transition: opacity 0.3s ease-in-out;
}
.stat-item:hover::before {
opacity: 0.1;
}
.stat-item:hover {
transform: scale(1.05);
}
.stat-value {
font-size: 1.5rem;
font-weight: 700;
@@ -121,7 +126,7 @@
position: relative;
color: #1F2937;
}
.stat-label {
font-size: 0.75rem;
font-weight: 500;
@@ -132,7 +137,7 @@
position: relative;
color: #6B7280;
}
.stat-icon {
position: absolute;
right: 0.5rem;
@@ -142,63 +147,30 @@
transform: rotate(12deg);
transition: all 0.3s ease-in-out;
}
.stat-item:hover .stat-icon {
opacity: 0.2;
transform: scale(1.1) rotate(0deg);
}
/* 统计类型样式 */
.stat-primary {
color: #4F46E5;
background-color: rgba(238, 242, 255, 0.5);
}
.stat-success {
color: #10B981;
background-color: rgba(236, 253, 245, 0.5);
}
.stat-danger {
color: #EF4444;
background-color: rgba(254, 242, 242, 0.5);
}
.stat-warning {
color: #F59E0B;
background-color: rgba(255, 251, 235, 0.5);
}
.stat-info {
color: #3B82F6;
background-color: rgba(239, 246, 255, 0.5);
}
.stat-primary { color: #4F46E5; background-color: rgba(238, 242, 255, 0.5); }
.stat-success { color: #10B981; background-color: rgba(236, 253, 245, 0.5); }
.stat-danger { color: #EF4444; background-color: rgba(254, 242, 242, 0.5); }
.stat-warning { color: #F59E0B; background-color: rgba(255, 251, 235, 0.5); }
.stat-info { color: #3B82F6; background-color: rgba(239, 246, 255, 0.5); }
/* 响应式调整 */
@media (max-width: 640px) {
.stats-dashboard {
gap: 1rem;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
padding: 0.5rem;
}
.stat-item {
padding: 0.5rem;
}
.stat-value {
font-size: 1.25rem;
}
.stat-label {
font-size: 0.625rem;
}
.stats-dashboard { gap: 1rem; }
.stats-grid { grid-template-columns: repeat(2, 1fr); gap: 0.5rem; padding: 0.5rem; }
.stat-item { padding: 0.5rem; }
.stat-value { font-size: 1.25rem; }
.stat-label { font-size: 0.625rem; }
.stats-card-header { padding: 0.5rem 0.75rem; } /* Adjust header padding */
.key-content ul { grid-template-columns: 1fr; } /* Stack keys vertically on small screens */
}
/* Tailwind Toggle Switch Helper CSS from config_editor.html */
/* Tailwind Toggle Switch Helper CSS */
.toggle-checkbox:checked {
@apply: right-0 border-primary-600;
right: 0;
@@ -209,6 +181,34 @@
background-color: #4F46E5;
}
/* Pagination Controls */
#validPaginationControls, #invalidPaginationControls {
display: flex;
justify-content: center;
align-items: center;
margin-top: 1rem; /* mt-4 */
gap: 0.5rem; /* space-x-2 */
}
/* Ensure list items are flex for alignment */
#validKeys li, #invalidKeys li {
display: flex;
align-items: flex-start; /* Align checkbox with top of content */
gap: 0.75rem; /* gap-3 */
}
/* Ensure grid layout for key lists */
#validKeys, #invalidKeys {
display: grid;
grid-template-columns: 1fr; /* Default single column */
gap: 0.75rem; /* gap-3 */
}
@media (min-width: 768px) { /* md breakpoint */
#validKeys, #invalidKeys {
grid-template-columns: repeat(2, 1fr); /* Two columns on medium screens and up */
}
}
</style>
{% endblock %}
@@ -282,7 +282,7 @@
</div>
</div>
</div>
<!-- API调用统计卡片 -->
<div class="stats-card">
<div class="stats-card-header">
@@ -311,31 +311,50 @@
</div>
</div>
</div>
<!-- 有效密钥区域 -->
<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 flex-grow"> <!-- Added flex-grow -->
<!-- Left side: Title and Toggle Icon -->
<div class="flex items-center gap-3 flex-shrink-0"> <!-- Prevent shrinking -->
<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>
<div class="flex items-center gap-2 ml-4">
<label for="failCountThreshold" class="text-sm text-gray-600 select-none">失败次数≥</label>
<h2 class="text-lg font-semibold whitespace-nowrap">有效密钥列表 ({{ valid_key_count }})</h2>
</div>
<!-- Middle: Filters and Search (Allow wrapping) -->
<div class="flex items-center gap-x-4 gap-y-2 flex-grow flex-wrap justify-start md:justify-center"> <!-- Allow wrapping, center on medium+ -->
<!-- 失败次数筛选 -->
<div class="flex items-center gap-1">
<label for="failCountThreshold" class="text-sm text-gray-600 select-none whitespace-nowrap">失败次数≥</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 class="flex items-center gap-1">
<label for="keySearchInput" class="text-sm text-gray-600 select-none whitespace-nowrap"><i class="fas fa-search mr-1"></i>搜索</label>
<input type="search" id="keySearchInput" placeholder="输入密钥..." class="form-input h-7 w-32 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">
<label for="itemsPerPageSelect" class="text-sm text-gray-600 select-none whitespace-nowrap">每页</label>
<select id="itemsPerPageSelect" class="form-select h-7 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-primary-500 focus:border-primary-500 bg-white" onclick="event.stopPropagation();">
<option value="10">10</option>
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
<span class="text-sm text-gray-600 select-none"></span>
</div>
</div>
<!-- 原始批量操作按钮(可选保留或移除) -->
<!-- <div class="flex gap-2"> ... </div> -->
<!-- Right side: Select All -->
<div class="flex items-center gap-1 flex-shrink-0" onclick="event.stopPropagation();"> <!-- Prevent shrinking -->
<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 whitespace-nowrap">全选</label>
</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>
<div id="validBatchActions" class="p-3 bg-gray-50 border-t border-gray-200 hidden flex items-center flex-wrap gap-3"> <!-- Added flex-wrap -->
<span class="text-sm font-medium text-gray-700 whitespace-nowrap">已选择 <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>
@@ -348,14 +367,17 @@
</div>
<div class="key-content p-4 bg-white bg-opacity-40">
<!-- Key list will be populated by JS -->
<ul id="validKeys" class="grid grid-cols-1 md:grid-cols-2 gap-3">
{# Initial keys rendered by server-side for non-JS users or initial load #}
{# JS will replace this content with paginated/filtered results #}
{% 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 flex items-start gap-3" data-fail-count="{{ fail_count }}" data-key="{{ key }}"> <!-- Added flex, items-start, gap, data-key -->
<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 }}" data-key="{{ 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')">
<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 }}">
<!-- Key Info -->
<div class="flex-grow"> <!-- Added flex-grow -->
<div class="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">
@@ -363,7 +385,7 @@
</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)">
<button class="text-gray-500 hover:text-primary-600 transition-colors" onclick="toggleKeyVisibility(this)" title="显示/隐藏密钥">
<i class="fas fa-eye"></i>
</button>
</div>
@@ -393,34 +415,37 @@
</div>
</div>
</li>
{% endfor %}
{% endfor %}
{% else %}
<li class="text-center text-gray-500 py-4 col-span-full">暂无有效密钥</li> <!-- Added col-span-full -->
<li class="text-center text-gray-500 py-4 col-span-full">暂无有效密钥</li>
{% endif %}
</ul>
<!-- 有效密钥分页控件容器 -->
<div id="validPaginationControls" class="flex justify-center items-center mt-4 space-x-2">
<!-- Pagination controls will be generated by JS -->
</div>
</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 flex-grow"> <!-- Added flex-grow -->
<!-- Left side: Title and Toggle Icon -->
<div class="flex items-center gap-3 flex-shrink-0"> <!-- Prevent shrinking -->
<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>
<h2 class="text-lg font-semibold whitespace-nowrap">无效密钥列表 ({{ invalid_key_count }})</h2>
</div>
<!-- Right side: Select All -->
<div class="flex items-center gap-1 ml-auto flex-shrink-0" onclick="event.stopPropagation();"> <!-- Use ml-auto, Prevent shrinking -->
<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 whitespace-nowrap">全选</label>
</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>
<div id="invalidBatchActions" class="p-3 bg-gray-50 border-t border-gray-200 hidden flex items-center flex-wrap gap-3"> <!-- Added flex-wrap -->
<span class="text-sm font-medium text-gray-700 whitespace-nowrap">已选择 <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>
@@ -433,14 +458,17 @@
</div>
<div class="key-content p-4 bg-white bg-opacity-40">
<!-- Key list will be populated by JS -->
<ul id="invalidKeys" class="grid grid-cols-1 md:grid-cols-2 gap-3">
{# Initial keys rendered by server-side #}
{# JS will replace this content with paginated results #}
{% 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 flex items-start gap-3" data-key="{{ key }}"> <!-- Added flex, items-start, gap, data-key -->
<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" data-key="{{ 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')">
<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 }}">
<!-- Key Info -->
<div class="flex-grow"> <!-- Added flex-grow -->
<div class="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">
@@ -448,7 +476,7 @@
</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)">
<button class="text-gray-500 hover:text-primary-600 transition-colors" onclick="toggleKeyVisibility(this)" title="显示/隐藏密钥">
<i class="fas fa-eye"></i>
</button>
</div>
@@ -470,7 +498,7 @@
<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 }}')">
<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>
@@ -478,18 +506,22 @@
</div>
</div>
</li>
{% endfor %}
{% endfor %}
{% else %}
<li class="text-center text-gray-500 py-4 col-span-full">暂无无效密钥</li> <!-- Added col-span-full -->
<li class="text-center text-gray-500 py-4 col-span-full">暂无无效密钥</li>
{% endif %}
</ul>
<!-- 无效密钥分页控件容器 -->
<div id="invalidPaginationControls" class="flex justify-center items-center mt-4 space-x-2">
<!-- Pagination controls will be generated by JS -->
</div>
</div>
</div>
<!-- Removed old total keys display -->
</div>
</div>
<!-- Scroll buttons are now in base.html -->
<div class="scroll-buttons">
<button class="scroll-button" onclick="scrollToTop()" title="回到顶部">
@@ -499,7 +531,7 @@
<i class="fas fa-chevron-down"></i>
</button>
</div>
<!-- Notification component is now in base.html (use id="notification") -->
<div id="notification" class="notification"></div>
<!-- 重置确认模态框 -->
@@ -524,7 +556,7 @@
</div>
</div>
</div>
<!-- 验证确认模态框移到 resetModal 外部,避免嵌套导致显示异常 -->
<!-- 验证确认模态框 -->
<div id="verifyModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg p-6 shadow-xl max-w-md w-full animate-fade-in">
<div class="flex items-center justify-between mb-4">
@@ -546,7 +578,7 @@
</div>
</div>
</div>
<!-- 操作结果模态框 -->
<div id="resultModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-2xl p-0 shadow-2xl max-w-lg w-full animate-fade-in border border-gray-200">
@@ -561,7 +593,7 @@
</div>
<div class="px-8 pb-2 w-full">
<div id="resultModalMessage"
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 */
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"
style="font-family: 'JetBrains Mono', 'Fira Mono', 'Consolas', 'monospace';">
<!-- Content is dynamically generated by JS -->
</div>
@@ -621,65 +653,15 @@
</div>
</div>
</div>
<!-- Footer is now in base.html -->
{% endblock %}
{% block body_scripts %}
<script>
// keys_status.html specific JavaScript initialization
document.addEventListener('DOMContentLoaded', () => {
// Filter functionality based on fail count threshold
const thresholdInput = document.getElementById('failCountThreshold');
const validKeysList = document.getElementById('validKeys');
function filterValidKeys() {
const threshold = parseInt(thresholdInput.value, 10);
if (isNaN(threshold)) return; // Do nothing if input is not a number
const keys = validKeysList.querySelectorAll('li');
let visibleCount = 0;
keys.forEach(keyItem => {
// Check if it's a key item (has data-fail-count) before processing
if (keyItem.hasAttribute('data-fail-count')) {
const failCount = parseInt(keyItem.getAttribute('data-fail-count'), 10);
if (failCount >= threshold) {
keyItem.style.display = ''; // Show item
visibleCount++;
} else {
keyItem.style.display = 'none'; // Hide item
}
}
});
// Optional: Show a message if no keys match the filter
const noMatchMsgId = 'no-valid-keys-msg';
let noMatchMsg = validKeysList.querySelector(`#${noMatchMsgId}`);
if (visibleCount === 0 && keys.length > 0) { // Only show if there were keys initially
if (!noMatchMsg) {
noMatchMsg = document.createElement('li');
noMatchMsg.id = noMatchMsgId;
noMatchMsg.className = 'text-center text-gray-500 py-4';
noMatchMsg.textContent = '没有符合条件的有效密钥';
validKeysList.appendChild(noMatchMsg);
}
noMatchMsg.style.display = '';
} else if (noMatchMsg) {
noMatchMsg.style.display = 'none';
}
}
if (thresholdInput && validKeysList) {
thresholdInput.addEventListener('input', filterValidKeys);
// Initial filter on load
filterValidKeys();
}
// 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
// The toggleSection logic is now correctly handled by keys_status.js
// Removed duplicate inline definition
});
// keys_status.html specific JavaScript initialization is now handled by keys_status.js
// The DOMContentLoaded listener in keys_status.js will execute after the DOM is ready.
// No inline script needed here anymore.
</script>
{% endblock %}