// 错误日志页面JavaScript (Updated for new structure, no Bootstrap) // 页面滚动功能 function scrollToTop() { window.scrollTo({ top: 0, behavior: 'smooth' }); } function scrollToBottom() { window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); } // 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 currentSearch = { // Store current search parameters key: '', error: '', errorCode: '', // Added error code search startDate: '', endDate: '' }; // DOM Elements Cache let pageSizeSelector; // let refreshBtn; // Removed, as the button is deleted let tableBody; let paginationElement; let loadingIndicator; let noDataMessage; let errorMessage; let logDetailModal; let modalCloseBtns; // Collection of close buttons for the modal let keySearchInput; let errorSearchInput; let errorCodeSearchInput; // Added error code input let startDateInput; let endDateInput; let searchBtn; let pageInput; let goToPageBtn; let selectAllCheckbox; // 新增:全选复选框 let copySelectedKeysBtn; // 新增:复制选中按钮 let selectedCountSpan; // 新增:选中计数显示 // 页面加载完成后执行 document.addEventListener('DOMContentLoaded', function() { // Cache DOM elements 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 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'); // 新增 selectedCountSpan = document.getElementById('selectedCount'); // 新增 // Initialize page size selector if (pageSizeSelector) { pageSizeSelector.value = pageSize; pageSizeSelector.addEventListener('change', function() { pageSize = parseInt(this.value); currentPage = 1; // Reset to first page loadErrorLogs(); }); } // Refresh button event listener removed // Initialize search button 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 loadErrorLogs(); }); } // Initialize modal close buttons 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(); } }); } // 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(); // 触发按钮点击 } }); } }); // Fallback copy function using document.execCommand function fallbackCopyTextToClipboard(text) { const textArea = document.createElement("textarea"); textArea.value = text; // Avoid scrolling to bottom textArea.style.top = "0"; textArea.style.left = "0"; textArea.style.position = "fixed"; document.body.appendChild(textArea); textArea.focus(); textArea.select(); let successful = false; try { successful = document.execCommand('copy'); } catch (err) { console.error('Fallback copy failed:', err); successful = false; } document.body.removeChild(textArea); return successful; } // Helper function to handle feedback after copy attempt (both modern and fallback) function handleCopyResult(buttonElement, success) { const originalIcon = buttonElement.querySelector('i').className; // Store original icon class const iconElement = buttonElement.querySelector('i'); if (success) { iconElement.className = 'fas fa-check text-success-500'; // Use checkmark icon class showNotification('已复制到剪贴板', 'success', 2000); } else { iconElement.className = 'fas fa-times text-danger-500'; // Use error icon class showNotification('复制失败', 'error', 3000); } setTimeout(() => { iconElement.className = originalIcon; }, success ? 2000 : 3000); // Restore original icon class } // 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); }); } // Extracted click handler logic for reusability and removing listeners function handleCopyButtonClick() { const button = this; // 'this' refers to the button clicked const targetId = button.getAttribute('data-target'); const textToCopyDirect = button.getAttribute('data-copy-text'); // For direct text copy (e.g., table key) let textToCopy = ''; if (textToCopyDirect) { textToCopy = textToCopyDirect; } else if (targetId) { const targetElement = document.getElementById(targetId); if (targetElement) { textToCopy = targetElement.textContent; } else { console.error('Target element not found:', targetId); showNotification('复制出错:找不到目标元素', 'error'); return; // Exit if target element not found } } else { console.error('No data-target or data-copy-text attribute found on button:', button); showNotification('复制出错:未指定复制内容', 'error'); return; // Exit if no source specified } 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 } } 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) { selectAllCheckbox.addEventListener('change', handleSelectAllChange); } if (tableBody) { // 使用事件委托处理行复选框的点击 tableBody.addEventListener('change', handleRowCheckboxChange); } if (copySelectedKeysBtn) { copySelectedKeysBtn.addEventListener('click', handleCopySelectedKeys); } } // 新增:处理“全选”复选框变化的函数 function handleSelectAllChange() { const isChecked = selectAllCheckbox.checked; const rowCheckboxes = tableBody.querySelectorAll('.row-checkbox'); rowCheckboxes.forEach(checkbox => { checkbox.checked = isChecked; }); updateSelectedState(); } // 新增:处理行复选框变化的函数 (事件委托) function handleRowCheckboxChange(event) { if (event.target.classList.contains('row-checkbox')) { updateSelectedState(); } } // 新增:更新选中状态(计数、按钮状态、全选框状态) function updateSelectedState() { const rowCheckboxes = tableBody.querySelectorAll('.row-checkbox'); const selectedCheckboxes = tableBody.querySelectorAll('.row-checkbox:checked'); const selectedCount = selectedCheckboxes.length; // 移除了数字显示,不再更新selectedCountSpan // 仍然更新复制按钮的禁用状态 if (copySelectedKeysBtn) { copySelectedKeysBtn.disabled = selectedCount === 0; // 可选:根据选中项数量更新按钮标题属性 copySelectedKeysBtn.setAttribute('title', `复制${selectedCount}项选中密钥`); } // 更新“全选”复选框的状态 if (selectAllCheckbox) { if (rowCheckboxes.length > 0 && selectedCount === rowCheckboxes.length) { selectAllCheckbox.checked = true; selectAllCheckbox.indeterminate = false; } else if (selectedCount > 0) { selectAllCheckbox.checked = false; selectAllCheckbox.indeterminate = true; // 部分选中状态 } else { selectAllCheckbox.checked = false; selectAllCheckbox.indeterminate = false; } } } // 新增:处理“复制选中密钥”按钮点击的函数 function handleCopySelectedKeys() { const selectedCheckboxes = tableBody.querySelectorAll('.row-checkbox:checked'); const keysToCopy = []; selectedCheckboxes.forEach(checkbox => { const key = checkbox.getAttribute('data-key'); if (key) { keysToCopy.push(key); } }); if (keysToCopy.length > 0) { const textToCopy = keysToCopy.join('\n'); // 每行一个密钥 copyTextToClipboard(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'); } } // 加载错误日志数据 async function loadErrorLogs() { // 重置选择状态 if (selectAllCheckbox) selectAllCheckbox.checked = false; if (selectAllCheckbox) selectAllCheckbox.indeterminate = false; updateSelectedState(); // 更新按钮状态和计数 showLoading(true); showError(false); showNoData(false); const offset = (currentPage - 1) * pageSize; try { // Construct the API URL with search parameters let apiUrl = `/api/logs/errors?limit=${pageSize}&offset=${offset}`; if (currentSearch.key) { apiUrl += `&key_search=${encodeURIComponent(currentSearch.key)}`; } if (currentSearch.error) { apiUrl += `&error_search=${encodeURIComponent(currentSearch.error)}`; } if (currentSearch.errorCode) { // Add error code to API request apiUrl += `&error_code_search=${encodeURIComponent(currentSearch.errorCode)}`; } if (currentSearch.startDate) { apiUrl += `&start_date=${encodeURIComponent(currentSearch.startDate)}`; } if (currentSearch.endDate) { apiUrl += `&end_date=${encodeURIComponent(currentSearch.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(); // API 现在返回 { logs: [], total: count } if (data && Array.isArray(data.logs)) { errorLogs = data.logs; // Store the list data (contains error_code) renderErrorLogs(errorLogs); updatePagination(errorLogs.length, data.total || -1); } else { throw new Error('无法识别的API响应格式'); } showLoading(false); if (errorLogs.length === 0) { showNoData(true); } } catch (error) { console.error('获取错误日志失败:', error); showLoading(false); showError(true, error.message); // Show specific error message } } // 渲染错误日志表格 function renderErrorLogs(logs) { if (!tableBody) return; tableBody.innerHTML = ''; // Clear previous entries // 重置全选复选框状态(在清空表格后) if (selectAllCheckbox) { selectAllCheckbox.checked = false; selectAllCheckbox.indeterminate = false; } if (!logs || logs.length === 0) { // Handled by showNoData return; } const startIndex = (currentPage - 1) * pageSize; // Calculate starting index for the current page logs.forEach((log, index) => { // Add index parameter to forEach 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 = `