mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-05-06 20:32:47 +08:00
feat: 添加下拉菜单和批量操作功能
- 新增 /api/keys/all 端点获取所有密钥 - 添加下拉菜单界面,提供复制全部密钥和验证所有密钥选项 - 重构批量重置逻辑,改为逐个处理以提供更好的进度反馈 - 新增批量操作进度模态框,实时显示操作状态和日志 - 在验证模态框中添加批次大小配置选项 - 优化用户体验,提供更直观的批量操作界面
This commit is contained in:
@@ -61,3 +61,23 @@ async def get_keys_paginated(
|
||||
"total_pages": (total_items + limit - 1) // limit,
|
||||
"current_page": page,
|
||||
}
|
||||
|
||||
@router.get("/api/keys/all")
|
||||
async def get_all_keys(
|
||||
request: Request,
|
||||
key_manager: KeyManager = Depends(get_key_manager_instance),
|
||||
):
|
||||
"""
|
||||
Get all keys (both valid and invalid) for bulk operations.
|
||||
"""
|
||||
auth_token = request.cookies.get("auth_token")
|
||||
if not auth_token or not verify_auth_token(auth_token):
|
||||
return JSONResponse(status_code=401, content={"detail": "Unauthorized"})
|
||||
|
||||
all_keys_with_status = await key_manager.get_all_keys_with_fail_count()
|
||||
|
||||
return {
|
||||
"valid_keys": list(all_keys_with_status["valid_keys"].keys()),
|
||||
"invalid_keys": list(all_keys_with_status["invalid_keys"].keys()),
|
||||
"total_count": len(all_keys_with_status["valid_keys"]) + len(all_keys_with_status["invalid_keys"])
|
||||
}
|
||||
|
||||
@@ -602,82 +602,50 @@ function showVerificationResultModal(data) {
|
||||
}
|
||||
|
||||
async function executeResetAll(type) {
|
||||
try {
|
||||
// 关闭确认模态框
|
||||
closeResetModal();
|
||||
closeResetModal();
|
||||
const keysToReset = getSelectedKeys(type);
|
||||
if (keysToReset.length === 0) {
|
||||
showNotification("没有选中的密钥可重置", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
// 找到对应的重置按钮以显示加载状态
|
||||
const resetButton = document.querySelector(
|
||||
`button[data-reset-type="${type}"]`
|
||||
);
|
||||
if (!resetButton) {
|
||||
showResultModal(
|
||||
false,
|
||||
`找不到${type === "valid" ? "有效" : "无效"}密钥区域的批量重置按钮`,
|
||||
false
|
||||
); // Don't reload if button not found
|
||||
return;
|
||||
}
|
||||
showProgressModal(`批量重置 ${keysToReset.length} 个密钥的失败计数`);
|
||||
|
||||
// 获取选中的密钥
|
||||
const keysToReset = getSelectedKeys(type);
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
if (keysToReset.length === 0) {
|
||||
showNotification(
|
||||
`没有选中的${type === "valid" ? "有效" : "无效"}密钥可重置`,
|
||||
"warning"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用按钮并显示加载状态
|
||||
resetButton.disabled = true;
|
||||
const originalHtml = resetButton.innerHTML;
|
||||
resetButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 重置中';
|
||||
for (let i = 0; i < keysToReset.length; i++) {
|
||||
const key = keysToReset[i];
|
||||
const keyDisplay = `${key.substring(0, 4)}...${key.substring(
|
||||
key.length - 4
|
||||
)}`;
|
||||
updateProgress(i, keysToReset.length, `正在重置: ${keyDisplay}`);
|
||||
|
||||
try {
|
||||
const options = {
|
||||
const data = await fetchAPI(`/gemini/v1beta/reset-fail-count/${key}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ keys: keysToReset, key_type: type }),
|
||||
};
|
||||
const data = await fetchAPI(
|
||||
`/gemini/v1beta/reset-selected-fail-counts`,
|
||||
options
|
||||
);
|
||||
|
||||
// 根据重置结果显示模态框
|
||||
});
|
||||
if (data.success) {
|
||||
const message =
|
||||
data.reset_count !== undefined
|
||||
? `成功重置 ${data.reset_count} 个选中的${
|
||||
type === "valid" ? "有效" : "无效"
|
||||
}密钥的失败次数`
|
||||
: `成功重置 ${keysToReset.length} 个选中的密钥`;
|
||||
showResultModal(true, message); // 成功后刷新页面
|
||||
successCount++;
|
||||
addProgressLog(`✅ ${keyDisplay}: 重置成功`);
|
||||
} else {
|
||||
const errorMsg = data.message || "批量重置失败";
|
||||
// 失败后不自动刷新页面,让用户看到错误信息
|
||||
showResultModal(false, "批量重置失败: " + errorMsg, false);
|
||||
failCount++;
|
||||
addProgressLog(
|
||||
`❌ ${keyDisplay}: 重置失败 - ${data.message || "未知错误"}`,
|
||||
true
|
||||
);
|
||||
}
|
||||
} catch (apiError) {
|
||||
console.error("批量重置 API 请求失败:", apiError);
|
||||
showResultModal(false, `批量重置请求失败: ${apiError.message}`, false);
|
||||
} finally {
|
||||
// 恢复按钮状态 (仅在不刷新的情况下)
|
||||
if (
|
||||
!document.getElementById("resultModal") ||
|
||||
document.getElementById("resultModal").classList.contains("hidden") ||
|
||||
document.getElementById("resultModalTitle").textContent.includes("失败")
|
||||
) {
|
||||
resetButton.innerHTML = originalHtml;
|
||||
resetButton.disabled = false;
|
||||
}
|
||||
failCount++;
|
||||
addProgressLog(`❌ ${keyDisplay}: 请求失败 - ${apiError.message}`, true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("批量重置处理失败:", error);
|
||||
showResultModal(false, "批量重置处理失败: " + error.message, false); // 失败后不自动刷新
|
||||
}
|
||||
|
||||
updateProgress(
|
||||
keysToReset.length,
|
||||
keysToReset.length,
|
||||
`重置完成!成功: ${successCount}, 失败: ${failCount}`
|
||||
);
|
||||
}
|
||||
|
||||
function scrollToTop() {
|
||||
@@ -929,58 +897,138 @@ function initializeGlobalBatchVerificationHandlers() {
|
||||
|
||||
// executeVerifyAll 变为 initializeGlobalBatchVerificationHandlers 的局部函数
|
||||
async function executeVerifyAll(type) {
|
||||
// Removed window.
|
||||
try {
|
||||
window.closeVerifyModal(); // Calls the global close function, which is fine.
|
||||
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;
|
||||
}
|
||||
return;
|
||||
}
|
||||
showNotification("开始批量验证,请稍候...", "info");
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ keys: keysToVerify }),
|
||||
};
|
||||
const data = await fetchAPI(
|
||||
`/gemini/v1beta/verify-selected-keys`,
|
||||
options
|
||||
);
|
||||
if (data) {
|
||||
showVerificationResultModal(data);
|
||||
} else {
|
||||
throw new Error("API did not return verification data.");
|
||||
}
|
||||
} catch (apiError) {
|
||||
console.error("批量验证处理失败:", apiError);
|
||||
showResultModal(false, `批量验证处理失败: ${apiError.message}`, true);
|
||||
} finally {
|
||||
console.log("Bulk verification process finished.");
|
||||
// Button state will be reset on page reload or by updateBatchActions
|
||||
closeVerifyModal();
|
||||
const keysToVerify = getSelectedKeys(type);
|
||||
if (keysToVerify.length === 0) {
|
||||
showNotification("没有选中的密钥可验证", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
const batchSizeInput = document.getElementById("batchSize");
|
||||
const batchSize = parseInt(batchSizeInput.value, 10) || 10;
|
||||
|
||||
showProgressModal(`批量验证 ${keysToVerify.length} 个密钥`);
|
||||
|
||||
let allSuccessfulKeys = [];
|
||||
let allFailedKeys = {};
|
||||
let processedCount = 0;
|
||||
|
||||
for (let i = 0; i < keysToVerify.length; i += batchSize) {
|
||||
const batch = keysToVerify.slice(i, i + batchSize);
|
||||
const progressText = `正在验证批次 ${Math.floor(i / batchSize) + 1} / ${Math.ceil(keysToVerify.length / batchSize)} (密钥 ${i + 1}-${Math.min(i + batchSize, keysToVerify.length)})`;
|
||||
|
||||
updateProgress(i, keysToVerify.length, progressText);
|
||||
addProgressLog(`处理批次: ${batch.length}个密钥...`);
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ keys: batch }),
|
||||
};
|
||||
const data = await fetchAPI(`/gemini/v1beta/verify-selected-keys`, options);
|
||||
|
||||
if (data) {
|
||||
if (data.successful_keys && data.successful_keys.length > 0) {
|
||||
allSuccessfulKeys = allSuccessfulKeys.concat(data.successful_keys);
|
||||
addProgressLog(`✅ 批次成功: ${data.successful_keys.length} 个`);
|
||||
}
|
||||
if (data.failed_keys && Object.keys(data.failed_keys).length > 0) {
|
||||
Object.assign(allFailedKeys, data.failed_keys);
|
||||
addProgressLog(`❌ 批次失败: ${Object.keys(data.failed_keys).length} 个`, true);
|
||||
}
|
||||
} else {
|
||||
addProgressLog(`- 批次返回空数据`, true);
|
||||
}
|
||||
} catch (apiError) {
|
||||
addProgressLog(`❌ 批次请求失败: ${apiError.message}`, true);
|
||||
// Mark all keys in this batch as failed due to API error
|
||||
batch.forEach(key => {
|
||||
allFailedKeys[key] = apiError.message;
|
||||
});
|
||||
}
|
||||
processedCount += batch.length;
|
||||
updateProgress(processedCount, keysToVerify.length, progressText);
|
||||
}
|
||||
|
||||
updateProgress(
|
||||
keysToVerify.length,
|
||||
keysToVerify.length,
|
||||
`所有批次验证完成!`
|
||||
);
|
||||
|
||||
// Close progress modal and show final results
|
||||
closeProgressModal(false); // Don't reload yet
|
||||
showVerificationResultModal({
|
||||
successful_keys: allSuccessfulKeys,
|
||||
failed_keys: allFailedKeys,
|
||||
valid_count: allSuccessfulKeys.length,
|
||||
invalid_count: Object.keys(allFailedKeys).length
|
||||
});
|
||||
}
|
||||
// The confirmButton.onclick in showVerifyModal (defined earlier in initializeGlobalBatchVerificationHandlers)
|
||||
// will correctly reference this local executeVerifyAll due to closure.
|
||||
}
|
||||
|
||||
// --- 进度条模态框函数 ---
|
||||
function showProgressModal(title) {
|
||||
const modal = document.getElementById("progressModal");
|
||||
const titleElement = document.getElementById("progressModalTitle");
|
||||
const statusText = document.getElementById("progressStatusText");
|
||||
const progressBar = document.getElementById("progressBar");
|
||||
const progressPercentage = document.getElementById("progressPercentage");
|
||||
const progressLog = document.getElementById("progressLog");
|
||||
const closeButton = document.getElementById("progressModalCloseBtn");
|
||||
const closeIcon = document.getElementById("closeProgressModalBtn");
|
||||
|
||||
titleElement.textContent = title;
|
||||
statusText.textContent = "准备开始...";
|
||||
progressBar.style.width = "0%";
|
||||
progressPercentage.textContent = "0%";
|
||||
progressLog.innerHTML = "";
|
||||
closeButton.disabled = true;
|
||||
closeIcon.disabled = true;
|
||||
|
||||
modal.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function updateProgress(processed, total, status) {
|
||||
const progressBar = document.getElementById("progressBar");
|
||||
const progressPercentage = document.getElementById("progressPercentage");
|
||||
const statusText = document.getElementById("progressStatusText");
|
||||
const closeButton = document.getElementById("progressModalCloseBtn");
|
||||
const closeIcon = document.getElementById("closeProgressModalBtn");
|
||||
|
||||
const percentage = total > 0 ? Math.round((processed / total) * 100) : 0;
|
||||
progressBar.style.width = `${percentage}%`;
|
||||
progressPercentage.textContent = `${percentage}%`;
|
||||
statusText.textContent = status;
|
||||
|
||||
if (processed === total) {
|
||||
closeButton.disabled = false;
|
||||
closeIcon.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function addProgressLog(message, isError = false) {
|
||||
const progressLog = document.getElementById("progressLog");
|
||||
const logEntry = document.createElement("div");
|
||||
logEntry.textContent = message;
|
||||
logEntry.className = isError
|
||||
? "text-danger-600"
|
||||
: "text-gray-700";
|
||||
progressLog.appendChild(logEntry);
|
||||
progressLog.scrollTop = progressLog.scrollHeight; // Auto-scroll to bottom
|
||||
}
|
||||
|
||||
function closeProgressModal(reload = false) {
|
||||
const modal = document.getElementById("progressModal");
|
||||
modal.classList.add("hidden");
|
||||
if (reload) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
function initializeKeySelectionListeners() {
|
||||
const setupEventListenersForList = (listId, keyType) => {
|
||||
const listElement = document.getElementById(listId);
|
||||
@@ -1309,6 +1357,25 @@ function registerServiceWorker() {
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化下拉菜单
|
||||
function initializeDropdownMenu() {
|
||||
// 阻止下拉菜单按钮的点击事件冒泡
|
||||
const dropdownButton = document.getElementById('dropdownMenuButton');
|
||||
if (dropdownButton) {
|
||||
dropdownButton.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
// 阻止下拉菜单内部点击事件冒泡
|
||||
const dropdownMenu = document.getElementById('dropdownMenu');
|
||||
if (dropdownMenu) {
|
||||
dropdownMenu.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initializePageAnimationsAndEffects();
|
||||
@@ -1319,6 +1386,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
initializeAutoRefreshControls();
|
||||
initializeKeyPaginationAndSearch(); // This will also handle initial display
|
||||
registerServiceWorker();
|
||||
initializeDropdownMenu(); // 初始化下拉菜单
|
||||
|
||||
// Initial batch actions update might be needed if not covered by displayPage
|
||||
// updateBatchActions('valid');
|
||||
@@ -1896,4 +1964,180 @@ function setupPaginationControls(type, currentPage, totalPages) {
|
||||
*/
|
||||
function filterAndSearchValidKeys() {
|
||||
fetchAndDisplayKeys('valid', 1);
|
||||
}
|
||||
|
||||
// --- 下拉菜单功能 ---
|
||||
|
||||
// 切换下拉菜单显示/隐藏
|
||||
window.toggleDropdownMenu = function() {
|
||||
const dropdownMenu = document.getElementById('dropdownMenu');
|
||||
const isVisible = dropdownMenu.classList.contains('show');
|
||||
|
||||
if (isVisible) {
|
||||
hideDropdownMenu();
|
||||
} else {
|
||||
showDropdownMenu();
|
||||
}
|
||||
}
|
||||
|
||||
// 显示下拉菜单
|
||||
function showDropdownMenu() {
|
||||
const dropdownMenu = document.getElementById('dropdownMenu');
|
||||
dropdownMenu.classList.add('show');
|
||||
|
||||
// 点击其他地方时隐藏菜单
|
||||
document.addEventListener('click', handleOutsideClick);
|
||||
}
|
||||
|
||||
// 隐藏下拉菜单
|
||||
function hideDropdownMenu() {
|
||||
const dropdownMenu = document.getElementById('dropdownMenu');
|
||||
dropdownMenu.classList.remove('show');
|
||||
|
||||
// 移除事件监听器
|
||||
document.removeEventListener('click', handleOutsideClick);
|
||||
}
|
||||
|
||||
// 处理点击菜单外部区域
|
||||
function handleOutsideClick(event) {
|
||||
const dropdownToggle = document.querySelector('.dropdown-toggle');
|
||||
if (!dropdownToggle.contains(event.target)) {
|
||||
hideDropdownMenu();
|
||||
}
|
||||
}
|
||||
|
||||
// 复制全部密钥
|
||||
async function copyAllKeys() {
|
||||
hideDropdownMenu();
|
||||
|
||||
try {
|
||||
// 获取所有密钥(有效和无效)
|
||||
const response = await fetchAPI('/api/keys/all');
|
||||
|
||||
const allKeys = [...response.valid_keys, ...response.invalid_keys];
|
||||
|
||||
if (allKeys.length === 0) {
|
||||
showNotification("没有找到任何密钥", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
const keysText = allKeys.join('\n');
|
||||
await copyToClipboard(keysText);
|
||||
showNotification(`已成功复制 ${allKeys.length} 个密钥到剪贴板`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('复制全部密钥失败:', error);
|
||||
showNotification(`复制失败: ${error.message}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
// 验证所有密钥
|
||||
window.verifyAllKeys = async function() {
|
||||
hideDropdownMenu();
|
||||
|
||||
try {
|
||||
// 获取所有密钥(有效和无效)
|
||||
const response = await fetchAPI('/api/keys/all');
|
||||
|
||||
const allKeys = [...response.valid_keys, ...response.invalid_keys];
|
||||
|
||||
if (allKeys.length === 0) {
|
||||
showNotification("没有找到任何密钥可验证", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用验证模态框显示确认对话框
|
||||
showVerifyModalForAllKeys(allKeys);
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取所有密钥失败:', error);
|
||||
showNotification(`获取密钥失败: ${error.message}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
// 显示验证所有密钥的模态框
|
||||
function showVerifyModalForAllKeys(allKeys) {
|
||||
const modalElement = document.getElementById("verifyModal");
|
||||
const titleElement = document.getElementById("verifyModalTitle");
|
||||
const messageElement = document.getElementById("verifyModalMessage");
|
||||
const confirmButton = document.getElementById("confirmVerifyBtn");
|
||||
|
||||
titleElement.textContent = "批量验证所有密钥";
|
||||
messageElement.textContent = `确定要验证所有 ${allKeys.length} 个密钥吗?此操作可能需要较长时间。`;
|
||||
confirmButton.disabled = false;
|
||||
|
||||
// 设置确认按钮事件
|
||||
confirmButton.onclick = () => executeVerifyAllKeys(allKeys);
|
||||
|
||||
// 显示模态框
|
||||
modalElement.classList.remove("hidden");
|
||||
}
|
||||
|
||||
// 执行验证所有密钥
|
||||
async function executeVerifyAllKeys(allKeys) {
|
||||
closeVerifyModal();
|
||||
|
||||
// 获取批次大小
|
||||
const batchSizeInput = document.getElementById("batchSize");
|
||||
const batchSize = parseInt(batchSizeInput.value, 10) || 10;
|
||||
|
||||
// 开始批量验证
|
||||
showProgressModal(`批量验证所有 ${allKeys.length} 个密钥`);
|
||||
|
||||
let allSuccessfulKeys = [];
|
||||
let allFailedKeys = {};
|
||||
let processedCount = 0;
|
||||
|
||||
for (let i = 0; i < allKeys.length; i += batchSize) {
|
||||
const batch = allKeys.slice(i, i + batchSize);
|
||||
const progressText = `正在验证批次 ${Math.floor(i / batchSize) + 1} / ${Math.ceil(allKeys.length / batchSize)} (密钥 ${i + 1}-${Math.min(i + batchSize, allKeys.length)})`;
|
||||
|
||||
updateProgress(i, allKeys.length, progressText);
|
||||
addProgressLog(`处理批次: ${batch.length}个密钥...`);
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ keys: batch }),
|
||||
};
|
||||
const data = await fetchAPI(`/gemini/v1beta/verify-selected-keys`, options);
|
||||
|
||||
if (data) {
|
||||
if (data.successful_keys && data.successful_keys.length > 0) {
|
||||
allSuccessfulKeys = allSuccessfulKeys.concat(data.successful_keys);
|
||||
addProgressLog(`✅ 批次成功: ${data.successful_keys.length} 个`);
|
||||
}
|
||||
if (data.failed_keys && Object.keys(data.failed_keys).length > 0) {
|
||||
Object.assign(allFailedKeys, data.failed_keys);
|
||||
addProgressLog(`❌ 批次失败: ${Object.keys(data.failed_keys).length} 个`, true);
|
||||
}
|
||||
} else {
|
||||
addProgressLog(`- 批次返回空数据`, true);
|
||||
}
|
||||
} catch (apiError) {
|
||||
addProgressLog(`❌ 批次请求失败: ${apiError.message}`, true);
|
||||
// 将此批次的所有密钥标记为失败
|
||||
batch.forEach(key => {
|
||||
allFailedKeys[key] = apiError.message;
|
||||
});
|
||||
}
|
||||
processedCount += batch.length;
|
||||
updateProgress(processedCount, allKeys.length, progressText);
|
||||
}
|
||||
|
||||
updateProgress(
|
||||
allKeys.length,
|
||||
allKeys.length,
|
||||
`所有批次验证完成!`
|
||||
);
|
||||
|
||||
// 关闭进度模态框并显示最终结果
|
||||
closeProgressModal(false);
|
||||
showVerificationResultModal({
|
||||
successful_keys: allSuccessfulKeys,
|
||||
failed_keys: allFailedKeys,
|
||||
valid_count: allSuccessfulKeys.length,
|
||||
invalid_count: Object.keys(allFailedKeys).length
|
||||
});
|
||||
}
|
||||
@@ -1026,6 +1026,69 @@ endblock %} {% block head_extra_styles %}
|
||||
color: #fca5b3 !important;
|
||||
}
|
||||
/* End of API Call Details Modal Specific Styling Adjustments */
|
||||
|
||||
/* 下拉菜单样式 */
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
background-color: rgba(255, 255, 255, 0.98);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
min-width: 200px;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.dropdown-menu.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
color: #374151;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.dropdown-item:first-child {
|
||||
border-top-left-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-item:last-child {
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-item i {
|
||||
width: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
{% endblock %} {% block head_extra_scripts %}
|
||||
<!-- keys_status.js needs to be loaded in head because it might be used by inline scripts -->
|
||||
@@ -1061,6 +1124,28 @@ endblock %} {% block head_extra_styles %}
|
||||
>
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
<!-- 下拉菜单按钮 -->
|
||||
<div class="dropdown-toggle relative">
|
||||
<button
|
||||
id="dropdownMenuButton"
|
||||
class="bg-white bg-opacity-20 hover:bg-opacity-30 rounded-full w-8 h-8 flex items-center justify-center text-primary-600 transition-all duration-300"
|
||||
onclick="toggleDropdownMenu()"
|
||||
title="更多操作"
|
||||
>
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<!-- 下拉菜单 -->
|
||||
<div id="dropdownMenu" class="dropdown-menu">
|
||||
<button class="dropdown-item" onclick="copyAllKeys()">
|
||||
<i class="fas fa-copy"></i>
|
||||
<span>复制全部密钥</span>
|
||||
</button>
|
||||
<button class="dropdown-item" onclick="verifyAllKeys()">
|
||||
<i class="fas fa-check-double"></i>
|
||||
<span>验证所有密钥</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1
|
||||
@@ -1520,7 +1605,11 @@ endblock %} {% block head_extra_styles %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<p style="color: #374151" id="verifyModalMessage"></p>
|
||||
<p style="color: #374151" id="verifyModalMessage" class="mb-4"></p>
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="batchSize" class="text-sm font-medium" style="color: #374151;">每批次验证数量:</label>
|
||||
<input type="number" id="batchSize" value="10" min="1" class="form-input h-8 w-20 px-2 py-1 text-sm border rounded focus:ring-primary-500 focus:border-primary-500">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
@@ -1644,6 +1733,78 @@ endblock %} {% block head_extra_styles %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批量操作进度模态框 -->
|
||||
<div
|
||||
id="progressModal"
|
||||
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-2xl w-full animate-fade-in"
|
||||
style="
|
||||
background-color: rgba(255, 255, 255, 0.98);
|
||||
color: #374151;
|
||||
border-color: rgba(0, 0, 0, 0.08);
|
||||
"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3
|
||||
class="text-lg font-semibold text-gray-800"
|
||||
id="progressModalTitle"
|
||||
style="color: #1f2937; font-weight: 600"
|
||||
>
|
||||
批量操作进度
|
||||
</h3>
|
||||
<button
|
||||
onclick="closeProgressModal()"
|
||||
id="closeProgressModalBtn"
|
||||
class="text-gray-500 hover:text-gray-700 focus:outline-none"
|
||||
disabled
|
||||
>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<p id="progressStatusText" class="text-sm text-gray-600 mb-2">
|
||||
准备开始...
|
||||
</p>
|
||||
<div class="w-full bg-gray-200 rounded-full h-4 dark:bg-gray-700">
|
||||
<div
|
||||
id="progressBar"
|
||||
class="bg-primary-600 h-4 rounded-full transition-all duration-300"
|
||||
style="width: 0%"
|
||||
></div>
|
||||
</div>
|
||||
<p
|
||||
id="progressPercentage"
|
||||
class="text-center text-sm font-semibold mt-1"
|
||||
style="color: #1f2937"
|
||||
>
|
||||
0%
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
id="progressLog"
|
||||
class="text-xs max-h-60 overflow-y-auto bg-gray-50 p-3 rounded border border-gray-200 space-y-1 font-mono"
|
||||
style="
|
||||
background-color: rgba(249, 250, 251, 0.95);
|
||||
border-color: rgba(0, 0, 0, 0.08);
|
||||
"
|
||||
>
|
||||
<!-- Log entries will be added here -->
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 mt-6">
|
||||
<button
|
||||
id="progressModalCloseBtn"
|
||||
onclick="closeProgressModal(true)"
|
||||
class="px-4 py-1.5 text-sm font-medium bg-primary-700 hover:bg-primary-800 text-white rounded-lg transition-colors"
|
||||
disabled
|
||||
>
|
||||
完成并刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作结果模态框 -->
|
||||
<div
|
||||
id="resultModal"
|
||||
|
||||
Reference in New Issue
Block a user