';
+ }
+ }
+ arrayItem.remove();
+ // Check and add placeholder for safety settings if empty
+ if (isSafetySettingItem && parentContainer && parentContainer.children.length === 0) {
+ parentContainer.innerHTML = '
定义模型的安全过滤阈值。
';
+ }
+ } else if (generateButton && generateButton.closest(`.${ARRAY_ITEM_CLASS}`)) {
+ const inputField = generateButton.closest(`.${ARRAY_ITEM_CLASS}`).querySelector(`.${ARRAY_INPUT_CLASS}`);
+ if (inputField) {
+ const newToken = generateRandomToken();
+ inputField.value = newToken;
+ if (inputField.classList.contains(SENSITIVE_INPUT_CLASS)) {
+ const event = new Event('focusout', { bubbles: true, cancelable: true });
+ inputField.dispatchEvent(event);
+ }
+ showNotification('已生成新令牌', 'success');
+ }
+ }
+ });
+ }
+
+
+ // Add Safety Setting button
const addSafetySettingBtn = document.getElementById('addSafetySettingBtn');
if (addSafetySettingBtn) {
addSafetySettingBtn.addEventListener('click', () => addSafetySettingItem());
}
- // --- 结束:安全设置相关 ---
-}); // <-- DOMContentLoaded 结束括号
+ initializeSensitiveFields(); // Initialize sensitive field handling
+}); // <-- DOMContentLoaded end
-// --- 新增:生成唯一ID ---
+/**
+ * Initializes sensitive input field behavior (masking/unmasking).
+ */
+function initializeSensitiveFields() {
+ if (!configForm) return;
+
+ // Helper function: Mask field
+ function maskField(field) {
+ if (field.value && field.value !== MASKED_VALUE) {
+ field.setAttribute('data-real-value', field.value);
+ field.value = MASKED_VALUE;
+ } else if (!field.value) { // If field value is empty string
+ field.removeAttribute('data-real-value');
+ // Ensure empty value doesn't show as asterisks
+ if (field.value === MASKED_VALUE) field.value = '';
+ }
+ }
+
+ // Helper function: Unmask field
+ function unmaskField(field) {
+ if (field.hasAttribute('data-real-value')) {
+ field.value = field.getAttribute('data-real-value');
+ }
+ // If no data-real-value and value is MASKED_VALUE, it might be an initial empty sensitive field, clear it
+ else if (field.value === MASKED_VALUE && !field.hasAttribute('data-real-value')) {
+ field.value = '';
+ }
+ }
+
+ // Initial masking for existing sensitive fields on page load
+ // This function is called after populateForm and after dynamic element additions (via event delegation)
+ function initialMaskAllExisting() {
+ const sensitiveFields = configForm.querySelectorAll(`.${SENSITIVE_INPUT_CLASS}`);
+ sensitiveFields.forEach(field => {
+ if (field.type === 'password') {
+ // For password fields, browser handles it. We just ensure data-original-type is set
+ // and if it has a value, we also store data-real-value so it can be shown when switched to text
+ if (field.value) {
+ field.setAttribute('data-real-value', field.value);
+ }
+ // No need to set to MASKED_VALUE as browser handles it.
+ } else if (field.type === 'text' || field.tagName.toLowerCase() === 'textarea') {
+ maskField(field);
+ }
+ });
+ }
+ initialMaskAllExisting();
+
+
+ // Event delegation for dynamic and static fields
+ configForm.addEventListener('focusin', function(event) {
+ const target = event.target;
+ if (target.classList.contains(SENSITIVE_INPUT_CLASS)) {
+ if (target.type === 'password') {
+ // Record original type to switch back on blur
+ if (!target.hasAttribute('data-original-type')) {
+ target.setAttribute('data-original-type', 'password');
+ }
+ target.type = 'text'; // Switch to text type to show content
+ // If data-real-value exists (e.g., set during populateForm), use it
+ if (target.hasAttribute('data-real-value')) {
+ target.value = target.getAttribute('data-real-value');
+ }
+ // Otherwise, the browser's existing password value will be shown directly
+ } else { // For type="text" or textarea
+ unmaskField(target);
+ }
+ }
+ });
+
+ configForm.addEventListener('focusout', function(event) {
+ const target = event.target;
+ if (target.classList.contains(SENSITIVE_INPUT_CLASS)) {
+ // First, if the field is currently text and has a value, update data-real-value
+ if (target.type === 'text' || target.tagName.toLowerCase() === 'textarea') {
+ if (target.value && target.value !== MASKED_VALUE) {
+ target.setAttribute('data-real-value', target.value);
+ } else if (!target.value) { // If value is empty, remove data-real-value
+ target.removeAttribute('data-real-value');
+ }
+ }
+
+ // Then handle type switching and masking
+ if (target.getAttribute('data-original-type') === 'password' && target.type === 'text') {
+ target.type = 'password'; // Switch back to password type
+ // For password type, browser handles masking automatically, no need to set MASKED_VALUE manually
+ // data-real-value has already been updated by the logic above
+ } else if (target.type === 'text' || target.tagName.toLowerCase() === 'textarea') {
+ // For text or textarea sensitive fields, perform masking
+ maskField(target);
+ }
+ }
+ });
+}
+
+/**
+ * Generates a UUID.
+ * @returns {string} A new UUID.
+ */
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
-// --- 结束:生成唯一ID ---
-
-// 初始化配置
+/**
+ * Initializes the configuration by fetching it from the server and populating the form.
+ */
async function initConfig() {
try {
showNotification('正在加载配置...', 'info');
@@ -385,8 +427,12 @@ async function initConfig() {
// --- 结束:处理 SAFETY_SETTINGS 默认值 ---
populateForm(config);
+ // After populateForm, initialize masking for all populated sensitive fields
+ if (configForm) { // Ensure form exists
+ initializeSensitiveFields(); // Call initializeSensitiveFields to handle initial masking
+ }
- // 确保上传提供商有默认值
+ // Ensure upload provider has a default value
const uploadProvider = document.getElementById('UPLOAD_PROVIDER');
if (uploadProvider && !uploadProvider.value) {
uploadProvider.value = 'smms'; // 设置默认值为 smms
@@ -412,11 +458,17 @@ async function initConfig() {
};
populateForm(defaultConfig);
+ if (configForm) { // Ensure form exists
+ initializeSensitiveFields(); // Call initializeSensitiveFields to handle initial masking
+ }
toggleProviderConfig('smms');
}
}
-// 填充表单
+/**
+ * Populates the configuration form with data.
+ * @param {object} config - The configuration object.
+ */
function populateForm(config) {
const modelIdMap = {}; // modelName -> modelId
@@ -440,7 +492,6 @@ function populateForm(config) {
config.THINKING_MODELS.forEach(modelName => {
if (modelName && typeof modelName === 'string' && modelName.trim()) {
const trimmedModelName = modelName.trim();
- // Call addArrayItemWithValue to add the model DOM element and get its ID
const modelId = addArrayItemWithValue('THINKING_MODELS', trimmedModelName);
if (modelId) {
modelIdMap[trimmedModelName] = modelId;
@@ -464,11 +515,9 @@ function populateForm(config) {
const trimmedModelName = modelName.trim();
const modelId = modelIdMap[trimmedModelName]; // Look up the ID
if (modelId) {
- // Call the function specifically designed to add ONLY the budget map DOM element
createAndAppendBudgetMapItem(trimmedModelName, budgetValue, modelId);
budgetItemsAdded = true;
} else {
- // Log if a budget entry exists but its corresponding model wasn't found/added
console.warn(`Budget map: Could not find model ID for '${trimmedModelName}'. Skipping budget item.`);
}
} else {
@@ -476,7 +525,6 @@ function populateForm(config) {
}
}
}
- // Add placeholder only if no budget items were successfully added
if (!budgetItemsAdded && budgetMapContainer) {
budgetMapContainer.innerHTML = '
请在上方添加思考模型,预算将自动关联。
';
}
@@ -486,10 +534,9 @@ function populateForm(config) {
if (Array.isArray(value) && key !== 'THINKING_MODELS') {
const container = document.getElementById(`${key}_container`);
if (container) {
- // Container already cleared, just add items
value.forEach(itemValue => {
if (typeof itemValue === 'string') {
- addArrayItemWithValue(key, itemValue); // This adds non-thinking model array items
+ addArrayItemWithValue(key, itemValue);
} else {
console.warn(`Invalid item found in array '${key}':`, itemValue);
}
@@ -522,7 +569,7 @@ function populateForm(config) {
toggleProviderConfig(uploadProvider.value);
}
- // --- 新增:填充 SAFETY_SETTINGS ---
+ // Populate SAFETY_SETTINGS
let safetyItemsAdded = false;
if (safetySettingsContainer && Array.isArray(config.SAFETY_SETTINGS)) {
config.SAFETY_SETTINGS.forEach(setting => {
@@ -534,209 +581,179 @@ function populateForm(config) {
}
});
}
- // 如果没有添加任何安全设置项,则显示占位符
if (safetySettingsContainer && !safetyItemsAdded) {
safetySettingsContainer.innerHTML = '
定义模型的安全过滤阈值。
';
}
- // --- 结束:填充 SAFETY_SETTINGS ---
}
-// --- 新增:处理批量添加 API Key 的逻辑 ---
+/**
+ * Handles the bulk addition of API keys from the modal input.
+ */
function handleBulkAddApiKeys() {
- const apiKeyBulkInput = document.getElementById('apiKeyBulkInput');
const apiKeyContainer = document.getElementById('API_KEYS_container');
- const apiKeyModal = document.getElementById('apiKeyModal');
-
if (!apiKeyBulkInput || !apiKeyContainer || !apiKeyModal) return;
const bulkText = apiKeyBulkInput.value;
- const keyRegex = /AIzaSy\S{33}/g; // 全局匹配
- const extractedKeys = bulkText.match(keyRegex) || [];
+ const extractedKeys = bulkText.match(API_KEY_REGEX) || [];
- // 获取当前已有的 keys
- const currentKeyInputs = apiKeyContainer.querySelectorAll('.array-input');
- const currentKeys = Array.from(currentKeyInputs).map(input => input.value).filter(key => key.trim() !== '');
+ const currentKeyInputs = apiKeyContainer.querySelectorAll(`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`);
+ 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 existingItems = apiKeyContainer.querySelectorAll('.array-item');
- existingItems.forEach(item => item.remove());
+ apiKeyContainer.innerHTML = ''; // Clear existing items more directly
- // 重新填充列表
uniqueKeys.forEach(key => {
addArrayItemWithValue('API_KEYS', key);
});
+
+ 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);
+ }
+ });
- // 关闭模态框
- apiKeyModal.classList.remove('show');
+ closeModal(apiKeyModal);
showNotification(`添加/更新了 ${uniqueKeys.length} 个唯一密钥`, 'success');
}
-// --- 新增:处理 API Key 搜索的逻辑 ---
+/**
+ * Handles searching/filtering of API keys in the list.
+ */
function handleApiKeySearch() {
- const apiKeySearchInput = document.getElementById('apiKeySearchInput');
const apiKeyContainer = document.getElementById('API_KEYS_container');
-
if (!apiKeySearchInput || !apiKeyContainer) return;
const searchTerm = apiKeySearchInput.value.toLowerCase();
- const keyItems = apiKeyContainer.querySelectorAll('.array-item');
+ const keyItems = apiKeyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`);
keyItems.forEach(item => {
- const input = item.querySelector('.array-input');
+ const input = item.querySelector(`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`);
if (input) {
- const key = input.value.toLowerCase();
- if (key.includes(searchTerm)) {
- item.style.display = 'flex'; // 或者 'block',取决于你的布局
- } else {
- item.style.display = 'none';
- }
+ const realValue = input.hasAttribute('data-real-value') ? input.getAttribute('data-real-value').toLowerCase() : input.value.toLowerCase();
+ item.style.display = realValue.includes(searchTerm) ? 'flex' : 'none';
}
});
}
-// --- 新增:处理批量删除 API Key 的逻辑 ---
+/**
+ * Handles the bulk deletion of API keys based on input from the modal.
+ */
function handleBulkDeleteApiKeys() {
- const bulkDeleteTextarea = document.getElementById('bulkDeleteApiKeyInput'); // Use the textarea ID
const apiKeyContainer = document.getElementById('API_KEYS_container');
- const bulkDeleteModal = document.getElementById('bulkDeleteApiKeyModal');
+ if (!bulkDeleteApiKeyInput || !apiKeyContainer || !bulkDeleteApiKeyModal) return;
- if (!bulkDeleteTextarea || !apiKeyContainer || !bulkDeleteModal) return;
-
- const bulkText = bulkDeleteTextarea.value;
+ const bulkText = bulkDeleteApiKeyInput.value;
if (!bulkText.trim()) {
showNotification('请粘贴需要删除的 API 密钥', 'warning');
return;
}
- // Use the same regex as for adding keys to extract keys to delete
- const keyRegex = /AIzaSy\S{33}/g;
- const keysToDelete = new Set(bulkText.match(keyRegex) || []); // Create a Set for efficient lookup
+ const keysToDelete = new Set(bulkText.match(API_KEY_REGEX) || []);
if (keysToDelete.size === 0) {
showNotification('未在输入内容中提取到有效的 API 密钥格式', 'warning');
- // Optionally clear the textarea or keep it as is
- // bulkDeleteTextarea.value = '';
return;
}
- const keyItems = apiKeyContainer.querySelectorAll('.array-item');
+ const keyItems = apiKeyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`);
let deleteCount = 0;
keyItems.forEach(item => {
- const input = item.querySelector('.array-input');
- // Check if the input exists and its value is in the set of keys to delete
- if (input && keysToDelete.has(input.value)) {
- item.remove(); // Remove the entire array item element
+ 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++;
}
});
- // Close the modal
- bulkDeleteModal.classList.remove('show');
+ closeModal(bulkDeleteApiKeyModal);
- // Provide feedback
if (deleteCount > 0) {
showNotification(`成功删除了 ${deleteCount} 个匹配的密钥`, 'success');
} else {
- // This message implies keys were extracted but not found in the current list
showNotification('列表中未找到您输入的任何密钥进行删除', 'info');
}
-
- // Clear the textarea after processing
- bulkDeleteTextarea.value = '';
+ bulkDeleteApiKeyInput.value = '';
}
-// --- 新增:处理批量添加 Proxy 的逻辑 ---
+/**
+ * Handles the bulk addition of proxies from the modal input.
+ */
function handleBulkAddProxies() {
- const proxyBulkInput = document.getElementById('proxyBulkInput');
const proxyContainer = document.getElementById('PROXIES_container');
- const proxyModal = document.getElementById('proxyModal');
-
if (!proxyBulkInput || !proxyContainer || !proxyModal) return;
const bulkText = proxyBulkInput.value;
- // 匹配 http(s):// 或 socks5:// 格式的代理,允许包含用户名密码
- const proxyRegex = /(?:https?|socks5):\/\/(?:[^:@\/]+(?::[^@\/]+)?@)?(?:[^:\/\s]+)(?::\d+)?/g;
- const extractedProxies = bulkText.match(proxyRegex) || [];
+ const extractedProxies = bulkText.match(PROXY_REGEX) || [];
- // 获取当前已有的 proxies
- const currentProxyInputs = proxyContainer.querySelectorAll('.array-input');
+ const currentProxyInputs = proxyContainer.querySelectorAll(`.${ARRAY_INPUT_CLASS}`);
const currentProxies = Array.from(currentProxyInputs).map(input => input.value).filter(proxy => proxy.trim() !== '');
- // 合并并去重
const combinedProxies = new Set([...currentProxies, ...extractedProxies]);
const uniqueProxies = Array.from(combinedProxies);
- // 清空现有列表显示
- const existingItems = proxyContainer.querySelectorAll('.array-item');
- existingItems.forEach(item => item.remove());
+ proxyContainer.innerHTML = ''; // Clear existing items
- // 重新填充列表
uniqueProxies.forEach(proxy => {
addArrayItemWithValue('PROXIES', proxy);
});
- // 关闭模态框
- proxyModal.classList.remove('show');
+ closeModal(proxyModal);
showNotification(`添加/更新了 ${uniqueProxies.length} 个唯一代理`, 'success');
}
-// --- 结束:处理批量添加 Proxy 的逻辑 ---
-// --- 新增:处理批量删除 Proxy 的逻辑 ---
+/**
+ * Handles the bulk deletion of proxies based on input from the modal.
+ */
function handleBulkDeleteProxies() {
- const bulkDeleteTextarea = document.getElementById('bulkDeleteProxyInput');
const proxyContainer = document.getElementById('PROXIES_container');
- const bulkDeleteModal = document.getElementById('bulkDeleteProxyModal');
+ if (!bulkDeleteProxyInput || !proxyContainer || !bulkDeleteProxyModal) return;
- if (!bulkDeleteTextarea || !proxyContainer || !bulkDeleteModal) return;
-
- const bulkText = bulkDeleteTextarea.value;
+ const bulkText = bulkDeleteProxyInput.value;
if (!bulkText.trim()) {
showNotification('请粘贴需要删除的代理地址', 'warning');
return;
}
- // 使用与添加时相同的正则表达式来提取要删除的代理
- const proxyRegex = /(?:https?|socks5):\/\/(?:[^:@\/]+(?::[^@\/]+)?@)?(?:[^:\/\s]+)(?::\d+)?/g;
- const proxiesToDelete = new Set(bulkText.match(proxyRegex) || []); // 使用 Set 进行高效查找
+ const proxiesToDelete = new Set(bulkText.match(PROXY_REGEX) || []);
if (proxiesToDelete.size === 0) {
showNotification('未在输入内容中提取到有效的代理地址格式', 'warning');
return;
}
- const proxyItems = proxyContainer.querySelectorAll('.array-item');
+ const proxyItems = proxyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`);
let deleteCount = 0;
proxyItems.forEach(item => {
- const input = item.querySelector('.array-input');
- // 检查输入框是否存在及其值是否在要删除的集合中
+ const input = item.querySelector(`.${ARRAY_INPUT_CLASS}`);
if (input && proxiesToDelete.has(input.value)) {
- item.remove(); // 删除整个数组项元素
+ item.remove();
deleteCount++;
}
});
- // 关闭模态框
- bulkDeleteModal.classList.remove('show');
+ closeModal(bulkDeleteProxyModal);
- // 提供反馈
if (deleteCount > 0) {
showNotification(`成功删除了 ${deleteCount} 个匹配的代理`, 'success');
} else {
showNotification('列表中未找到您输入的任何代理进行删除', 'info');
}
-
- // 处理后清空文本区域
- bulkDeleteTextarea.value = '';
+ bulkDeleteProxyInput.value = '';
}
-// --- 结束:处理批量删除 Proxy 的逻辑 ---
-// 切换标签
+/**
+ * Switches the active configuration tab.
+ * @param {string} tabId - The ID of the tab to switch to.
+ */
function switchTab(tabId) {
// 更新标签按钮状态
const tabButtons = document.querySelectorAll('.tab-btn');
@@ -763,7 +780,10 @@ function switchTab(tabId) {
});
}
-// 切换上传提供商配置
+/**
+ * Toggles the visibility of configuration sections for different upload providers.
+ * @param {string} provider - The selected upload provider.
+ */
function toggleProviderConfig(provider) {
const providerConfigs = document.querySelectorAll('.provider-config');
providerConfigs.forEach(config => {
@@ -775,113 +795,137 @@ function toggleProviderConfig(provider) {
});
}
+/**
+ * Creates and appends an input field for an array item.
+ * @param {string} key - The configuration key for the array.
+ * @param {string} value - The initial value for the input field.
+ * @param {boolean} isSensitive - Whether the input is for sensitive data.
+ * @param {string|null} modelId - Optional model ID for thinking models.
+ * @returns {HTMLInputElement} The created input element.
+ */
+function createArrayInput(key, value, isSensitive, modelId = null) {
+ const input = document.createElement('input');
+ input.type = 'text';
+ input.name = `${key}[]`; // Used for form submission if not handled by JS
+ input.value = value;
+ let inputClasses = `${ARRAY_INPUT_CLASS} flex-grow px-3 py-2 border-none rounded-l-md focus:outline-none`;
+ if (isSensitive) {
+ inputClasses += ` ${SENSITIVE_INPUT_CLASS}`;
+ }
+ input.className = inputClasses;
+ if (modelId) {
+ input.setAttribute('data-model-id', modelId);
+ input.placeholder = '思考模型名称';
+ }
+ return input;
+}
-// 添加数组项
+/**
+ * Creates a generate token button for allowed tokens.
+ * @returns {HTMLButtonElement} The created button element.
+ */
+function createGenerateTokenButton() {
+ const generateBtn = document.createElement('button');
+ generateBtn.type = 'button';
+ generateBtn.className = 'generate-btn px-2 py-2 text-gray-500 hover:text-primary-600 focus:outline-none rounded-r-md bg-gray-100 hover:bg-gray-200 transition-colors';
+ generateBtn.innerHTML = '';
+ generateBtn.title = '生成随机令牌';
+ // Event listener will be added via delegation in DOMContentLoaded
+ return generateBtn;
+}
+
+/**
+ * Creates a remove button for an array item.
+ * @returns {HTMLButtonElement} The created button element.
+ */
+function createRemoveButton() {
+ const removeBtn = document.createElement('button');
+ removeBtn.type = 'button';
+ removeBtn.className = 'remove-btn text-gray-400 hover:text-red-500 focus:outline-none transition-colors duration-150';
+ removeBtn.innerHTML = '';
+ removeBtn.title = '删除';
+ // Event listener will be added via delegation in DOMContentLoaded
+ return removeBtn;
+}
+
+
+/**
+ * Adds a new item to an array configuration section (e.g., API_KEYS, ALLOWED_TOKENS).
+ * This function is typically called by a "+" button.
+ * @param {string} key - The configuration key for the array (e.g., 'API_KEYS').
+ */
function addArrayItem(key) {
const container = document.getElementById(`${key}_container`);
if (!container) return;
- const newItemValue = ''; // Start with an empty value for new items
- const modelId = addArrayItemWithValue(key, newItemValue); // Add the DOM element
+ const newItemValue = ''; // New items start empty
+ const modelId = addArrayItemWithValue(key, newItemValue); // This adds the DOM element
- // If it's a thinking model, also add the corresponding budget map item
if (key === 'THINKING_MODELS' && modelId) {
createAndAppendBudgetMapItem(newItemValue, 0, modelId); // Default budget 0
}
}
-// 添加带值的数组项 (Adds array item DOM, returns modelId if it's a thinking model)
+
+/**
+ * Adds an array item with a specific value to the DOM.
+ * This is used both for initially populating the form and for adding new items.
+ * @param {string} key - The configuration key (e.g., 'API_KEYS', 'THINKING_MODELS').
+ * @param {string} value - The value for the array item.
+ * @returns {string|null} The generated modelId if it's a thinking model, otherwise null.
+ */
function addArrayItemWithValue(key, value) {
const container = document.getElementById(`${key}_container`);
if (!container) return null;
const isThinkingModel = key === 'THINKING_MODELS';
+ const isAllowedToken = key === 'ALLOWED_TOKENS';
+ const isSensitive = key === 'API_KEYS' || isAllowedToken;
const modelId = isThinkingModel ? generateUUID() : null;
const arrayItem = document.createElement('div');
- // 主容器使用 Flexbox
- arrayItem.className = 'array-item flex items-center mb-2 gap-2'; // 添加 gap-2 来分隔元素
+ arrayItem.className = `${ARRAY_ITEM_CLASS} flex items-center mb-2 gap-2`;
if (isThinkingModel) {
- arrayItem.setAttribute('data-model-id', modelId); // 添加ID属性
+ arrayItem.setAttribute('data-model-id', modelId);
}
-
- // 创建一个包装器 div 来包含输入框和生成按钮
const inputWrapper = document.createElement('div');
- // 这个包装器占据主要空间,并使用 Flexbox
inputWrapper.className = 'flex items-center flex-grow border border-gray-300 rounded-md focus-within:border-primary-500 focus-within:ring focus-within:ring-primary-200 focus-within:ring-opacity-50';
- const input = document.createElement('input');
- input.type = 'text';
- input.name = `${key}[]`;
- input.value = value;
- // 输入框占据包装器内的主要空间,移除边框和圆角,因为包装器已有
- input.className = 'array-input flex-grow px-3 py-2 border-none rounded-l-md focus:outline-none'; // 移除右侧圆角
- if (isThinkingModel) {
- input.setAttribute('data-model-id', modelId); // 添加ID属性
- input.placeholder = '思考模型名称'; // 添加占位符
- }
+ const input = createArrayInput(key, value, isSensitive, isThinkingModel ? modelId : null);
+ inputWrapper.appendChild(input);
-
- inputWrapper.appendChild(input); // 将输入框添加到包装器
-
- // 只为 ALLOWED_TOKENS 添加生成按钮
- if (key === 'ALLOWED_TOKENS') {
- const generateBtn = document.createElement('button');
- generateBtn.type = 'button';
- // 按钮样式,放在输入框右侧,有背景和内边距,调整颜色
- generateBtn.className = 'generate-btn px-2 py-2 text-gray-500 hover:text-primary-600 focus:outline-none rounded-r-md bg-gray-100 hover:bg-gray-200 transition-colors'; // 添加背景和右侧圆角
- generateBtn.innerHTML = '';
- generateBtn.title = '生成随机令牌';
- generateBtn.addEventListener('click', function() {
- const newToken = generateRandomToken();
- input.value = newToken;
- showNotification('已生成新令牌', 'success');
- });
- inputWrapper.appendChild(generateBtn); // 将生成按钮添加到包装器
+ if (isAllowedToken) {
+ const generateBtn = createGenerateTokenButton();
+ inputWrapper.appendChild(generateBtn);
} else {
- // 如果不是 ALLOWED_TOKENS,确保输入框有右侧圆角
+ // Ensure right-side rounding if no button is present
input.classList.add('rounded-r-md');
}
+
+ const removeBtn = createRemoveButton();
- const removeBtn = document.createElement('button');
- removeBtn.type = 'button';
- // 删除按钮样式,保持不变
- removeBtn.className = 'remove-btn text-gray-400 hover:text-red-500 focus:outline-none transition-colors duration-150';
- removeBtn.innerHTML = '';
- removeBtn.title = '删除';
- removeBtn.addEventListener('click', function() {
- const currentArrayItem = this.closest('.array-item');
- if (isThinkingModel) {
- const currentModelId = currentArrayItem.getAttribute('data-model-id');
- // 查找并删除对应的预算映射项
- const budgetMapItem = document.querySelector(`.map-item[data-model-id="${currentModelId}"]`);
- if (budgetMapItem) {
- budgetMapItem.remove();
- // 检查预算映射容器是否为空,如果是,则添加回占位符
- const budgetContainer = document.getElementById('THINKING_BUDGET_MAP_container');
- if (budgetContainer && budgetContainer.children.length === 0) {
- budgetContainer.innerHTML = '
请在上方添加思考模型,预算将自动关联。
';
- }
- }
- }
- currentArrayItem.remove(); // 删除模型项本身
- });
-
- // 将包装器(包含输入框和可能的生成按钮)和删除按钮添加到主容器
arrayItem.appendChild(inputWrapper);
arrayItem.appendChild(removeBtn);
-
- // 插入到容器末尾
container.appendChild(arrayItem);
- // 返回生成的 ID (如果是思考模型) 或 null
+ // Initialize sensitive field if applicable
+ if (isSensitive && input.value) {
+ if (configForm && typeof initializeSensitiveFields === 'function') {
+ const focusoutEvent = new Event('focusout', { bubbles: true, cancelable: true });
+ input.dispatchEvent(focusoutEvent);
+ }
+ }
return isThinkingModel ? modelId : null;
- // Note: This function no longer automatically calls createAndAppendBudgetMapItem
}
-// --- 新增:专门用于创建和添加预算映射 DOM 元素 ---
+/**
+ * Creates and appends a DOM element for a thinking model's budget mapping.
+ * @param {string} mapKey - The model name (key for the map).
+ * @param {number|string} mapValue - The budget value.
+ * @param {string} modelId - The unique ID of the corresponding thinking model.
+ */
function createAndAppendBudgetMapItem(mapKey, mapValue, modelId) {
const container = document.getElementById('THINKING_BUDGET_MAP_container');
if (!container) {
@@ -897,37 +941,33 @@ function createAndAppendBudgetMapItem(mapKey, mapValue, modelId) {
}
const mapItem = document.createElement('div');
- mapItem.className = 'map-item flex items-center mb-2 gap-2';
- mapItem.setAttribute('data-model-id', modelId); // Add ID attribute
+ mapItem.className = `${MAP_ITEM_CLASS} flex items-center mb-2 gap-2`;
+ mapItem.setAttribute('data-model-id', modelId);
- // Key Input (Model Name) - Read-only
const keyInput = document.createElement('input');
keyInput.type = 'text';
keyInput.value = mapKey;
keyInput.placeholder = '模型名称 (自动关联)';
keyInput.readOnly = true;
- keyInput.className = 'map-key-input flex-grow px-3 py-2 border border-gray-300 rounded-md focus:outline-none bg-gray-100 text-gray-500';
+ keyInput.className = `${MAP_KEY_INPUT_CLASS} flex-grow px-3 py-2 border border-gray-300 rounded-md focus:outline-none bg-gray-100 text-gray-500`;
keyInput.setAttribute('data-model-id', modelId);
- // Value Input (Budget) - Integer
const valueInput = document.createElement('input');
valueInput.type = 'number';
- // Ensure mapValue is treated as integer, default to 0 if invalid
const intValue = parseInt(mapValue, 10);
valueInput.value = isNaN(intValue) ? 0 : intValue;
valueInput.placeholder = '预算 (整数)';
- valueInput.className = 'map-value-input w-24 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50';
- valueInput.min = 0; // 添加最小值
- valueInput.max = 24576; // 添加最大值
+ valueInput.className = `${MAP_VALUE_INPUT_CLASS} w-24 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50`;
+ valueInput.min = 0;
+ valueInput.max = 24576;
valueInput.addEventListener('input', function() {
- // 限制输入为0到24576之间的整数
- let value = this.value.replace(/[^0-9]/g, '');
- if (value !== '') {
- value = parseInt(value, 10);
- if (value < 0) value = 0;
- if (value > 24576) value = 24576;
- }
- this.value = value;
+ let val = this.value.replace(/[^0-9]/g, '');
+ if (val !== '') {
+ val = parseInt(val, 10);
+ if (val < 0) val = 0;
+ if (val > 24576) val = 24576;
+ }
+ this.value = val; // Corrected variable name
});
// Remove Button - Removed for budget map items
@@ -944,62 +984,62 @@ function createAndAppendBudgetMapItem(mapKey, mapValue, modelId) {
container.appendChild(mapItem);
}
-// --- 结束:专门的预算映射项创建函数 ---
-
-// 收集表单数据
+/**
+ * Collects all data from the configuration form.
+ * @returns {object} An object containing all configuration data.
+ */
function collectFormData() {
const formData = {};
- // 处理普通输入
- const inputs = document.querySelectorAll('input[type="text"], input[type="number"], select');
- inputs.forEach(input => {
- if (!input.name.includes('[]')) {
- if (input.type === 'number') {
- formData[input.name] = parseFloat(input.value);
+ // 处理普通输入和 select
+ const inputsAndSelects = document.querySelectorAll('input[type="text"], input[type="number"], input[type="password"], select, textarea');
+ inputsAndSelects.forEach(element => {
+ if (element.name && !element.name.includes('[]') && !element.closest('.array-container') && !element.closest(`.${MAP_ITEM_CLASS}`) && !element.closest(`.${SAFETY_SETTING_ITEM_CLASS}`)) {
+ if (element.type === 'number') {
+ formData[element.name] = parseFloat(element.value);
+ } else if (element.classList.contains(SENSITIVE_INPUT_CLASS) && element.hasAttribute('data-real-value')) {
+ formData[element.name] = element.getAttribute('data-real-value');
} else {
- // 确保 select 元素的值也被正确收集
- formData[input.name] = input.value;
+ formData[element.name] = element.value;
}
}
});
- // 处理复选框
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(checkbox => {
formData[checkbox.name] = checkbox.checked;
});
- // 处理数组
const arrayContainers = document.querySelectorAll('.array-container');
arrayContainers.forEach(container => {
const key = container.id.replace('_container', '');
- const arrayInputs = container.querySelectorAll('.array-input');
- formData[key] = Array.from(arrayInputs).map(input => input.value).filter(value => value.trim() !== '');
+ const arrayInputs = container.querySelectorAll(`.${ARRAY_INPUT_CLASS}`);
+ formData[key] = Array.from(arrayInputs).map(input => {
+ if (input.classList.contains(SENSITIVE_INPUT_CLASS) && input.hasAttribute('data-real-value')) {
+ return input.getAttribute('data-real-value');
+ }
+ return input.value;
+ }).filter(value => value && value.trim() !== '' && value !== MASKED_VALUE); // Ensure MASKED_VALUE is also filtered if not handled
});
- // --- 新增:处理 THINKING_BUDGET_MAP ---
const budgetMapContainer = document.getElementById('THINKING_BUDGET_MAP_container');
if (budgetMapContainer) {
formData['THINKING_BUDGET_MAP'] = {};
- const mapItems = budgetMapContainer.querySelectorAll('.map-item');
+ const mapItems = budgetMapContainer.querySelectorAll(`.${MAP_ITEM_CLASS}`);
mapItems.forEach(item => {
- const keyInput = item.querySelector('.map-key-input');
- const valueInput = item.querySelector('.map-value-input');
+ const keyInput = item.querySelector(`.${MAP_KEY_INPUT_CLASS}`);
+ const valueInput = item.querySelector(`.${MAP_VALUE_INPUT_CLASS}`);
if (keyInput && valueInput && keyInput.value.trim() !== '') {
- // 将预算值解析为整数
- const budgetValue = parseInt(valueInput.value, 10); // 使用基数10
- // 检查是否为有效数字,如果不是则默认为 0
+ const budgetValue = parseInt(valueInput.value, 10);
formData['THINKING_BUDGET_MAP'][keyInput.value.trim()] = isNaN(budgetValue) ? 0 : budgetValue;
}
});
}
- // --- 结束:处理 THINKING_BUDGET_MAP ---
- // --- 新增:处理 SAFETY_SETTINGS ---
if (safetySettingsContainer) {
formData['SAFETY_SETTINGS'] = [];
- const settingItems = safetySettingsContainer.querySelectorAll('.safety-setting-item');
+ const settingItems = safetySettingsContainer.querySelectorAll(`.${SAFETY_SETTING_ITEM_CLASS}`);
settingItems.forEach(item => {
const categorySelect = item.querySelector('.safety-category-select');
const thresholdSelect = item.querySelector('.safety-threshold-select');
@@ -1011,12 +1051,13 @@ function collectFormData() {
}
});
}
- // --- 结束:处理 SAFETY_SETTINGS ---
return formData;
}
-// 辅助函数:停止定时任务
+/**
+ * Stops the scheduler task on the server.
+ */
async function stopScheduler() {
try {
const response = await fetch('/api/scheduler/stop', { method: 'POST' });
@@ -1030,7 +1071,9 @@ async function stopScheduler() {
}
}
-// 辅助函数:启动定时任务
+/**
+ * Starts the scheduler task on the server.
+ */
async function startScheduler() {
try {
const response = await fetch('/api/scheduler/start', { method: 'POST' });
@@ -1044,7 +1087,9 @@ async function startScheduler() {
}
}
-// 保存配置
+/**
+ * Saves the current configuration to the server.
+ */
async function saveConfig() {
try {
const formData = collectFormData();
@@ -1086,7 +1131,10 @@ async function saveConfig() {
}
}
-// 重置配置 (现在只负责打开模态框)
+/**
+ * Initiates the configuration reset process by showing a confirmation modal.
+ * @param {Event} [event] - The click event, if triggered by a button.
+ */
function resetConfig(event) {
// 阻止事件冒泡和默认行为
if (event) {
@@ -1096,25 +1144,22 @@ function resetConfig(event) {
console.log('resetConfig called. Event target:', event ? event.target.id : 'No event');
- // 确保只有当事件来自重置按钮时才显示模态框
- if (!event || event.target.id === 'resetBtn' || event.currentTarget.id === 'resetBtn') {
- const resetConfirmModal = document.getElementById('resetConfirmModal');
+ // Ensure modal is shown only if the event comes from the reset button
+ if (!event || event.target.id === 'resetBtn' || (event.currentTarget && event.currentTarget.id === 'resetBtn')) {
if (resetConfirmModal) {
- resetConfirmModal.classList.add('show');
+ openModal(resetConfirmModal);
} else {
- // Fallback if modal doesn't exist for some reason
console.error("Reset confirmation modal not found! Falling back to default confirm.");
- // Fallback to original confirm behavior
- if (!confirm('确定要重置所有配置吗?这将恢复到默认值。')) {
- return;
+ if (confirm('确定要重置所有配置吗?这将恢复到默认值。')) {
+ executeReset();
}
- // If confirmed, proceed with the reset logic directly (less ideal)
- executeReset();
}
}
}
-// --- 新增:将实际重置逻辑提取到一个单独的函数 ---
+/**
+ * Executes the actual configuration reset after confirmation.
+ */
async function executeReset() {
try {
showNotification('正在重置配置...', 'info');
@@ -1127,6 +1172,18 @@ async function executeReset() {
}
const config = await response.json();
populateForm(config);
+ // Re-initialize masking for sensitive fields after reset
+ if (configForm && typeof initializeSensitiveFields === 'function') {
+ const sensitiveFields = configForm.querySelectorAll(`.${SENSITIVE_INPUT_CLASS}`);
+ sensitiveFields.forEach(field => {
+ if (field.type === 'password') {
+ if (field.value) field.setAttribute('data-real-value', field.value);
+ } else if (field.type === 'text' || field.tagName.toLowerCase() === 'textarea') {
+ const focusoutEvent = new Event('focusout', { bubbles: true, cancelable: true });
+ field.dispatchEvent(focusoutEvent);
+ }
+ });
+ }
showNotification('配置已重置为默认值', 'success');
// 3. 启动新的定时任务
@@ -1139,7 +1196,12 @@ async function executeReset() {
await startScheduler();
}
}
-// 显示通知
+
+/**
+ * Displays a notification message to the user.
+ * @param {string} message - The message to display.
+ * @param {string} [type='info'] - The type of notification ('info', 'success', 'error', 'warning').
+ */
function showNotification(message, type = 'info') {
const notification = document.getElementById('notification');
notification.textContent = message;
@@ -1161,64 +1223,58 @@ function showNotification(message, type = 'info') {
}, 3000);
}
-// 刷新页面
+/**
+ * Refreshes the current page.
+ * @param {HTMLButtonElement} [button] - The button that triggered the refresh (to show loading state).
+ */
function refreshPage(button) {
- button.classList.add('loading');
+ if (button) button.classList.add('loading');
location.reload();
}
-// 滚动到顶部
+/**
+ * Scrolls the page to the top.
+ */
function scrollToTop() {
- window.scrollTo({
- top: 0,
- behavior: 'smooth'
- });
+ window.scrollTo({ top: 0, behavior: 'smooth' });
}
-// 滚动到底部
+/**
+ * Scrolls the page to the bottom.
+ */
function scrollToBottom() {
- window.scrollTo({
- top: document.body.scrollHeight,
- behavior: 'smooth'
- });
+ window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
}
-// 切换滚动按钮显示
+/**
+ * Toggles the visibility of scroll-to-top/bottom buttons based on scroll position.
+ */
function toggleScrollButtons() {
const scrollButtons = document.querySelector('.scroll-buttons');
-
- if (window.scrollY > 200) {
- scrollButtons.style.display = 'flex';
- } else {
- scrollButtons.style.display = 'none';
+ if (scrollButtons) {
+ scrollButtons.style.display = (window.scrollY > 200) ? 'flex' : 'none';
}
}
-// --- 新增:生成随机令牌函数 ---
+/**
+ * Generates a random token string.
+ * @returns {string} A randomly generated token.
+ */
function generateRandomToken() {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_';
const length = 48;
let result = 'sk-';
- const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
- result += characters.charAt(Math.floor(Math.random() * charactersLength));
+ result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}
-// --- 结束:生成随机令牌函数 ---
-
-// Deprecated: This function is now effectively replaced by createAndAppendBudgetMapItem
-// for the initial population logic. It delegates to the new function if called.
-function addBudgetMapItemWithValue(mapKey, mapValue, modelId) {
- // console.warn("Deprecated call to addBudgetMapItemWithValue, use createAndAppendBudgetMapItem instead for population.");
- // Delegate to the new function which handles DOM creation
- createAndAppendBudgetMapItem(mapKey, mapValue, modelId);
-}
-/* --- 结束:(addBudgetMapItemWithValue 已弃用) --- */
-
-
-// --- 新增:添加安全设置项的函数 ---
+/**
+ * Adds a new safety setting item to the DOM.
+ * @param {string} [category=''] - The initial category for the setting.
+ * @param {string} [threshold=''] - The initial threshold for the setting.
+ */
function addSafetySettingItem(category = '', threshold = '') {
const container = document.getElementById('SAFETY_SETTINGS_container');
if (!container) {
@@ -1248,48 +1304,34 @@ function addSafetySettingItem(category = '', threshold = '') {
];
const settingItem = document.createElement('div');
- settingItem.className = 'safety-setting-item flex items-center mb-2 gap-2';
+ settingItem.className = `${SAFETY_SETTING_ITEM_CLASS} flex items-center mb-2 gap-2`;
- // Category Select
const categorySelect = document.createElement('select');
categorySelect.className = 'safety-category-select flex-grow px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 bg-white';
harmCategories.forEach(cat => {
const option = document.createElement('option');
option.value = cat;
- option.textContent = cat.replace('HARM_CATEGORY_', ''); // 显示更友好的名称
- if (cat === category) {
- option.selected = true;
- }
+ option.textContent = cat.replace('HARM_CATEGORY_', '');
+ if (cat === category) option.selected = true;
categorySelect.appendChild(option);
});
- // Threshold Select
const thresholdSelect = document.createElement('select');
thresholdSelect.className = 'safety-threshold-select w-48 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 bg-white';
harmThresholds.forEach(thr => {
const option = document.createElement('option');
option.value = thr;
- option.textContent = thr.replace('BLOCK_', '').replace('_AND_ABOVE', '+'); // 简化显示
- if (thr === threshold) {
- option.selected = true;
- }
+ option.textContent = thr.replace('BLOCK_', '').replace('_AND_ABOVE', '+');
+ if (thr === threshold) option.selected = true;
thresholdSelect.appendChild(option);
});
- // Remove Button
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'remove-btn text-gray-400 hover:text-red-500 focus:outline-none transition-colors duration-150';
removeBtn.innerHTML = '';
removeBtn.title = '删除此设置';
- removeBtn.addEventListener('click', function() {
- const currentItem = this.closest('.safety-setting-item');
- currentItem.remove();
- // 检查容器是否为空,如果是,则添加回占位符
- if (container.children.length === 0) {
- container.innerHTML = '
定义模型的安全过滤阈值。
';
- }
- });
+ // Event listener for removeBtn is now handled by event delegation in DOMContentLoaded
settingItem.appendChild(categorySelect);
settingItem.appendChild(thresholdSelect);
@@ -1297,5 +1339,3 @@ function addSafetySettingItem(category = '', threshold = '') {
container.appendChild(settingItem);
}
-// --- 结束:添加安全设置项的函数 ---
-
diff --git a/app/static/js/error_logs.js b/app/static/js/error_logs.js
index 64d13e0..69a1d74 100644
--- a/app/static/js/error_logs.js
+++ b/app/static/js/error_logs.js
@@ -9,24 +9,67 @@ function scrollToBottom() {
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
}
+// API 调用辅助函数
+async function fetchAPI(url, options = {}) {
+ try {
+ const response = await fetch(url, options);
+
+ // Handle cases where response might be empty but still ok (e.g., 204 No Content for DELETE)
+ if (response.status === 204) {
+ return null; // Indicate success with no content
+ }
+
+ let responseData;
+ try {
+ responseData = await response.json();
+ } catch (e) {
+ // Handle non-JSON responses if necessary, or assume error if JSON expected
+ if (!response.ok) {
+ // If response is not ok and not JSON, use statusText
+ throw new Error(`HTTP error! status: ${response.status} - ${response.statusText}`);
+ }
+ // If response is ok but not JSON, maybe return raw text or handle differently
+ // For now, let's assume successful non-JSON is not expected or handled later
+ console.warn("Response was not JSON for URL:", url);
+ return await response.text(); // Or handle as needed
+ }
+
+
+ if (!response.ok) {
+ // Prefer error message from API response body if available
+ const message = responseData?.detail || `HTTP error! status: ${response.status} - ${response.statusText}`;
+ throw new Error(message);
+ }
+
+ return responseData; // Return parsed JSON data for successful responses
+
+ } catch (error) {
+ // Catch network errors or errors thrown from above
+ console.error('API Call Failed:', error.message, 'URL:', url, 'Options:', options);
+ // Re-throw the error so the calling function knows the operation failed
+ throw error;
+ }
+}
+
// Refresh function removed as the buttons are gone.
// If refresh functionality is needed elsewhere, it can be triggered directly by calling loadErrorLogs().
-// 全局变量
-let currentPage = 1;
-let pageSize = 10;
-// let totalPages = 1; // totalPages will be calculated dynamically based on API response if available, or based on fetched data length
-let errorLogs = []; // Store fetched logs for details view
-let currentSort = { // 新增:存储当前排序状态
- field: 'id', // 默认按 ID 排序
- order: 'desc' // 默认降序
-};
-let currentSearch = { // Store current search parameters
- key: '',
- error: '',
- errorCode: '', // Added error code search
- startDate: '',
- endDate: ''
+// 全局状态管理
+let errorLogState = {
+ currentPage: 1,
+ pageSize: 10,
+ logs: [], // 存储获取的日志
+ sort: {
+ field: 'id', // 默认按 ID 排序
+ order: 'desc' // 默认降序
+ },
+ search: {
+ key: '',
+ error: '',
+ errorCode: '',
+ startDate: '',
+ endDate: ''
+ }
};
// DOM Elements Cache
@@ -60,73 +103,70 @@ let confirmDeleteBtn; // 新增:确认删除按钮
let deleteConfirmMessage; // 新增:删除确认消息元素
let idsToDeleteGlobally = []; // 新增:存储待删除的ID
-// 页面加载完成后执行
-document.addEventListener('DOMContentLoaded', function() {
- // Cache DOM elements
+// Helper functions for initialization
+function cacheDOMElements() {
pageSizeSelector = document.getElementById('pageSize');
- // refreshBtn = document.getElementById('refreshBtn'); // Removed
tableBody = document.getElementById('errorLogsTable');
paginationElement = document.getElementById('pagination');
loadingIndicator = document.getElementById('loadingIndicator');
noDataMessage = document.getElementById('noDataMessage');
errorMessage = document.getElementById('errorMessage');
logDetailModal = document.getElementById('logDetailModal');
- // Get all elements that should close the modal
modalCloseBtns = document.querySelectorAll('#closeLogDetailModalBtn, #closeModalFooterBtn');
keySearchInput = document.getElementById('keySearch');
errorSearchInput = document.getElementById('errorSearch');
- errorCodeSearchInput = document.getElementById('errorCodeSearch'); // Get error code input
+ errorCodeSearchInput = document.getElementById('errorCodeSearch');
startDateInput = document.getElementById('startDate');
endDateInput = document.getElementById('endDate');
searchBtn = document.getElementById('searchBtn');
pageInput = document.getElementById('pageInput');
goToPageBtn = document.getElementById('goToPageBtn');
- selectAllCheckbox = document.getElementById('selectAllCheckbox'); // 新增
- copySelectedKeysBtn = document.getElementById('copySelectedKeysBtn'); // 新增
- deleteSelectedBtn = document.getElementById('deleteSelectedBtn'); // 新增
- sortByIdHeader = document.getElementById('sortById'); // 新增
+ selectAllCheckbox = document.getElementById('selectAllCheckbox');
+ copySelectedKeysBtn = document.getElementById('copySelectedKeysBtn');
+ deleteSelectedBtn = document.getElementById('deleteSelectedBtn');
+ sortByIdHeader = document.getElementById('sortById');
if (sortByIdHeader) {
- sortIcon = sortByIdHeader.querySelector('i'); // 新增
+ sortIcon = sortByIdHeader.querySelector('i');
}
- selectedCountSpan = document.getElementById('selectedCount'); // 新增
- deleteConfirmModal = document.getElementById('deleteConfirmModal'); // 新增
- closeDeleteConfirmModalBtn = document.getElementById('closeDeleteConfirmModalBtn'); // 新增
- cancelDeleteBtn = document.getElementById('cancelDeleteBtn'); // 新增
- confirmDeleteBtn = document.getElementById('confirmDeleteBtn'); // 新增
- deleteConfirmMessage = document.getElementById('deleteConfirmMessage'); // 新增
+ selectedCountSpan = document.getElementById('selectedCount');
+ deleteConfirmModal = document.getElementById('deleteConfirmModal');
+ closeDeleteConfirmModalBtn = document.getElementById('closeDeleteConfirmModalBtn');
+ cancelDeleteBtn = document.getElementById('cancelDeleteBtn');
+ confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
+ deleteConfirmMessage = document.getElementById('deleteConfirmMessage');
+}
- // Initialize page size selector
+function initializePageSizeControls() {
if (pageSizeSelector) {
- pageSizeSelector.value = pageSize;
+ pageSizeSelector.value = errorLogState.pageSize;
pageSizeSelector.addEventListener('change', function() {
- pageSize = parseInt(this.value);
- currentPage = 1; // Reset to first page
+ errorLogState.pageSize = parseInt(this.value);
+ errorLogState.currentPage = 1; // Reset to first page
loadErrorLogs();
});
}
+}
- // Refresh button event listener removed
-
- // Initialize search button
+function initializeSearchControls() {
if (searchBtn) {
searchBtn.addEventListener('click', function() {
- // Update search parameters from input fields
- currentSearch.key = keySearchInput ? keySearchInput.value.trim() : '';
- currentSearch.error = errorSearchInput ? errorSearchInput.value.trim() : '';
- currentSearch.errorCode = errorCodeSearchInput ? errorCodeSearchInput.value.trim() : ''; // Get error code value
- currentSearch.startDate = startDateInput ? startDateInput.value : '';
- currentSearch.endDate = endDateInput ? endDateInput.value : '';
- currentPage = 1; // Reset to first page on new search
+ errorLogState.search.key = keySearchInput ? keySearchInput.value.trim() : '';
+ errorLogState.search.error = errorSearchInput ? errorSearchInput.value.trim() : '';
+ errorLogState.search.errorCode = errorCodeSearchInput ? errorCodeSearchInput.value.trim() : '';
+ errorLogState.search.startDate = startDateInput ? startDateInput.value : '';
+ errorLogState.search.endDate = endDateInput ? endDateInput.value : '';
+ errorLogState.currentPage = 1; // Reset to first page on new search
loadErrorLogs();
});
}
+}
- // Initialize modal close buttons
+function initializeModalControls() {
+ // Log Detail Modal
if (logDetailModal && modalCloseBtns) {
modalCloseBtns.forEach(btn => {
btn.addEventListener('click', closeLogDetailModal);
});
- // Optional: Close modal if clicking outside the content
logDetailModal.addEventListener('click', function(event) {
if (event.target === logDetailModal) {
closeLogDetailModal();
@@ -134,52 +174,7 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
- // Initial load of error logs
- loadErrorLogs();
-
- // Add event listeners for copy buttons inside the modal and table
- setupCopyButtons(); // This will now also handle table copy buttons if called after render
-
- // Add event listeners for bulk selection
- setupBulkSelectionListeners(); // 新增:设置批量选择监听器
-
- // 新增:为页码跳转按钮添加事件监听器
- if (goToPageBtn && pageInput) {
- goToPageBtn.addEventListener('click', function() {
- const targetPage = parseInt(pageInput.value);
- // 需要获取总页数来验证输入
- // 暂时无法直接获取 totalPages,需要在 updatePagination 中存储或重新计算
- // 简单的验证:必须是正整数
- if (!isNaN(targetPage) && targetPage >= 1) {
- // 理想情况下,应检查 targetPage <= totalPages
- // 但 totalPages 可能未知,所以暂时只跳转
- currentPage = targetPage;
- loadErrorLogs();
- pageInput.value = ''; // 清空输入框
- } else {
- showNotification('请输入有效的页码', 'error', 2000);
- pageInput.value = ''; // 清空无效输入
- }
- });
- // 允许按 Enter 键跳转
- pageInput.addEventListener('keypress', function(event) {
- if (event.key === 'Enter') {
- goToPageBtn.click(); // 触发按钮点击
- }
- });
- }
-
- // 新增:为批量删除按钮添加事件监听器
- if (deleteSelectedBtn) {
- deleteSelectedBtn.addEventListener('click', handleDeleteSelected);
- }
-
- // 新增:为 ID 排序表头添加事件监听器
- if (sortByIdHeader) {
- sortByIdHeader.addEventListener('click', handleSortById);
- }
-
- // 新增:为删除确认模态框按钮添加事件监听器
+ // Delete Confirm Modal
if (closeDeleteConfirmModalBtn) {
closeDeleteConfirmModalBtn.addEventListener('click', hideDeleteConfirmModal);
}
@@ -189,7 +184,6 @@ document.addEventListener('DOMContentLoaded', function() {
if (confirmDeleteBtn) {
confirmDeleteBtn.addEventListener('click', handleConfirmDelete);
}
- // Optional: Close modal if clicking outside the content
if (deleteConfirmModal) {
deleteConfirmModal.addEventListener('click', function(event) {
if (event.target === deleteConfirmModal) {
@@ -197,6 +191,55 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
}
+}
+
+function initializePaginationJumpControls() {
+ if (goToPageBtn && pageInput) {
+ goToPageBtn.addEventListener('click', function() {
+ const targetPage = parseInt(pageInput.value);
+ if (!isNaN(targetPage) && targetPage >= 1) {
+ errorLogState.currentPage = targetPage;
+ loadErrorLogs();
+ pageInput.value = '';
+ } else {
+ showNotification('请输入有效的页码', 'error', 2000);
+ pageInput.value = '';
+ }
+ });
+ pageInput.addEventListener('keypress', function(event) {
+ if (event.key === 'Enter') {
+ goToPageBtn.click();
+ }
+ });
+ }
+}
+
+function initializeActionControls() {
+ if (deleteSelectedBtn) {
+ deleteSelectedBtn.addEventListener('click', handleDeleteSelected);
+ }
+ if (sortByIdHeader) {
+ sortByIdHeader.addEventListener('click', handleSortById);
+ }
+ // Bulk selection listeners are closely related to actions
+ setupBulkSelectionListeners();
+}
+
+// 页面加载完成后执行
+document.addEventListener('DOMContentLoaded', function() {
+ cacheDOMElements();
+ initializePageSizeControls();
+ initializeSearchControls();
+ initializeModalControls();
+ initializePaginationJumpControls();
+ initializeActionControls();
+
+ // Initial load of error logs
+ loadErrorLogs();
+
+ // Add event listeners for copy buttons inside the modal and table
+ // This needs to be called after initial render and potentially after each render if content is dynamic
+ setupCopyButtons();
});
// 新增:显示删除确认模态框
@@ -265,6 +308,36 @@ function handleCopyResult(buttonElement, success) {
setTimeout(() => { iconElement.className = originalIcon; }, success ? 2000 : 3000); // Restore original icon class
}
+// 新的内部辅助函数,封装实际的复制操作和反馈
+function _performCopy(text, buttonElement) {
+ let copySuccess = false;
+ if (navigator.clipboard && window.isSecureContext) {
+ navigator.clipboard.writeText(text).then(() => {
+ if (buttonElement) {
+ handleCopyResult(buttonElement, true);
+ } else {
+ showNotification('已复制到剪贴板', 'success');
+ }
+ }).catch(err => {
+ console.error('Clipboard API failed, attempting fallback:', err);
+ copySuccess = fallbackCopyTextToClipboard(text);
+ if (buttonElement) {
+ handleCopyResult(buttonElement, copySuccess);
+ } else {
+ showNotification(copySuccess ? '已复制到剪贴板' : '复制失败', copySuccess ? 'success' : 'error');
+ }
+ });
+ } else {
+ console.warn("Clipboard API not available or context insecure. Using fallback copy method.");
+ copySuccess = fallbackCopyTextToClipboard(text);
+ if (buttonElement) {
+ handleCopyResult(buttonElement, copySuccess);
+ } else {
+ showNotification(copySuccess ? '已复制到剪贴板' : '复制失败', copySuccess ? 'success' : 'error');
+ }
+ }
+}
+
// Function to set up copy button listeners (using modern API with fallback) - Updated to handle table copy buttons
function setupCopyButtons(containerSelector = 'body') {
// Find buttons within the specified container (defaults to body)
@@ -306,44 +379,13 @@ function handleCopyButtonClick() {
if (textToCopy) {
- let copySuccess = false;
- // Try modern clipboard API first (requires HTTPS or localhost)
- if (navigator.clipboard && window.isSecureContext) {
- navigator.clipboard.writeText(textToCopy).then(() => {
- handleCopyResult(button, true); // Use helper for feedback
- }).catch(err => {
- console.error('Clipboard API failed, attempting fallback:', err);
- // Attempt fallback if modern API fails
- copySuccess = fallbackCopyTextToClipboard(textToCopy);
- handleCopyResult(button, copySuccess); // Use helper for feedback
- });
- } else {
- // Use fallback if modern API is not available or context is insecure
- console.warn("Clipboard API not available or context insecure. Using fallback copy method.");
- copySuccess = fallbackCopyTextToClipboard(textToCopy);
- handleCopyResult(button, copySuccess); // Use helper for feedback
- }
+ _performCopy(textToCopy, button); // 使用新的辅助函数
} else {
console.warn('No text found to copy for target:', targetId || 'direct text');
showNotification('没有内容可复制', 'warning');
}
} // End of handleCopyButtonClick function
-// Function to set up copy button listeners (using modern API with fallback) - Updated to handle table copy buttons
-function setupCopyButtons(containerSelector = 'body') {
- // Find buttons within the specified container (defaults to body)
- const container = document.querySelector(containerSelector);
- if (!container) return;
-
- const copyButtons = container.querySelectorAll('.copy-btn');
- copyButtons.forEach(button => {
- // Remove existing listener to prevent duplicates if called multiple times
- button.removeEventListener('click', handleCopyButtonClick);
- // Add the listener
- button.addEventListener('click', handleCopyButtonClick);
- });
-}
-
// 新增:设置批量选择相关的事件监听器
function setupBulkSelectionListeners() {
if (selectAllCheckbox) {
@@ -432,33 +474,12 @@ function handleCopySelectedKeys() {
if (keysToCopy.length > 0) {
const textToCopy = keysToCopy.join('\n'); // 每行一个密钥
- copyTextToClipboard(textToCopy, copySelectedKeysBtn); // 使用通用复制函数
+ _performCopy(textToCopy, copySelectedKeysBtn); // 使用新的辅助函数
} else {
showNotification('没有选中的密钥可复制', 'warning');
}
}
-// 新增:通用的文本复制函数(结合现有逻辑)
-function copyTextToClipboard(text, buttonElement = null) {
- let copySuccess = false;
- if (navigator.clipboard && window.isSecureContext) {
- navigator.clipboard.writeText(text).then(() => {
- if (buttonElement) handleCopyResult(buttonElement, true);
- else showNotification('已复制到剪贴板', 'success');
- }).catch(err => {
- console.error('Clipboard API failed, attempting fallback:', err);
- copySuccess = fallbackCopyTextToClipboard(text);
- if (buttonElement) handleCopyResult(buttonElement, copySuccess);
- else showNotification(copySuccess ? '已复制到剪贴板' : '复制失败', copySuccess ? 'success' : 'error');
- });
- } else {
- console.warn("Clipboard API not available or context insecure. Using fallback copy method.");
- copySuccess = fallbackCopyTextToClipboard(text);
- if (buttonElement) handleCopyResult(buttonElement, copySuccess);
- else showNotification(copySuccess ? '已复制到剪贴板' : '复制失败', copySuccess ? 'success' : 'error');
- }
-}
-
// 修改:处理批量删除按钮点击的函数 - 改为显示模态框
function handleDeleteSelected() {
const selectedCheckboxes = tableBody.querySelectorAll('.row-checkbox:checked');
@@ -495,23 +516,17 @@ async function performActualDelete(logIds) {
const method = 'DELETE';
const body = isSingleDelete ? null : JSON.stringify({ ids: logIds });
const headers = isSingleDelete ? {} : { 'Content-Type': 'application/json' };
+ const options = {
+ method: method,
+ headers: headers,
+ body: body, // fetchAPI handles null body correctly
+ };
try {
- // Rename 'response' to 'deleteResponse' and remove duplicate fetch
- const deleteResponse = await fetch(url, {
- method: method,
- headers: headers,
- body: body,
- });
- // Removed duplicate fetch call below
-
- if (!deleteResponse.ok) {
- let errorData;
- try { errorData = await deleteResponse.json(); } catch (e) { /* ignore */ }
- const actionText = isSingleDelete ? `删除该条日志` : `批量删除 ${logIds.length} 条日志`;
- throw new Error(errorData?.detail || `${actionText}失败: ${deleteResponse.statusText}`);
- }
+ // Use fetchAPI for the delete request
+ await fetchAPI(url, options); // fetchAPI returns null for 204 No Content
+ // If fetchAPI doesn't throw, the request was successful
const successMessage = isSingleDelete ? `成功删除该日志` : `成功删除 ${logIds.length} 条日志`;
showNotification(successMessage, 'success');
// 取消全选
@@ -537,18 +552,18 @@ function handleDeleteLogRow(logId) {
// 新增:处理 ID 排序点击的函数
function handleSortById() {
- if (currentSort.field === 'id') {
+ if (errorLogState.sort.field === 'id') {
// 如果当前是按 ID 排序,切换顺序
- currentSort.order = currentSort.order === 'asc' ? 'desc' : 'asc';
+ errorLogState.sort.order = errorLogState.sort.order === 'asc' ? 'desc' : 'asc';
} else {
// 如果当前不是按 ID 排序,切换到按 ID 排序,默认为降序
- currentSort.field = 'id';
- currentSort.order = 'desc';
+ errorLogState.sort.field = 'id';
+ errorLogState.sort.order = 'desc';
}
// 更新图标
updateSortIcon();
// 重新加载第一页数据
- currentPage = 1;
+ errorLogState.currentPage = 1;
loadErrorLogs();
}
@@ -558,8 +573,8 @@ function updateSortIcon() {
// 移除所有可能的排序类
sortIcon.classList.remove('fa-sort', 'fa-sort-up', 'fa-sort-down', 'text-gray-400', 'text-primary-600');
- if (currentSort.field === 'id') {
- sortIcon.classList.add(currentSort.order === 'asc' ? 'fa-sort-up' : 'fa-sort-down');
+ if (errorLogState.sort.field === 'id') {
+ sortIcon.classList.add(errorLogState.sort.order === 'asc' ? 'fa-sort-up' : 'fa-sort-down');
sortIcon.classList.add('text-primary-600'); // 高亮显示
} else {
// 如果不是按 ID 排序,显示默认图标
@@ -578,56 +593,49 @@ async function loadErrorLogs() {
showError(false);
showNoData(false);
- const offset = (currentPage - 1) * pageSize;
+ const offset = (errorLogState.currentPage - 1) * errorLogState.pageSize;
try {
// Construct the API URL with search and sort parameters
- let apiUrl = `/api/logs/errors?limit=${pageSize}&offset=${offset}`;
+ let apiUrl = `/api/logs/errors?limit=${errorLogState.pageSize}&offset=${offset}`;
// 添加排序参数
- apiUrl += `&sort_by=${currentSort.field}&sort_order=${currentSort.order}`;
+ apiUrl += `&sort_by=${errorLogState.sort.field}&sort_order=${errorLogState.sort.order}`;
// 添加搜索参数
- if (currentSearch.key) {
- apiUrl += `&key_search=${encodeURIComponent(currentSearch.key)}`;
+ if (errorLogState.search.key) {
+ apiUrl += `&key_search=${encodeURIComponent(errorLogState.search.key)}`;
}
- if (currentSearch.error) {
- apiUrl += `&error_search=${encodeURIComponent(currentSearch.error)}`;
+ if (errorLogState.search.error) {
+ apiUrl += `&error_search=${encodeURIComponent(errorLogState.search.error)}`;
}
- if (currentSearch.errorCode) { // Add error code to API request
- apiUrl += `&error_code_search=${encodeURIComponent(currentSearch.errorCode)}`;
+ if (errorLogState.search.errorCode) { // Add error code to API request
+ apiUrl += `&error_code_search=${encodeURIComponent(errorLogState.search.errorCode)}`;
}
- if (currentSearch.startDate) {
- apiUrl += `&start_date=${encodeURIComponent(currentSearch.startDate)}`;
+ if (errorLogState.search.startDate) {
+ apiUrl += `&start_date=${encodeURIComponent(errorLogState.search.startDate)}`;
}
- if (currentSearch.endDate) {
- apiUrl += `&end_date=${encodeURIComponent(currentSearch.endDate)}`;
+ if (errorLogState.search.endDate) {
+ apiUrl += `&end_date=${encodeURIComponent(errorLogState.search.endDate)}`;
}
- const response = await fetch(apiUrl);
- if (!response.ok) {
- // Try to get error message from response body
- let errorData;
- try {
- errorData = await response.json();
- } catch (e) {
- // Ignore if response is not JSON
- }
- throw new Error(errorData?.detail || `网络响应异常: ${response.statusText}`);
- }
- const data = await response.json();
+ // Use fetchAPI to get logs
+ const data = await fetchAPI(apiUrl);
+
// API 现在返回 { logs: [], total: count }
+ // fetchAPI already parsed JSON
if (data && Array.isArray(data.logs)) {
- errorLogs = data.logs; // Store the list data (contains error_code)
- renderErrorLogs(errorLogs);
- updatePagination(errorLogs.length, data.total || -1);
+ errorLogState.logs = data.logs; // Store the list data (contains error_code)
+ renderErrorLogs(errorLogState.logs);
+ updatePagination(errorLogState.logs.length, data.total || -1); // Use total from response
} else {
- throw new Error('无法识别的API响应格式');
+ // Handle unexpected data format even after successful fetch
+ console.error('Unexpected API response format:', data);
+ throw new Error('无法识别的API响应格式');
}
-
showLoading(false);
- if (errorLogs.length === 0) {
+ if (errorLogState.logs.length === 0) {
showNoData(true);
}
} catch (error) {
@@ -637,6 +645,54 @@ async function loadErrorLogs() {
}
}
+// Helper function to create HTML for a single log row
+function _createLogRowHtml(log, sequentialId) {
+ // Format date
+ let formattedTime = 'N/A';
+ try {
+ const requestTime = new Date(log.request_time);
+ if (!isNaN(requestTime)) {
+ formattedTime = requestTime.toLocaleString('zh-CN', {
+ year: 'numeric', month: '2-digit', day: '2-digit',
+ hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
+ });
+ }
+ } catch (e) { console.error("Error formatting date:", e); }
+
+ const errorCodeContent = log.error_code || '无';
+
+ const maskKey = (key) => {
+ if (!key || key.length < 8) return key || '无';
+ return `${key.substring(0, 4)}...${key.substring(key.length - 4)}`;
+ };
+ const maskedKey = maskKey(log.gemini_key);
+ const fullKey = log.gemini_key || '';
+
+ return `
+
+
+
+
${sequentialId}
+
+ ${maskedKey}
+
+
+
${log.error_type || '未知'}
+
${errorCodeContent}
+
${log.model_name || '未知'}
+
${formattedTime}
+
+
+
+
+ `;
+}
// 渲染错误日志表格
function renderErrorLogs(logs) {
@@ -654,61 +710,12 @@ function renderErrorLogs(logs) {
return;
}
- const startIndex = (currentPage - 1) * pageSize; // Calculate starting index for the current page
+ const startIndex = (errorLogState.currentPage - 1) * errorLogState.pageSize;
- logs.forEach((log, index) => { // Add index parameter to forEach
+ logs.forEach((log, index) => {
+ const sequentialId = startIndex + index + 1;
const row = document.createElement('tr');
- const sequentialId = startIndex + index + 1; // Calculate sequential ID for the current page
- // Format date
- let formattedTime = 'N/A';
- try {
- const requestTime = new Date(log.request_time);
- if (!isNaN(requestTime)) {
- formattedTime = requestTime.toLocaleString('zh-CN', {
- year: 'numeric', month: '2-digit', day: '2-digit',
- hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
- });
- }
- } catch (e) { console.error("Error formatting date:", e); }
-
-
- // Display error code instead of truncated log
- const errorCodeContent = log.error_code || '无';
-
- // Mask the Gemini key for display in the table
- const maskKey = (key) => {
- if (!key || key.length < 8) return key || '无'; // Don't mask short keys or null
- return `${key.substring(0, 4)}...${key.substring(key.length - 4)}`;
- };
- const maskedKey = maskKey(log.gemini_key);
- const fullKey = log.gemini_key || ''; // Store the full key
-
- row.innerHTML = `
-
-
-
-
${sequentialId}
-
- ${maskedKey}
-
-
-
-
${log.error_type || '未知'}
-
${errorCodeContent}
-
${log.model_name || '未知'}
-
${formattedTime}
-
-
-
-
- `;
-
+ row.innerHTML = _createLogRowHtml(log, sequentialId);
tableBody.appendChild(row);
});
@@ -751,15 +758,14 @@ async function showLogDetails(logId) {
document.body.style.overflow = 'hidden'; // Prevent body scrolling
try {
- const response = await fetch(`/api/logs/errors/${logId}/details`);
- if (!response.ok) {
- let errorData;
- try {
- errorData = await response.json();
- } catch (e) { /* ignore */ }
- throw new Error(errorData?.detail || `获取日志详情失败: ${response.statusText}`);
+ // Use fetchAPI to get log details
+ const logDetails = await fetchAPI(`/api/logs/errors/${logId}/details`);
+
+ // fetchAPI handles response.ok check and JSON parsing
+ if (!logDetails) {
+ // Handle case where API returns success but no data (if possible)
+ throw new Error('未找到日志详情');
}
- const logDetails = await response.json();
// Format date
let formattedTime = 'N/A';
@@ -839,8 +845,8 @@ function updatePagination(currentItemCount, totalItems) {
// Calculate total pages only if totalItems is known and valid
let totalPages = 1;
if (totalItems >= 0) {
- totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
- } else if (currentItemCount < pageSize && currentPage === 1) {
+ totalPages = Math.max(1, Math.ceil(totalItems / errorLogState.pageSize));
+ } else if (currentItemCount < errorLogState.pageSize && errorLogState.currentPage === 1) {
// If less items than page size fetched on page 1, assume it's the only page
totalPages = 1;
} else {
@@ -848,15 +854,15 @@ function updatePagination(currentItemCount, totalItems) {
// We can show Prev/Next based on current page and if items were returned
console.warn("Total item count unknown, pagination will be limited.");
// Basic Prev/Next for unknown total
- addPaginationLink(paginationElement, '«', currentPage > 1, () => { currentPage--; loadErrorLogs(); });
- addPaginationLink(paginationElement, currentPage.toString(), true, null, true); // Current page number (non-clickable)
- addPaginationLink(paginationElement, '»', currentItemCount === pageSize, () => { currentPage++; loadErrorLogs(); }); // Next enabled if full page was returned
+ addPaginationLink(paginationElement, '«', errorLogState.currentPage > 1, () => { errorLogState.currentPage--; loadErrorLogs(); });
+ addPaginationLink(paginationElement, errorLogState.currentPage.toString(), true, null, true); // Current page number (non-clickable)
+ addPaginationLink(paginationElement, '»', currentItemCount === errorLogState.pageSize, () => { errorLogState.currentPage++; loadErrorLogs(); }); // Next enabled if full page was returned
return; // Exit here for limited pagination
}
const maxPagesToShow = 5; // Max number of page links to show
- let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
+ let startPage = Math.max(1, errorLogState.currentPage - Math.floor(maxPagesToShow / 2));
let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
// Adjust startPage if endPage reaches the limit first
@@ -866,11 +872,11 @@ function updatePagination(currentItemCount, totalItems) {
// Previous Button
- addPaginationLink(paginationElement, '«', currentPage > 1, () => { currentPage--; loadErrorLogs(); });
+ addPaginationLink(paginationElement, '«', errorLogState.currentPage > 1, () => { errorLogState.currentPage--; loadErrorLogs(); });
// First Page Button
if (startPage > 1) {
- addPaginationLink(paginationElement, '1', true, () => { currentPage = 1; loadErrorLogs(); });
+ addPaginationLink(paginationElement, '1', true, () => { errorLogState.currentPage = 1; loadErrorLogs(); });
if (startPage > 2) {
addPaginationLink(paginationElement, '...', false); // Ellipsis
}
@@ -878,7 +884,7 @@ function updatePagination(currentItemCount, totalItems) {
// Page Number Buttons
for (let i = startPage; i <= endPage; i++) {
- addPaginationLink(paginationElement, i.toString(), true, () => { currentPage = i; loadErrorLogs(); }, i === currentPage);
+ addPaginationLink(paginationElement, i.toString(), true, () => { errorLogState.currentPage = i; loadErrorLogs(); }, i === errorLogState.currentPage);
}
// Last Page Button
@@ -886,12 +892,12 @@ function updatePagination(currentItemCount, totalItems) {
if (endPage < totalPages - 1) {
addPaginationLink(paginationElement, '...', false); // Ellipsis
}
- addPaginationLink(paginationElement, totalPages.toString(), true, () => { currentPage = totalPages; loadErrorLogs(); });
+ addPaginationLink(paginationElement, totalPages.toString(), true, () => { errorLogState.currentPage = totalPages; loadErrorLogs(); });
}
// Next Button
- addPaginationLink(paginationElement, '»', currentPage < totalPages, () => { currentPage++; loadErrorLogs(); });
+ addPaginationLink(paginationElement, '»', errorLogState.currentPage < totalPages, () => { errorLogState.currentPage++; loadErrorLogs(); });
}
// Helper function to add pagination links
diff --git a/app/static/js/keys_status.js b/app/static/js/keys_status.js
index 255f4bd..32355ca 100644
--- a/app/static/js/keys_status.js
+++ b/app/static/js/keys_status.js
@@ -27,6 +27,49 @@ function copyToClipboard(text) {
}
}
+// API 调用辅助函数 (与 error_logs.js 中的版本类似)
+async function fetchAPI(url, options = {}) {
+ try {
+ const response = await fetch(url, options);
+
+ if (response.status === 204) {
+ return null; // Indicate success with no content for DELETE etc.
+ }
+
+ let responseData;
+ try {
+ // Clone the response to allow reading it multiple times if needed (e.g., for text fallback)
+ const clonedResponse = response.clone();
+ responseData = await response.json();
+ } catch (e) {
+ // If JSON parsing fails, try to get text, especially if response wasn't ok
+ if (!response.ok) {
+ const textResponse = await response.text(); // Use original response for text
+ throw new Error(textResponse || `HTTP error! status: ${response.status} - ${response.statusText}`);
+ }
+ // If response is ok but not JSON, maybe return raw text or handle differently
+ console.warn("Response was not JSON for URL:", url);
+ // Consider returning text or null based on expected non-JSON success cases
+ return await response.text(); // Example: return text for non-JSON success
+ }
+
+
+ if (!response.ok) {
+ // Prefer error message from API response body (already parsed as JSON)
+ const message = responseData?.detail || responseData?.message || responseData?.error || `HTTP error! status: ${response.status}`;
+ throw new Error(message);
+ }
+
+ return responseData; // Return parsed JSON data
+
+ } catch (error) {
+ console.error('API Call Failed:', error.message, 'URL:', url, 'Options:', options);
+ // Re-throw the error so the calling function knows the operation failed
+ // Add more context if possible
+ throw new Error(`API请求失败: ${error.message}`);
+ }
+}
+
// 添加统计项动画效果
function initStatItemAnimations() {
const statItems = document.querySelectorAll('.stat-item');
@@ -132,7 +175,7 @@ function copyKey(key) {
});
}
-// 移除 showCopyStatus 函数,因为它已被 showNotification 替代
+// showCopyStatus 函数已废弃。
async function verifyKey(key, button) {
try {
@@ -142,13 +185,10 @@ async function verifyKey(key, button) {
button.innerHTML = ' 验证中';
try {
- const response = await fetch(`/gemini/v1beta/verify-key/${key}`, {
- method: 'POST'
- });
- const data = await response.json();
+ const data = await fetchAPI(`/gemini/v1beta/verify-key/${key}`, { method: 'POST' });
// 根据验证结果更新UI并显示模态提示框
- if (data.success || data.status === 'valid') {
+ if (data && (data.success || data.status === 'valid')) {
// 验证成功,显示成功结果
button.style.backgroundColor = '#27ae60';
// 使用结果模态框显示成功消息
@@ -161,9 +201,9 @@ async function verifyKey(key, button) {
// 使用结果模态框显示失败消息,改为true以在关闭时刷新
showResultModal(false, '密钥验证失败: ' + errorMsg, true);
}
- } catch (fetchError) {
- console.error('API请求失败:', fetchError);
- showResultModal(false, '验证请求失败: ' + fetchError.message, true); // 改为true以在关闭时刷新
+ } catch (apiError) {
+ console.error('密钥验证 API 请求失败:', apiError);
+ showResultModal(false, `验证请求失败: ${apiError.message}`, true);
} finally {
// 1秒后恢复按钮原始状态 (如果页面不刷新)
// 由于现在成功和失败都会刷新,这部分逻辑可以简化或移除
@@ -193,10 +233,7 @@ async function resetKeyFailCount(key, button) {
const originalHtml = button.innerHTML;
button.innerHTML = ' 重置中';
- const response = await fetch(`/gemini/v1beta/reset-fail-count/${key}`, {
- method: 'POST'
- });
- const data = await response.json();
+ const data = await fetchAPI(`/gemini/v1beta/reset-fail-count/${key}`, { method: 'POST' });
// 根据重置结果更新UI
if (data.success) {
@@ -220,9 +257,9 @@ async function resetKeyFailCount(key, button) {
// 恢复按钮状态逻辑已移至成功/失败分支内
- } catch (error) {
- console.error('重置失败:', error);
- showNotification('重置请求失败: ' + error.message, 'error');
+ } catch (apiError) {
+ console.error('重置失败:', apiError);
+ showNotification(`重置请求失败: ${apiError.message}`, 'error');
// 确保在捕获到错误时恢复按钮状态
button.disabled = false;
button.innerHTML = ' 重置'; // 恢复原始图标和文本
@@ -508,29 +545,12 @@ async function executeResetAll(type) {
resetButton.innerHTML = ' 重置中';
try {
- // 调用新的后端 API 来重置选定的密钥
- const response = await fetch(`/gemini/v1beta/reset-selected-fail-counts`, { // 假设的新 API 端点
+ const options = {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ keys: keysToReset, key_type: type }) // 发送密钥列表和类型
- });
-
- if (!response.ok) {
- // 尝试解析错误信息
- let errorMsg = `服务器返回错误: ${response.status}`;
- try {
- const errorData = await response.json();
- errorMsg = errorData.message || errorMsg;
- } catch (e) {
- // 如果解析失败,使用原始错误信息
- }
-
- throw new Error(errorMsg);
- }
-
- const data = await response.json();
+ 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) {
@@ -543,9 +563,9 @@ async function executeResetAll(type) {
// 失败后不自动刷新页面,让用户看到错误信息
showResultModal(false, '批量重置失败: ' + errorMsg, false);
}
- } catch (fetchError) {
- console.error('API请求失败:', fetchError);
- showResultModal(false, '批量重置请求失败: ' + fetchError.message, false); // 失败后不自动刷新
+ } 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('失败')) {
@@ -587,85 +607,106 @@ function refreshPage(button) {
}
-// 恢复之前的 toggleSection 函数以修复展开/收缩动画
+// 展开/收起区块内容的函数,带有平滑动画效果。
+// @param {HTMLElement} header - 被点击的区块头部元素。
+// @param {string} sectionId - (当前未使用,但可用于更精确的目标定位) 关联内容区块的ID。
function toggleSection(header, sectionId) {
const toggleIcon = header.querySelector('.toggle-icon');
- // 需要找到正确的 content 元素。它不再是紧邻的兄弟元素,而是 card 内的 key-content div
+ // 内容元素是卡片内的 .key-content div
const card = header.closest('.stats-card');
const content = card ? card.querySelector('.key-content') : null;
- const batchActions = card ? card.querySelector('[id$="BatchActions"]') : null; // 获取批量操作栏
- const pagination = card ? card.querySelector('[id$="PaginationControls"]') : 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');
+ if (!toggleIcon || !content) {
+ console.error("Toggle section failed: Icon or content element not found. Header:", header, "SectionId:", sectionId);
+ return;
+ }
- // 切换图标状态
- toggleIcon.classList.toggle('collapsed', !isCollapsed);
+ const isCollapsed = content.classList.contains('collapsed');
+ toggleIcon.classList.toggle('collapsed', !isCollapsed); // 更新箭头图标方向
- if (isCollapsed) {
- // 展开内容
- content.classList.remove('collapsed');
- // 先移除内联样式,让 CSS 控制初始状态
- content.style.maxHeight = '';
- content.style.opacity = '';
- content.style.padding = '';
- content.style.overflow = '';
- // 使用 requestAnimationFrame 确保浏览器应用了初始状态
- requestAnimationFrame(() => {
- // 计算内容的实际高度
- const scrollHeight = content.scrollHeight;
- let totalHeight = scrollHeight;
+ if (isCollapsed) {
+ // --- 准备展开动画 ---
+ content.classList.remove('collapsed'); // 移除 collapsed 类以应用展开的样式
- // 如果批量操作栏存在且可见,也计算其高度
- if (batchActions && !batchActions.classList.contains('hidden')) {
- totalHeight += batchActions.offsetHeight;
- }
- // 如果分页控件存在且可见,也计算其高度和 margin-top
- if (pagination && pagination.offsetHeight > 0) {
- // Assuming mt-4 which is 1rem = 16px (adjust if needed)
- totalHeight += pagination.offsetHeight + 16;
- }
+ // 步骤 1: 重置内联样式,让CSS控制初始的“隐藏”状态 (通常是 maxHeight: 0, opacity: 0)。
+ // 同时,确保 overflow 在动画开始前是 hidden。
+ content.style.maxHeight = ''; // 清除可能存在的内联 maxHeight
+ content.style.opacity = ''; // 清除可能存在的内联 opacity
+ content.style.paddingTop = ''; // 清除内联 padding
+ content.style.paddingBottom = '';
+ content.style.overflow = 'hidden'; // 动画过程中隐藏溢出内容
- content.style.maxHeight = totalHeight + 'px';
- content.style.opacity = '1';
- content.style.padding = '1rem'; // 恢复 padding
- content.style.overflow = 'hidden'; // Keep hidden during transition
+ // 步骤 2: 使用 requestAnimationFrame (rAF) 确保浏览器在计算 scrollHeight 之前
+ // 已经应用了上一步的样式重置(特别是如果CSS中有过渡效果)。
+ requestAnimationFrame(() => {
+ // 步骤 3: 计算内容区的目标高度。
+ // 这包括内容本身的 scrollHeight,以及任何可见的批量操作栏和分页控件的高度。
+ let targetHeight = content.scrollHeight;
+
+ if (batchActions && !batchActions.classList.contains('hidden')) {
+ targetHeight += batchActions.offsetHeight;
+ }
+ if (pagination && pagination.offsetHeight > 0) {
+ // 尝试获取分页控件的 margin-top,以获得更精确的高度
+ const paginationStyle = getComputedStyle(pagination);
+ const paginationMarginTop = parseFloat(paginationStyle.marginTop) || 0;
+ targetHeight += pagination.offsetHeight + paginationMarginTop;
+ }
- // 动画结束后移除 max-height 以允许内容动态变化
- content.addEventListener('transitionend', function handler() {
- content.removeEventListener('transitionend', handler);
- if (!content.classList.contains('collapsed')) { // 确保是在展开状态
- content.style.maxHeight = '';
- content.style.overflow = 'visible';
- }
- }, { once: true });
- });
- } else {
- // 收起内容
- // 先计算当前总高度
- let currentHeight = content.scrollHeight;
- if (batchActions && !batchActions.classList.contains('hidden')) {
- 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.classList.add('collapsed');
- });
- }
+ // 步骤 4: 设置 maxHeight 和 opacity 以触发CSS过渡到展开状态。
+ content.style.maxHeight = targetHeight + 'px';
+ content.style.opacity = '1';
+ // 假设展开后的 padding 为 1rem (p-4 in Tailwind). 根据实际情况调整。
+ content.style.paddingTop = '1rem';
+ content.style.paddingBottom = '1rem';
+
+ // 步骤 5: 监听 transitionend 事件。动画结束后,移除 maxHeight 以允许内容动态调整,
+ // 并将 overflow 设置为 visible,以防内容变化后被裁剪。
+ content.addEventListener('transitionend', function onExpansionEnd() {
+ content.removeEventListener('transitionend', onExpansionEnd); // 清理监听器
+ // 再次检查确保是在展开状态 (避免在快速连续点击时出错)
+ if (!content.classList.contains('collapsed')) {
+ content.style.maxHeight = ''; // 允许内容自适应高度
+ content.style.overflow = 'visible'; // 允许内容溢出(如果需要)
+ }
+ }, { once: true }); // 确保监听器只执行一次
+ });
} else {
- console.error("Toggle section failed: Icon or content not found.", header, sectionId);
+ // --- 准备收起动画 ---
+ // 步骤 1: 获取当前内容区的可见高度。
+ // 这对于从当前渲染高度平滑过渡到0是必要的。
+ let currentVisibleHeight = content.scrollHeight; // scrollHeight 应该已经是包括padding的内部高度
+ if (batchActions && !batchActions.classList.contains('hidden')) {
+ currentVisibleHeight += batchActions.offsetHeight;
+ }
+ if (pagination && pagination.offsetHeight > 0) {
+ const paginationStyle = getComputedStyle(pagination);
+ const paginationMarginTop = parseFloat(paginationStyle.marginTop) || 0;
+ currentVisibleHeight += pagination.offsetHeight + paginationMarginTop;
+ }
+
+ // 步骤 2: 将 maxHeight 设置为当前计算的可见高度,以确保过渡从当前高度开始。
+ // 同时,确保 overflow 在动画开始前是 hidden。
+ content.style.maxHeight = currentVisibleHeight + 'px';
+ content.style.overflow = 'hidden';
+
+ // 步骤 3: 使用 requestAnimationFrame (rAF) 确保浏览器应用了上述 maxHeight。
+ requestAnimationFrame(() => {
+ // 步骤 4: 过渡到目标状态 (收起): maxHeight 和 padding 设为0,opacity 设为0。
+ content.style.maxHeight = '0px';
+ content.style.opacity = '0';
+ content.style.paddingTop = '0';
+ content.style.paddingBottom = '0';
+ // 在动画开始(或即将开始)后添加 collapsed 类,以便CSS可以应用最终的折叠样式。
+ content.classList.add('collapsed');
+ });
}
}
-
// 筛选有效密钥(根据失败次数阈值)并更新批量操作状态
function filterValidKeys() {
const thresholdInput = document.getElementById('failCountThreshold');
@@ -720,140 +761,109 @@ function filterValidKeys() {
}
-// 初始化
-document.addEventListener('DOMContentLoaded', () => {
- // 初始化统计区块动画
- initStatItemAnimations();
+// --- Initialization Helper Functions ---
+function initializePageAnimationsAndEffects() {
+ initStatItemAnimations(); // Already an external function
- // 添加数字滚动动画效果
const animateCounters = () => {
const statValues = document.querySelectorAll('.stat-value');
statValues.forEach(valueElement => {
const finalValue = parseInt(valueElement.textContent, 10);
if (!isNaN(finalValue)) {
- // 保存原始值以便稍后恢复
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) {
const progress = elapsedTime / duration;
- // 使用缓动函数使动画更自然
const easeOutValue = 1 - Math.pow(1 - progress, 3);
const currentValue = Math.floor(easeOutValue * finalValue);
valueElement.textContent = currentValue;
requestAnimationFrame(updateCounter);
} else {
- // 恢复为原始值,以确保准确性
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 = '';
});
});
+}
- // 监听展开/折叠事件 (确保使用正确的选择器和函数)
+function initializeSectionToggleListeners() {
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, select')) {
- return;
- }
- // 从 header 中提取 sectionId (例如从关联的 content div 的 id)
- const card = header.closest('.stats-card');
- const content = card ? card.querySelector('.key-content') : null;
- const sectionId = content ? content.id : null;
- if (sectionId) {
+ if (header.querySelector('.toggle-icon')) {
+ header.addEventListener('click', (event) => {
+ if (event.target.closest('input, label, button, select')) {
+ return;
+ }
+ const card = header.closest('.stats-card');
+ const content = card ? card.querySelector('.key-content') : null;
+ const sectionId = content ? content.id : null;
+ if (sectionId) {
toggleSection(header, sectionId);
- } else {
- console.warn("Could not determine sectionId for toggle.");
- }
- });
- }
+ } else {
+ console.warn("Could not determine sectionId for toggle.");
+ }
+ });
+ }
});
+}
- // 添加筛选输入框事件监听
+function initializeKeyFilterControls() {
const thresholdInput = document.getElementById('failCountThreshold');
if (thresholdInput) {
- // 使用 'input' 事件实时响应输入变化
thresholdInput.addEventListener('input', filterValidKeys);
- // 初始加载时应用一次筛选 (现在由 pagination/search 初始化处理)
- // filterValidKeys();
}
+}
- // --- 批量验证相关函数 (明确挂载到 window) ---
-
- // 显示验证确认模态框 (基于选中的密钥)
+function initializeGlobalBatchVerificationHandlers() {
window.showVerifyModal = function(type, event) {
- // 阻止事件冒泡(如果从按钮点击触发)
if (event) {
event.stopPropagation();
}
-
const modalElement = document.getElementById('verifyModal');
const titleElement = document.getElementById('verifyModalTitle');
const messageElement = document.getElementById('verifyModalMessage');
const confirmButton = document.getElementById('confirmVerifyBtn');
-
const selectedKeys = getSelectedKeys(type);
const count = selectedKeys.length;
-
- // 设置标题和消息
titleElement.textContent = '批量验证密钥';
if (count > 0) {
messageElement.textContent = `确定要批量验证选中的 ${count} 个${type === 'valid' ? '有效' : '无效'}密钥吗?此操作可能需要一些时间。`;
- confirmButton.disabled = false; // 确保按钮可用
+ confirmButton.disabled = false;
} else {
- // 这个情况理论上不会发生,因为按钮在未选中时是禁用的
messageElement.textContent = `请先选择要验证的${type === 'valid' ? '有效' : '无效'}密钥。`;
confirmButton.disabled = true;
}
-
- // 设置确认按钮事件
confirmButton.onclick = () => executeVerifyAll(type);
-
- // 显示模态框
modalElement.classList.remove('hidden');
- }
+ };
window.closeVerifyModal = function() {
document.getElementById('verifyModal').classList.add('hidden');
- }
+ };
- window.executeVerifyAll = async function(type) {
+ // executeVerifyAll 变为 initializeGlobalBatchVerificationHandlers 的局部函数
+ async function executeVerifyAll(type) { // Removed window.
try {
- // 关闭确认模态框
- closeVerifyModal();
-
- // 找到对应的验证按钮以显示加载状态
+ 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) {
@@ -861,85 +871,64 @@ document.addEventListener('DOMContentLoaded', () => {
verifyButton.disabled = true;
verifyButton.innerHTML = ' 验证中';
}
-
-
- // 获取选中的密钥
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
- }
+ if (verifyButton) { // Restore button if no keys selected
+ verifyButton.innerHTML = originalVerifyHtml;
+ }
return;
}
-
- // 显示一个通用的加载提示
showNotification('开始批量验证,请稍候...', 'info');
-
- // 调用新的后端 API 来验证选定的密钥
- const response = await fetch(`/gemini/v1beta/verify-selected-keys`, { // 假设的新 API 端点
+ const options = {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ keys: keysToVerify }) // 只发送密钥列表
- });
-
- if (!response.ok) {
- let errorMsg = `服务器返回错误: ${response.status}`;
- try {
- const errorData = await response.json();
- errorMsg = errorData.message || errorMsg;
- } catch (e) { /*忽略解析错误*/ }
- throw new Error(errorMsg);
+ 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.");
}
-
- const data = await response.json();
-
- // 使用新的专用模态框显示结果
- showVerificationResultModal(data);
- // 注意:autoReload 逻辑已移至 showVerificationResultModal 内部 (现在总是刷新)
-
- } catch (error) {
- console.error('批量验证处理失败:', error);
- // 失败后也刷新页面,让用户看到可能更新的状态
- showResultModal(false, '批量验证处理失败: ' + error.message, true);
+ } 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
+ console.log("Bulk verification process finished.");
+ // Button state will be reset on page reload or by updateBatchActions
}
}
+ // The confirmButton.onclick in showVerifyModal (defined earlier in initializeGlobalBatchVerificationHandlers)
+ // will correctly reference this local executeVerifyAll due to closure.
+}
- // --- 复选框事件监听 ---
- // 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');
- }
- });
+function initializeKeySelectionListeners() {
+ const validKeysList = document.getElementById('validKeys');
+ if (validKeysList) {
+ validKeysList.addEventListener('change', (event) => {
+ if (event.target.classList.contains('key-checkbox')) {
+ updateBatchActions('valid');
+ }
+ });
+ }
+ const invalidKeysList = document.getElementById('invalidKeys');
+ if (invalidKeysList) {
+ invalidKeysList.addEventListener('change', (event) => {
+ if (event.target.classList.contains('key-checkbox')) {
+ updateBatchActions('invalid');
+ }
+ });
+ }
+}
-
- // 初始化批量操作区域状态 (在 pagination 初始化后进行)
- // updateBatchActions('valid'); // Called by displayPage
- // updateBatchActions('invalid'); // Called by displayPage
-
-
- // --- 滚动和页面控制 --- (Scroll buttons handled by base.html)
- // --- 自动刷新控制 ---
+function initializeAutoRefreshControls() {
const autoRefreshToggle = document.getElementById('autoRefreshToggle');
const autoRefreshIntervalTime = 60000; // 60秒
let autoRefreshTimer = null;
function startAutoRefresh() {
- if (autoRefreshTimer) return; // 防止重复启动
+ if (autoRefreshTimer) return;
console.log('启动自动刷新...');
showNotification('自动刷新已启动', 'info', 2000);
autoRefreshTimer = setInterval(() => {
@@ -958,14 +947,11 @@ document.addEventListener('DOMContentLoaded', () => {
}
if (autoRefreshToggle) {
- // 从 localStorage 读取状态并初始化
const isAutoRefreshEnabled = localStorage.getItem('autoRefreshEnabled') === 'true';
autoRefreshToggle.checked = isAutoRefreshEnabled;
if (isAutoRefreshEnabled) {
startAutoRefresh();
}
-
- // 添加事件监听器
autoRefreshToggle.addEventListener('change', () => {
if (autoRefreshToggle.checked) {
localStorage.setItem('autoRefreshEnabled', 'true');
@@ -976,32 +962,37 @@ 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
+// These variables are used by pagination and search, define them in a scope accessible by initializeKeyPaginationAndSearch
+let allValidKeys = [];
+let allInvalidKeys = [];
+let filteredValidKeys = [];
+let itemsPerPage = 10; // Default
+let validCurrentPage = 1; // Also used by displayPage
+let invalidCurrentPage = 1; // Also used by displayPage
- // --- Get DOM Elements for Pagination/Search ---
+
+function initializeKeyPaginationAndSearch() {
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');
+ const thresholdInput = document.getElementById('failCountThreshold'); // Already used by initializeKeyFilterControls
- // --- 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
+ li.dataset.key = keyTextSpan.dataset.fullKey;
}
});
- filteredValidKeys = [...allValidKeys]; // Start with all keys
+ filteredValidKeys = [...allValidKeys];
}
if (invalidKeysListElement) {
allInvalidKeys = Array.from(invalidKeysListElement.querySelectorAll('li[data-key]'));
- allInvalidKeys.forEach(li => {
+ allInvalidKeys.forEach(li => {
const keyTextSpan = li.querySelector('.key-text');
if (keyTextSpan && keyTextSpan.dataset.fullKey) {
li.dataset.key = keyTextSpan.dataset.fullKey;
@@ -1009,41 +1000,54 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
- // --- Initial Display ---
if (itemsPerPageSelect) {
- itemsPerPage = parseInt(itemsPerPageSelect.value, 10);
+ itemsPerPage = parseInt(itemsPerPageSelect.value, 10); // Initialize itemsPerPage
+ itemsPerPageSelect.addEventListener('change', () => {
+ itemsPerPage = parseInt(itemsPerPageSelect.value, 10);
+ filterAndSearchValidKeys(); // Re-filter and display page 1 for valid keys
+ displayPage('invalid', 1, allInvalidKeys); // Reset invalid keys to page 1
+ });
}
- filterAndSearchValidKeys(); // This applies initial filter/search and calls displayPage('valid', 1, ...)
- displayPage('invalid', 1, allInvalidKeys); // Display first page of invalid keys
+
+ // Initial display calls
+ filterAndSearchValidKeys();
+ displayPage('invalid', 1, allInvalidKeys);
- // --- Event Listeners for Pagination/Search ---
- if (thresholdInput) {
- thresholdInput.addEventListener('input', filterAndSearchValidKeys);
- }
+ // Event listeners for search and filter (thresholdInput listener is in initializeKeyFilterControls)
if (searchInput) {
searchInput.addEventListener('input', filterAndSearchValidKeys);
}
- if (itemsPerPageSelect) {
- itemsPerPageSelect.addEventListener('change', () => {
- itemsPerPage = parseInt(itemsPerPageSelect.value, 10);
- filterAndSearchValidKeys(); // Re-filter and display page 1
+}
+
+function registerServiceWorker() {
+ if ('serviceWorker' in navigator) {
+ window.addEventListener('load', () => {
+ navigator.serviceWorker.register('/static/service-worker.js')
+ .then(registration => {
+ console.log('ServiceWorker注册成功:', registration.scope);
+ })
+ .catch(error => {
+ console.log('ServiceWorker注册失败:', error);
+ });
});
}
-
-});
-
-// Service Worker registration
-if ('serviceWorker' in navigator) {
- window.addEventListener('load', () => {
- navigator.serviceWorker.register('/static/service-worker.js')
- .then(registration => {
- console.log('ServiceWorker注册成功:', registration.scope);
- })
- .catch(error => {
- console.log('ServiceWorker注册失败:', error);
- });
- });
}
+
+// 初始化
+document.addEventListener('DOMContentLoaded', () => {
+ initializePageAnimationsAndEffects();
+ initializeSectionToggleListeners();
+ initializeKeyFilterControls();
+ initializeGlobalBatchVerificationHandlers();
+ initializeKeySelectionListeners();
+ initializeAutoRefreshControls();
+ initializeKeyPaginationAndSearch(); // This will also handle initial display
+ registerServiceWorker();
+
+ // Initial batch actions update might be needed if not covered by displayPage
+ // updateBatchActions('valid');
+ // updateBatchActions('invalid');
+});
function toggleKeyVisibility(button) {
const keyContainer = button.closest('.flex.items-center.gap-1');
const keyTextSpan = keyContainer.querySelector('.key-text');
@@ -1097,27 +1101,18 @@ async function showApiCallDetails(period) {
`;
try {
- // 调用后端 API 获取数据
- const response = await fetch(`/api/stats/details?period=${period}`);
- if (!response.ok) {
- let errorMsg = `服务器错误: ${response.status}`;
- try {
- const errorData = await response.json();
- errorMsg = errorData.detail || errorMsg;
- } catch (e) { /* 忽略解析错误 */ }
- throw new Error(errorMsg);
+ const data = await fetchAPI(`/api/stats/details?period=${period}`);
+ if (data) {
+ renderApiCallDetails(data, contentArea);
+ } else {
+ renderApiCallDetails([], contentArea); // Show empty state if no data
}
- const data = await response.json();
-
- // 渲染数据
- renderApiCallDetails(data, contentArea);
-
- } catch (error) {
- console.error('获取 API 调用详情失败:', error);
+ } catch (apiError) {
+ console.error('获取 API 调用详情失败:', apiError);
contentArea.innerHTML = `