mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-09-06 16:16:37 +08:00
- 添加API密钥分页显示功能,每页显示20个密钥
- 实现分页控件和搜索功能的集成 - 优化API密钥的数据处理逻辑,从DOM操作改为数组操作 - 修改登录成功后重定向路径从/config改为/keys - 重构routes.py的import语句,按字母顺序排列 - 改进代码格式和缩进风格
This commit is contained in:
+36
-12
@@ -6,10 +6,22 @@ from fastapi import FastAPI, Request
|
|||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
from app.core.security import verify_auth_token
|
|
||||||
from app.config.config import settings
|
from app.config.config import settings
|
||||||
|
from app.core.security import verify_auth_token
|
||||||
from app.log.logger import get_routes_logger
|
from app.log.logger import get_routes_logger
|
||||||
from app.router import error_log_routes, gemini_routes, openai_routes, config_routes, scheduler_routes, stats_routes, version_routes, openai_compatiable_routes, vertex_express_routes, files_routes, key_routes
|
from app.router import (
|
||||||
|
config_routes,
|
||||||
|
error_log_routes,
|
||||||
|
files_routes,
|
||||||
|
gemini_routes,
|
||||||
|
key_routes,
|
||||||
|
openai_compatiable_routes,
|
||||||
|
openai_routes,
|
||||||
|
scheduler_routes,
|
||||||
|
stats_routes,
|
||||||
|
version_routes,
|
||||||
|
vertex_express_routes,
|
||||||
|
)
|
||||||
from app.service.key.key_manager import get_key_manager_instance
|
from app.service.key.key_manager import get_key_manager_instance
|
||||||
from app.service.stats.stats_service import StatsService
|
from app.service.stats.stats_service import StatsService
|
||||||
|
|
||||||
@@ -69,9 +81,12 @@ def setup_page_routes(app: FastAPI) -> None:
|
|||||||
|
|
||||||
if verify_auth_token(auth_token):
|
if verify_auth_token(auth_token):
|
||||||
logger.info("Successful authentication")
|
logger.info("Successful authentication")
|
||||||
response = RedirectResponse(url="/config", status_code=302)
|
response = RedirectResponse(url="/keys", status_code=302)
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
key="auth_token", value=auth_token, httponly=True, max_age=settings.ADMIN_SESSION_EXPIRE
|
key="auth_token",
|
||||||
|
value=auth_token,
|
||||||
|
httponly=True,
|
||||||
|
max_age=settings.ADMIN_SESSION_EXPIRE,
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
logger.warning("Failed authentication attempt with invalid token")
|
logger.warning("Failed authentication attempt with invalid token")
|
||||||
@@ -91,7 +106,9 @@ def setup_page_routes(app: FastAPI) -> None:
|
|||||||
|
|
||||||
key_manager = await get_key_manager_instance()
|
key_manager = await get_key_manager_instance()
|
||||||
keys_status = await key_manager.get_keys_by_status()
|
keys_status = await key_manager.get_keys_by_status()
|
||||||
total_keys = len(keys_status["valid_keys"]) + len(keys_status["invalid_keys"])
|
total_keys = len(keys_status["valid_keys"]) + len(
|
||||||
|
keys_status["invalid_keys"]
|
||||||
|
)
|
||||||
valid_key_count = len(keys_status["valid_keys"])
|
valid_key_count = len(keys_status["valid_keys"])
|
||||||
invalid_key_count = len(keys_status["invalid_keys"])
|
invalid_key_count = len(keys_status["invalid_keys"])
|
||||||
|
|
||||||
@@ -133,7 +150,7 @@ def setup_page_routes(app: FastAPI) -> None:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/config", response_class=HTMLResponse)
|
@app.get("/config", response_class=HTMLResponse)
|
||||||
async def config_page(request: Request):
|
async def config_page(request: Request):
|
||||||
"""配置编辑页面"""
|
"""配置编辑页面"""
|
||||||
@@ -142,13 +159,15 @@ def setup_page_routes(app: FastAPI) -> None:
|
|||||||
if not auth_token or not verify_auth_token(auth_token):
|
if not auth_token or not verify_auth_token(auth_token):
|
||||||
logger.warning("Unauthorized access attempt to config page")
|
logger.warning("Unauthorized access attempt to config page")
|
||||||
return RedirectResponse(url="/", status_code=302)
|
return RedirectResponse(url="/", status_code=302)
|
||||||
|
|
||||||
logger.info("Config page accessed successfully")
|
logger.info("Config page accessed successfully")
|
||||||
return templates.TemplateResponse("config_editor.html", {"request": request})
|
return templates.TemplateResponse(
|
||||||
|
"config_editor.html", {"request": request}
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error accessing config page: {str(e)}")
|
logger.error(f"Error accessing config page: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@app.get("/logs", response_class=HTMLResponse)
|
@app.get("/logs", response_class=HTMLResponse)
|
||||||
async def logs_page(request: Request):
|
async def logs_page(request: Request):
|
||||||
"""错误日志页面"""
|
"""错误日志页面"""
|
||||||
@@ -157,7 +176,7 @@ def setup_page_routes(app: FastAPI) -> None:
|
|||||||
if not auth_token or not verify_auth_token(auth_token):
|
if not auth_token or not verify_auth_token(auth_token):
|
||||||
logger.warning("Unauthorized access attempt to logs page")
|
logger.warning("Unauthorized access attempt to logs page")
|
||||||
return RedirectResponse(url="/", status_code=302)
|
return RedirectResponse(url="/", status_code=302)
|
||||||
|
|
||||||
logger.info("Logs page accessed successfully")
|
logger.info("Logs page accessed successfully")
|
||||||
return templates.TemplateResponse("error_logs.html", {"request": request})
|
return templates.TemplateResponse("error_logs.html", {"request": request})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -187,6 +206,7 @@ def setup_api_stats_routes(app: FastAPI) -> None:
|
|||||||
Args:
|
Args:
|
||||||
app: FastAPI应用程序实例
|
app: FastAPI应用程序实例
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@app.get("/api/stats/details")
|
@app.get("/api/stats/details")
|
||||||
async def api_stats_details(request: Request, period: str):
|
async def api_stats_details(request: Request, period: str):
|
||||||
"""获取指定时间段内的 API 调用详情"""
|
"""获取指定时间段内的 API 调用详情"""
|
||||||
@@ -201,8 +221,12 @@ def setup_api_stats_routes(app: FastAPI) -> None:
|
|||||||
details = await stats_service.get_api_call_details(period)
|
details = await stats_service.get_api_call_details(period)
|
||||||
return details
|
return details
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.warning(f"Invalid period requested for API stats details: {period} - {str(e)}")
|
logger.warning(
|
||||||
|
f"Invalid period requested for API stats details: {period} - {str(e)}"
|
||||||
|
)
|
||||||
return {"error": str(e)}, 400
|
return {"error": str(e)}, 400
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error fetching API stats details for period {period}: {str(e)}")
|
logger.error(
|
||||||
|
f"Error fetching API stats details for period {period}: {str(e)}"
|
||||||
|
)
|
||||||
return {"error": "Internal server error"}, 500
|
return {"error": "Internal server error"}, 500
|
||||||
|
|||||||
+200
-62
@@ -16,6 +16,13 @@ const PROXY_REGEX =
|
|||||||
const VERTEX_API_KEY_REGEX = /AQ\.[a-zA-Z0-9_\-]{50}/g; // 新增 Vertex Express API Key 正则
|
const VERTEX_API_KEY_REGEX = /AQ\.[a-zA-Z0-9_\-]{50}/g; // 新增 Vertex Express API Key 正则
|
||||||
const MASKED_VALUE = "••••••••";
|
const MASKED_VALUE = "••••••••";
|
||||||
|
|
||||||
|
// API Keys Pagination Constants
|
||||||
|
const API_KEYS_PER_PAGE = 20; // 每页显示的API密钥数量
|
||||||
|
let currentApiKeyPage = 1;
|
||||||
|
let totalApiKeyPages = 1;
|
||||||
|
let allApiKeys = []; // 存储所有API密钥数据
|
||||||
|
let filteredApiKeys = []; // 存储过滤后的API密钥数据
|
||||||
|
|
||||||
// DOM Elements - Global Scope for frequently accessed elements
|
// DOM Elements - Global Scope for frequently accessed elements
|
||||||
const safetySettingsContainer = document.getElementById(
|
const safetySettingsContainer = document.getElementById(
|
||||||
"SAFETY_SETTINGS_container"
|
"SAFETY_SETTINGS_container"
|
||||||
@@ -147,6 +154,17 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
if (apiKeySearchInput)
|
if (apiKeySearchInput)
|
||||||
apiKeySearchInput.addEventListener("input", handleApiKeySearch);
|
apiKeySearchInput.addEventListener("input", handleApiKeySearch);
|
||||||
|
|
||||||
|
// API Key Pagination Event Listeners
|
||||||
|
const apiKeyPrevBtn = document.getElementById("apiKeyPrevBtn");
|
||||||
|
const apiKeyNextBtn = document.getElementById("apiKeyNextBtn");
|
||||||
|
|
||||||
|
if (apiKeyPrevBtn) {
|
||||||
|
apiKeyPrevBtn.addEventListener("click", prevApiKeyPage);
|
||||||
|
}
|
||||||
|
if (apiKeyNextBtn) {
|
||||||
|
apiKeyNextBtn.addEventListener("click", nextApiKeyPage);
|
||||||
|
}
|
||||||
|
|
||||||
// Bulk Delete API Key Modal Elements and Events
|
// Bulk Delete API Key Modal Elements and Events
|
||||||
const bulkDeleteApiKeyBtn = document.getElementById("bulkDeleteApiKeyBtn");
|
const bulkDeleteApiKeyBtn = document.getElementById("bulkDeleteApiKeyBtn");
|
||||||
const closeBulkDeleteModalBtn = document.getElementById(
|
const closeBulkDeleteModalBtn = document.getElementById(
|
||||||
@@ -924,9 +942,9 @@ function populateForm(config) {
|
|||||||
'<div class="text-gray-500 text-sm italic">添加自定义请求头,例如 X-Api-Key: your-key</div>';
|
'<div class="text-gray-500 text-sm italic">添加自定义请求头,例如 X-Api-Key: your-key</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Populate other array fields (excluding THINKING_MODELS)
|
// 4. Populate other array fields (excluding THINKING_MODELS and API_KEYS)
|
||||||
for (const [key, value] of Object.entries(config)) {
|
for (const [key, value] of Object.entries(config)) {
|
||||||
if (Array.isArray(value) && key !== "THINKING_MODELS") {
|
if (Array.isArray(value) && key !== "THINKING_MODELS" && key !== "API_KEYS") {
|
||||||
const container = document.getElementById(`${key}_container`);
|
const container = document.getElementById(`${key}_container`);
|
||||||
if (container) {
|
if (container) {
|
||||||
value.forEach((itemValue) => {
|
value.forEach((itemValue) => {
|
||||||
@@ -940,6 +958,17 @@ function populateForm(config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4.1. 特殊处理API_KEYS - 使用分页
|
||||||
|
if (Array.isArray(config.API_KEYS)) {
|
||||||
|
allApiKeys = config.API_KEYS.filter(key =>
|
||||||
|
typeof key === "string" && key.trim() !== ""
|
||||||
|
);
|
||||||
|
filteredApiKeys = [...allApiKeys];
|
||||||
|
currentApiKeyPage = 1;
|
||||||
|
renderApiKeyPage();
|
||||||
|
updateApiKeyPagination();
|
||||||
|
}
|
||||||
|
|
||||||
// 5. Populate non-array/non-budget fields
|
// 5. Populate non-array/non-budget fields
|
||||||
for (const [key, value] of Object.entries(config)) {
|
for (const [key, value] of Object.entries(config)) {
|
||||||
if (
|
if (
|
||||||
@@ -1062,44 +1091,31 @@ function populateForm(config) {
|
|||||||
* Handles the bulk addition of API keys from the modal input.
|
* Handles the bulk addition of API keys from the modal input.
|
||||||
*/
|
*/
|
||||||
function handleBulkAddApiKeys() {
|
function handleBulkAddApiKeys() {
|
||||||
const apiKeyContainer = document.getElementById("API_KEYS_container");
|
if (!apiKeyBulkInput || !apiKeyModal) return;
|
||||||
if (!apiKeyBulkInput || !apiKeyContainer || !apiKeyModal) return;
|
|
||||||
|
|
||||||
const bulkText = apiKeyBulkInput.value;
|
const bulkText = apiKeyBulkInput.value;
|
||||||
const extractedKeys = bulkText.match(API_KEY_REGEX) || [];
|
const extractedKeys = bulkText.match(API_KEY_REGEX) || [];
|
||||||
|
|
||||||
const currentKeyInputs = apiKeyContainer.querySelectorAll(
|
// 合并现有密钥和新密钥,去重
|
||||||
`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`
|
const combinedKeys = new Set([...allApiKeys, ...extractedKeys]);
|
||||||
);
|
|
||||||
let currentKeys = Array.from(currentKeyInputs)
|
|
||||||
.map((input) => {
|
|
||||||
return input.hasAttribute("data-real-value")
|
|
||||||
? input.getAttribute("data-real-value")
|
|
||||||
: input.value;
|
|
||||||
})
|
|
||||||
.filter((key) => key && key.trim() !== "" && key !== MASKED_VALUE);
|
|
||||||
|
|
||||||
const combinedKeys = new Set([...currentKeys, ...extractedKeys]);
|
|
||||||
const uniqueKeys = Array.from(combinedKeys);
|
const uniqueKeys = Array.from(combinedKeys);
|
||||||
|
|
||||||
apiKeyContainer.innerHTML = ""; // Clear existing items more directly
|
// 更新全局密钥数组
|
||||||
|
allApiKeys = uniqueKeys;
|
||||||
|
|
||||||
|
// 更新过滤后的数组
|
||||||
|
const searchTerm = apiKeySearchInput ? apiKeySearchInput.value.toLowerCase() : "";
|
||||||
|
if (!searchTerm) {
|
||||||
|
filteredApiKeys = [...allApiKeys];
|
||||||
|
} else {
|
||||||
|
filteredApiKeys = allApiKeys.filter(key =>
|
||||||
|
key.toLowerCase().includes(searchTerm)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
uniqueKeys.forEach((key) => {
|
// 重新渲染当前页
|
||||||
addArrayItemWithValue("API_KEYS", key);
|
renderApiKeyPage();
|
||||||
});
|
updateApiKeyPagination();
|
||||||
|
|
||||||
const newKeyInputs = apiKeyContainer.querySelectorAll(
|
|
||||||
`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`
|
|
||||||
);
|
|
||||||
newKeyInputs.forEach((input) => {
|
|
||||||
if (configForm && typeof initializeSensitiveFields === "function") {
|
|
||||||
const focusoutEvent = new Event("focusout", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
});
|
|
||||||
input.dispatchEvent(focusoutEvent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
closeModal(apiKeyModal);
|
closeModal(apiKeyModal);
|
||||||
showNotification(`添加/更新了 ${uniqueKeys.length} 个唯一密钥`, "success");
|
showNotification(`添加/更新了 ${uniqueKeys.length} 个唯一密钥`, "success");
|
||||||
@@ -1109,32 +1125,139 @@ function handleBulkAddApiKeys() {
|
|||||||
* Handles searching/filtering of API keys in the list.
|
* Handles searching/filtering of API keys in the list.
|
||||||
*/
|
*/
|
||||||
function handleApiKeySearch() {
|
function handleApiKeySearch() {
|
||||||
const apiKeyContainer = document.getElementById("API_KEYS_container");
|
if (!apiKeySearchInput) return;
|
||||||
if (!apiKeySearchInput || !apiKeyContainer) return;
|
|
||||||
|
|
||||||
const searchTerm = apiKeySearchInput.value.toLowerCase();
|
const searchTerm = apiKeySearchInput.value.toLowerCase();
|
||||||
const keyItems = apiKeyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`);
|
|
||||||
|
// 过滤API密钥
|
||||||
keyItems.forEach((item) => {
|
if (!searchTerm) {
|
||||||
const input = item.querySelector(
|
filteredApiKeys = [...allApiKeys];
|
||||||
`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`
|
} else {
|
||||||
|
filteredApiKeys = allApiKeys.filter(key =>
|
||||||
|
key.toLowerCase().includes(searchTerm)
|
||||||
);
|
);
|
||||||
if (input) {
|
}
|
||||||
const realValue = input.hasAttribute("data-real-value")
|
|
||||||
? input.getAttribute("data-real-value").toLowerCase()
|
// 重置到第一页
|
||||||
: input.value.toLowerCase();
|
currentApiKeyPage = 1;
|
||||||
item.style.display = realValue.includes(searchTerm) ? "flex" : "none";
|
|
||||||
}
|
// 重新渲染当前页
|
||||||
|
renderApiKeyPage();
|
||||||
|
updateApiKeyPagination();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染当前页的API密钥
|
||||||
|
*/
|
||||||
|
function renderApiKeyPage() {
|
||||||
|
const apiKeyContainer = document.getElementById("API_KEYS_container");
|
||||||
|
if (!apiKeyContainer) return;
|
||||||
|
|
||||||
|
// 清空容器
|
||||||
|
apiKeyContainer.innerHTML = "";
|
||||||
|
|
||||||
|
// 计算当前页的数据范围
|
||||||
|
const startIndex = (currentApiKeyPage - 1) * API_KEYS_PER_PAGE;
|
||||||
|
const endIndex = Math.min(startIndex + API_KEYS_PER_PAGE, filteredApiKeys.length);
|
||||||
|
const pageKeys = filteredApiKeys.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
// 渲染当前页的密钥
|
||||||
|
pageKeys.forEach((key) => {
|
||||||
|
addArrayItemWithValue("API_KEYS", key);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 如果没有密钥,显示提示信息
|
||||||
|
if (pageKeys.length === 0) {
|
||||||
|
const emptyMessage = document.createElement("div");
|
||||||
|
emptyMessage.className = "text-gray-500 text-sm italic text-center py-4";
|
||||||
|
emptyMessage.textContent = filteredApiKeys.length === 0 ?
|
||||||
|
(allApiKeys.length === 0 ? "暂无API密钥" : "未找到匹配的密钥") :
|
||||||
|
"当前页无数据";
|
||||||
|
apiKeyContainer.appendChild(emptyMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新分页控件
|
||||||
|
*/
|
||||||
|
function updateApiKeyPagination() {
|
||||||
|
totalApiKeyPages = Math.max(1, Math.ceil(filteredApiKeys.length / API_KEYS_PER_PAGE));
|
||||||
|
|
||||||
|
// 确保当前页在有效范围内
|
||||||
|
if (currentApiKeyPage > totalApiKeyPages) {
|
||||||
|
currentApiKeyPage = totalApiKeyPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paginationContainer = document.getElementById("apiKeyPagination");
|
||||||
|
if (!paginationContainer) return;
|
||||||
|
|
||||||
|
// 如果只有一页或没有数据,隐藏分页控件
|
||||||
|
if (totalApiKeyPages <= 1) {
|
||||||
|
paginationContainer.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
paginationContainer.style.display = "flex";
|
||||||
|
|
||||||
|
// 更新页码信息
|
||||||
|
const pageInfo = document.getElementById("apiKeyPageInfo");
|
||||||
|
if (pageInfo) {
|
||||||
|
pageInfo.textContent = `第 ${currentApiKeyPage} 页,共 ${totalApiKeyPages} 页 (${filteredApiKeys.length} 个密钥)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新按钮状态
|
||||||
|
const prevBtn = document.getElementById("apiKeyPrevBtn");
|
||||||
|
const nextBtn = document.getElementById("apiKeyNextBtn");
|
||||||
|
|
||||||
|
if (prevBtn) {
|
||||||
|
prevBtn.disabled = currentApiKeyPage <= 1;
|
||||||
|
prevBtn.className = currentApiKeyPage <= 1 ?
|
||||||
|
"px-3 py-1 rounded bg-gray-300 text-gray-500 cursor-not-allowed" :
|
||||||
|
"px-3 py-1 rounded bg-blue-500 text-white hover:bg-blue-600 cursor-pointer";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextBtn) {
|
||||||
|
nextBtn.disabled = currentApiKeyPage >= totalApiKeyPages;
|
||||||
|
nextBtn.className = currentApiKeyPage >= totalApiKeyPages ?
|
||||||
|
"px-3 py-1 rounded bg-gray-300 text-gray-500 cursor-not-allowed" :
|
||||||
|
"px-3 py-1 rounded bg-blue-500 text-white hover:bg-blue-600 cursor-pointer";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到指定页
|
||||||
|
*/
|
||||||
|
function goToApiKeyPage(page) {
|
||||||
|
if (page < 1 || page > totalApiKeyPages) return;
|
||||||
|
|
||||||
|
currentApiKeyPage = page;
|
||||||
|
renderApiKeyPage();
|
||||||
|
updateApiKeyPagination();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上一页
|
||||||
|
*/
|
||||||
|
function prevApiKeyPage() {
|
||||||
|
if (currentApiKeyPage > 1) {
|
||||||
|
goToApiKeyPage(currentApiKeyPage - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下一页
|
||||||
|
*/
|
||||||
|
function nextApiKeyPage() {
|
||||||
|
if (currentApiKeyPage < totalApiKeyPages) {
|
||||||
|
goToApiKeyPage(currentApiKeyPage + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the bulk deletion of API keys based on input from the modal.
|
* Handles the bulk deletion of API keys based on input from the modal.
|
||||||
*/
|
*/
|
||||||
function handleBulkDeleteApiKeys() {
|
function handleBulkDeleteApiKeys() {
|
||||||
const apiKeyContainer = document.getElementById("API_KEYS_container");
|
if (!bulkDeleteApiKeyInput || !bulkDeleteApiKeyModal) return;
|
||||||
if (!bulkDeleteApiKeyInput || !apiKeyContainer || !bulkDeleteApiKeyModal)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const bulkText = bulkDeleteApiKeyInput.value;
|
const bulkText = bulkDeleteApiKeyInput.value;
|
||||||
if (!bulkText.trim()) {
|
if (!bulkText.trim()) {
|
||||||
@@ -1149,24 +1272,30 @@ function handleBulkDeleteApiKeys() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyItems = apiKeyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`);
|
// 从allApiKeys数组中删除匹配的密钥
|
||||||
let deleteCount = 0;
|
let deleteCount = 0;
|
||||||
|
allApiKeys = allApiKeys.filter(key => {
|
||||||
keyItems.forEach((item) => {
|
if (keysToDelete.has(key)) {
|
||||||
const input = item.querySelector(
|
|
||||||
`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`
|
|
||||||
);
|
|
||||||
const realValue =
|
|
||||||
input &&
|
|
||||||
(input.hasAttribute("data-real-value")
|
|
||||||
? input.getAttribute("data-real-value")
|
|
||||||
: input.value);
|
|
||||||
if (realValue && keysToDelete.has(realValue)) {
|
|
||||||
item.remove();
|
|
||||||
deleteCount++;
|
deleteCount++;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 更新过滤后的数组
|
||||||
|
const searchTerm = apiKeySearchInput ? apiKeySearchInput.value.toLowerCase() : "";
|
||||||
|
if (!searchTerm) {
|
||||||
|
filteredApiKeys = [...allApiKeys];
|
||||||
|
} else {
|
||||||
|
filteredApiKeys = allApiKeys.filter(key =>
|
||||||
|
key.toLowerCase().includes(searchTerm)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新渲染当前页
|
||||||
|
renderApiKeyPage();
|
||||||
|
updateApiKeyPagination();
|
||||||
|
|
||||||
closeModal(bulkDeleteApiKeyModal);
|
closeModal(bulkDeleteApiKeyModal);
|
||||||
|
|
||||||
if (deleteCount > 0) {
|
if (deleteCount > 0) {
|
||||||
@@ -1782,6 +1911,15 @@ function collectFormData() {
|
|||||||
const arrayContainers = document.querySelectorAll(".array-container");
|
const arrayContainers = document.querySelectorAll(".array-container");
|
||||||
arrayContainers.forEach((container) => {
|
arrayContainers.forEach((container) => {
|
||||||
const key = container.id.replace("_container", "");
|
const key = container.id.replace("_container", "");
|
||||||
|
|
||||||
|
// 特殊处理API_KEYS - 使用全局数组而不是DOM元素
|
||||||
|
if (key === "API_KEYS") {
|
||||||
|
formData[key] = allApiKeys.filter(
|
||||||
|
(value) => value && value.trim() !== "" && value !== MASKED_VALUE
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const arrayInputs = container.querySelectorAll(`.${ARRAY_INPUT_CLASS}`);
|
const arrayInputs = container.querySelectorAll(`.${ARRAY_INPUT_CLASS}`);
|
||||||
formData[key] = Array.from(arrayInputs)
|
formData[key] = Array.from(arrayInputs)
|
||||||
.map((input) => {
|
.map((input) => {
|
||||||
|
|||||||
@@ -961,6 +961,31 @@ endblock %} {% block head_extra_styles %}
|
|||||||
<div class="array-container" id="API_KEYS_container">
|
<div class="array-container" id="API_KEYS_container">
|
||||||
<!-- 数组项将在这里动态添加 -->
|
<!-- 数组项将在这里动态添加 -->
|
||||||
</div>
|
</div>
|
||||||
|
<!-- API密钥分页控件 -->
|
||||||
|
<div id="apiKeyPagination" class="flex items-center justify-between mt-2 mb-2" style="display: none;">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="apiKeyPrevBtn"
|
||||||
|
onclick="prevApiKeyPage()"
|
||||||
|
class="px-3 py-1 rounded bg-blue-500 text-white hover:bg-blue-600 cursor-pointer"
|
||||||
|
>
|
||||||
|
<i class="fas fa-chevron-left"></i> 上一页
|
||||||
|
</button>
|
||||||
|
<span id="apiKeyPageInfo" class="text-sm text-gray-600">第 1 页,共 1 页</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="apiKeyNextBtn"
|
||||||
|
onclick="nextApiKeyPage()"
|
||||||
|
class="px-3 py-1 rounded bg-blue-500 text-white hover:bg-blue-600 cursor-pointer"
|
||||||
|
>
|
||||||
|
下一页 <i class="fas fa-chevron-right"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
每页显示 20 个密钥
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user