// 错误日志页面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" }); } // 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 errorLogState = { currentPage: 1, pageSize: 10, logs: [], // 存储获取的日志 sort: { field: "id", // 默认按 ID 排序 order: "desc", // 默认降序 }, search: { key: "", error: "", errorCode: "", 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 deleteSelectedBtn; // 新增:批量删除按钮 let sortByIdHeader; // 新增:ID 排序表头 let sortIcon; // 新增:排序图标 let selectedCountSpan; // 新增:选中计数显示 let deleteConfirmModal; // 新增:删除确认模态框 let closeDeleteConfirmModalBtn; // 新增:关闭删除模态框按钮 let cancelDeleteBtn; // 新增:取消删除按钮 let confirmDeleteBtn; // 新增:确认删除按钮 let deleteConfirmMessage; // 新增:删除确认消息元素 let idsToDeleteGlobally = []; // 新增:存储待删除的ID let currentConfirmCallback = null; // 新增:存储当前的确认回调 let deleteAllLogsBtn; // 新增:清空全部按钮 // Helper functions for initialization function cacheDOMElements() { pageSizeSelector = document.getElementById("pageSize"); tableBody = document.getElementById("errorLogsTable"); paginationElement = document.getElementById("pagination"); loadingIndicator = document.getElementById("loadingIndicator"); noDataMessage = document.getElementById("noDataMessage"); errorMessage = document.getElementById("errorMessage"); logDetailModal = document.getElementById("logDetailModal"); modalCloseBtns = document.querySelectorAll( "#closeLogDetailModalBtn, #closeModalFooterBtn" ); keySearchInput = document.getElementById("keySearch"); errorSearchInput = document.getElementById("errorSearch"); 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"); if (sortByIdHeader) { 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"); deleteAllLogsBtn = document.getElementById("deleteAllLogsBtn"); // 缓存清空全部按钮 } function initializePageSizeControls() { if (pageSizeSelector) { pageSizeSelector.value = errorLogState.pageSize; pageSizeSelector.addEventListener("change", function () { errorLogState.pageSize = parseInt(this.value); errorLogState.currentPage = 1; // Reset to first page loadErrorLogs(); }); } } function initializeSearchControls() { if (searchBtn) { searchBtn.addEventListener("click", function () { 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(); }); } } function initializeModalControls() { // Log Detail Modal if (logDetailModal && modalCloseBtns) { modalCloseBtns.forEach((btn) => { btn.addEventListener("click", closeLogDetailModal); }); logDetailModal.addEventListener("click", function (event) { if (event.target === logDetailModal) { closeLogDetailModal(); } }); } // Delete Confirm Modal if (closeDeleteConfirmModalBtn) { closeDeleteConfirmModalBtn.addEventListener( "click", hideDeleteConfirmModal ); } if (cancelDeleteBtn) { cancelDeleteBtn.addEventListener("click", hideDeleteConfirmModal); } if (confirmDeleteBtn) { confirmDeleteBtn.addEventListener("click", handleConfirmDelete); } if (deleteConfirmModal) { deleteConfirmModal.addEventListener("click", function (event) { if (event.target === deleteConfirmModal) { hideDeleteConfirmModal(); } }); } } 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(); // 为 "清空全部" 按钮添加事件监听器 if (deleteAllLogsBtn) { deleteAllLogsBtn.addEventListener("click", function() { const message = "您确定要清空所有错误日志吗?此操作不可恢复!"; showDeleteConfirmModal(message, handleDeleteAllLogs); // 传入回调 }); } } // 新增:处理 "清空全部" 逻辑的函数 async function handleDeleteAllLogs() { const url = "/api/logs/errors/all"; const options = { method: "DELETE", }; try { await fetchAPI(url, options); showNotification("所有错误日志已成功清空", "success"); if (selectAllCheckbox) selectAllCheckbox.checked = false; // 取消全选 loadErrorLogs(); // 重新加载日志 } catch (error) { console.error("清空所有错误日志失败:", error); showNotification(`清空失败: ${error.message}`, "error", 5000); } } // 页面加载完成后执行 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(); }); // 新增:显示删除确认模态框 function showDeleteConfirmModal(message, confirmCallback) { if (deleteConfirmModal && deleteConfirmMessage) { deleteConfirmMessage.textContent = message; currentConfirmCallback = confirmCallback; // 存储回调 deleteConfirmModal.classList.add("show"); document.body.style.overflow = "hidden"; // Prevent body scrolling } } // 新增:隐藏删除确认模态框 function hideDeleteConfirmModal() { if (deleteConfirmModal) { deleteConfirmModal.classList.remove("show"); document.body.style.overflow = ""; // Restore body scrolling idsToDeleteGlobally = []; // 清空待删除ID currentConfirmCallback = null; // 清除回调 } } // 新增:处理确认删除按钮点击 function handleConfirmDelete() { if (typeof currentConfirmCallback === 'function') { currentConfirmCallback(); // 调用存储的回调 } hideDeleteConfirmModal(); // 关闭模态框 } // 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 _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) 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) { _performCopy(textToCopy, button); // 使用新的辅助函数 } else { console.warn( "No text found to copy for target:", targetId || "direct text" ); showNotification("没有内容可复制", "warning"); } } // End of handleCopyButtonClick function // 新增:设置批量选择相关的事件监听器 function setupBulkSelectionListeners() { if (selectAllCheckbox) { selectAllCheckbox.addEventListener("change", handleSelectAllChange); } if (tableBody) { // 使用事件委托处理行复选框的点击 tableBody.addEventListener("change", handleRowCheckboxChange); } if (copySelectedKeysBtn) { copySelectedKeysBtn.addEventListener("click", handleCopySelectedKeys); } // 新增:为批量删除按钮添加事件监听器 (如果尚未添加) // 通常在 DOMContentLoaded 中添加一次即可 // if (deleteSelectedBtn && !deleteSelectedBtn.hasListener) { // deleteSelectedBtn.addEventListener('click', handleDeleteSelected); // deleteSelectedBtn.hasListener = true; // 标记已添加 // } } // 新增:处理"全选"复选框变化的函数 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 (deleteSelectedBtn) { deleteSelectedBtn.disabled = selectedCount === 0; deleteSelectedBtn.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"); // 每行一个密钥 _performCopy(textToCopy, copySelectedKeysBtn); // 使用新的辅助函数 } else { showNotification("没有选中的密钥可复制", "warning"); } } // 修改:处理批量删除按钮点击的函数 - 改为显示模态框 function handleDeleteSelected() { const selectedCheckboxes = tableBody.querySelectorAll( ".row-checkbox:checked" ); const logIdsToDelete = []; selectedCheckboxes.forEach((checkbox) => { const logId = checkbox.getAttribute("data-log-id"); // 需要在渲染时添加 data-log-id if (logId) { logIdsToDelete.push(parseInt(logId)); } }); if (logIdsToDelete.length === 0) { showNotification("没有选中的日志可删除", "warning"); return; } if (logIdsToDelete.length === 0) { showNotification("没有选中的日志可删除", "warning"); return; } // 存储待删除ID并显示模态框 idsToDeleteGlobally = logIdsToDelete; // 仍然需要设置,因为 performActualDelete 会用到 const message = `确定要删除选中的 ${logIdsToDelete.length} 条日志吗?此操作不可恢复!`; showDeleteConfirmModal(message, function() { // 传入匿名回调 performActualDelete(idsToDeleteGlobally); }); } // 新增:执行实际的删除操作(提取自原 handleDeleteSelected 和 handleDeleteLogRow) async function performActualDelete(logIds) { if (!logIds || logIds.length === 0) return; const isSingleDelete = logIds.length === 1; const url = isSingleDelete ? `/api/logs/errors/${logIds[0]}` : "/api/logs/errors"; 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 { // 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"); // 取消全选 if (selectAllCheckbox) selectAllCheckbox.checked = false; // 重新加载当前页数据 loadErrorLogs(); } catch (error) { console.error("批量删除错误日志失败:", error); showNotification(`批量删除失败: ${error.message}`, "error", 5000); } } // 修改:处理单行删除按钮点击的函数 - 改为显示模态框 function handleDeleteLogRow(logId) { if (!logId) return; // 存储待删除ID并显示模态框 idsToDeleteGlobally = [parseInt(logId)]; // 存储为数组 // 仍然需要设置,因为 performActualDelete 会用到 // 使用通用确认消息,不显示具体ID const message = `确定要删除这条日志吗?此操作不可恢复!`; showDeleteConfirmModal(message, function() { // 传入匿名回调 performActualDelete([parseInt(logId)]); // 确保传递的是数组 }); } // 新增:处理 ID 排序点击的函数 function handleSortById() { if (errorLogState.sort.field === "id") { // 如果当前是按 ID 排序,切换顺序 errorLogState.sort.order = errorLogState.sort.order === "asc" ? "desc" : "asc"; } else { // 如果当前不是按 ID 排序,切换到按 ID 排序,默认为降序 errorLogState.sort.field = "id"; errorLogState.sort.order = "desc"; } // 更新图标 updateSortIcon(); // 重新加载第一页数据 errorLogState.currentPage = 1; loadErrorLogs(); } // 新增:更新排序图标的函数 function updateSortIcon() { if (!sortIcon) return; // 移除所有可能的排序类 sortIcon.classList.remove( "fa-sort", "fa-sort-up", "fa-sort-down", "text-gray-400", "text-primary-600" ); 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 排序,显示默认图标 sortIcon.classList.add("fa-sort", "text-gray-400"); } } // 加载错误日志数据 async function loadErrorLogs() { // 重置选择状态 if (selectAllCheckbox) selectAllCheckbox.checked = false; if (selectAllCheckbox) selectAllCheckbox.indeterminate = false; updateSelectedState(); // 更新按钮状态和计数 showLoading(true); showError(false); showNoData(false); const offset = (errorLogState.currentPage - 1) * errorLogState.pageSize; try { // Construct the API URL with search and sort parameters let apiUrl = `/api/logs/errors?limit=${errorLogState.pageSize}&offset=${offset}`; // 添加排序参数 apiUrl += `&sort_by=${errorLogState.sort.field}&sort_order=${errorLogState.sort.order}`; // 添加搜索参数 if (errorLogState.search.key) { apiUrl += `&key_search=${encodeURIComponent(errorLogState.search.key)}`; } if (errorLogState.search.error) { apiUrl += `&error_search=${encodeURIComponent( errorLogState.search.error )}`; } if (errorLogState.search.errorCode) { // Add error code to API request apiUrl += `&error_code_search=${encodeURIComponent( errorLogState.search.errorCode )}`; } if (errorLogState.search.startDate) { apiUrl += `&start_date=${encodeURIComponent( errorLogState.search.startDate )}`; } if (errorLogState.search.endDate) { apiUrl += `&end_date=${encodeURIComponent(errorLogState.search.endDate)}`; } // Use fetchAPI to get logs const data = await fetchAPI(apiUrl); // API 现在返回 { logs: [], total: count } // fetchAPI already parsed JSON if (data && Array.isArray(data.logs)) { 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 { // Handle unexpected data format even after successful fetch console.error("Unexpected API response format:", data); throw new Error("无法识别的API响应格式"); } showLoading(false); if (errorLogState.logs.length === 0) { showNoData(true); } } catch (error) { console.error("获取错误日志失败:", error); showLoading(false); showError(true, error.message); // Show specific error message } } // 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) { 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 = (errorLogState.currentPage - 1) * errorLogState.pageSize; logs.forEach((log, index) => { const sequentialId = startIndex + index + 1; const row = document.createElement("tr"); row.innerHTML = _createLogRowHtml(log, sequentialId); tableBody.appendChild(row); }); // Add event listeners to new 'View Details' buttons document.querySelectorAll(".btn-view-details").forEach((button) => { button.addEventListener("click", function () { const logId = parseInt(this.getAttribute("data-log-id")); showLogDetails(logId); }); }); // 新增:为新渲染的删除按钮添加事件监听器 document.querySelectorAll(".btn-delete-row").forEach((button) => { button.addEventListener("click", function () { const logId = this.getAttribute("data-log-id"); handleDeleteLogRow(logId); }); }); // Re-initialize copy buttons specifically for the newly rendered table rows setupCopyButtons("#errorLogsTable"); // Update selected state after rendering updateSelectedState(); } // 显示错误日志详情 (从 API 获取) async function showLogDetails(logId) { if (!logDetailModal) return; // Show loading state in modal (optional) // Clear previous content and show a spinner or message document.getElementById("modalGeminiKey").textContent = "加载中..."; document.getElementById("modalErrorType").textContent = "加载中..."; document.getElementById("modalErrorLog").textContent = "加载中..."; document.getElementById("modalRequestMsg").textContent = "加载中..."; document.getElementById("modalModelName").textContent = "加载中..."; document.getElementById("modalRequestTime").textContent = "加载中..."; logDetailModal.classList.add("show"); document.body.style.overflow = "hidden"; // Prevent body scrolling try { // 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("未找到日志详情"); } // Format date let formattedTime = "N/A"; try { const requestTime = new Date(logDetails.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); } // Format request message (handle potential JSON) let formattedRequestMsg = "无"; if (logDetails.request_msg) { try { if ( typeof logDetails.request_msg === "object" && logDetails.request_msg !== null ) { formattedRequestMsg = JSON.stringify(logDetails.request_msg, null, 2); } else if (typeof logDetails.request_msg === "string") { // Try parsing if it looks like JSON, otherwise display as string const trimmedMsg = logDetails.request_msg.trim(); if (trimmedMsg.startsWith("{") || trimmedMsg.startsWith("[")) { formattedRequestMsg = JSON.stringify( JSON.parse(logDetails.request_msg), null, 2 ); } else { formattedRequestMsg = logDetails.request_msg; } } else { formattedRequestMsg = String(logDetails.request_msg); } } catch (e) { formattedRequestMsg = String(logDetails.request_msg); // Fallback console.warn("Could not parse request_msg as JSON:", e); } } // Populate modal content with fetched details document.getElementById("modalGeminiKey").textContent = logDetails.gemini_key || "无"; document.getElementById("modalErrorType").textContent = logDetails.error_type || "未知"; document.getElementById("modalErrorLog").textContent = logDetails.error_log || "无"; // Full error log document.getElementById("modalRequestMsg").textContent = formattedRequestMsg; // Full request message document.getElementById("modalModelName").textContent = logDetails.model_name || "未知"; document.getElementById("modalRequestTime").textContent = formattedTime; // Re-initialize copy buttons specifically for the modal after content is loaded setupCopyButtons("#logDetailModal"); } catch (error) { console.error("获取日志详情失败:", error); // Show error in modal document.getElementById("modalGeminiKey").textContent = "错误"; document.getElementById("modalErrorType").textContent = "错误"; document.getElementById( "modalErrorLog" ).textContent = `加载失败: ${error.message}`; document.getElementById("modalRequestMsg").textContent = "错误"; document.getElementById("modalModelName").textContent = "错误"; document.getElementById("modalRequestTime").textContent = "错误"; // Optionally show a notification showNotification(`加载日志详情失败: ${error.message}`, "error", 5000); } } // Close Log Detail Modal function closeLogDetailModal() { if (logDetailModal) { logDetailModal.classList.remove("show"); // Optional: Restore body scrolling document.body.style.overflow = ""; } } // 更新分页控件 function updatePagination(currentItemCount, totalItems) { if (!paginationElement) return; paginationElement.innerHTML = ""; // Clear existing pagination // Calculate total pages only if totalItems is known and valid let totalPages = 1; if (totalItems >= 0) { 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 { // If total is unknown and more items might exist, we can't build full pagination // 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, "«", 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, errorLogState.currentPage - Math.floor(maxPagesToShow / 2) ); let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1); // Adjust startPage if endPage reaches the limit first if (endPage === totalPages) { startPage = Math.max(1, endPage - maxPagesToShow + 1); } // Previous Button addPaginationLink( paginationElement, "«", errorLogState.currentPage > 1, () => { errorLogState.currentPage--; loadErrorLogs(); } ); // First Page Button if (startPage > 1) { addPaginationLink(paginationElement, "1", true, () => { errorLogState.currentPage = 1; loadErrorLogs(); }); if (startPage > 2) { addPaginationLink(paginationElement, "...", false); // Ellipsis } } // Page Number Buttons for (let i = startPage; i <= endPage; i++) { addPaginationLink( paginationElement, i.toString(), true, () => { errorLogState.currentPage = i; loadErrorLogs(); }, i === errorLogState.currentPage ); } // Last Page Button if (endPage < totalPages) { if (endPage < totalPages - 1) { addPaginationLink(paginationElement, "...", false); // Ellipsis } addPaginationLink(paginationElement, totalPages.toString(), true, () => { errorLogState.currentPage = totalPages; loadErrorLogs(); }); } // Next Button addPaginationLink( paginationElement, "»", errorLogState.currentPage < totalPages, () => { errorLogState.currentPage++; loadErrorLogs(); } ); } // Helper function to add pagination links function addPaginationLink( parentElement, text, enabled, clickHandler, isActive = false ) { // const pageItem = document.createElement('li'); // We are not using
  • anymore const pageLink = document.createElement("a"); // Base Tailwind classes for layout, size, and transition. Colors/borders will come from CSS. let baseClasses = "px-3 py-1 rounded-md text-sm transition duration-150 ease-in-out"; // Common classes if (isActive) { pageLink.className = `${baseClasses} active`; // Add 'active' class for CSS } else if (enabled) { pageLink.className = baseClasses; // Just base classes, CSS handles the rest } else { // Disabled link (e.g., '...' or unavailable prev/next) pageLink.className = `${baseClasses} disabled`; // Add 'disabled' class for CSS } pageLink.href = "#"; // Prevent page jump pageLink.innerHTML = text; if (enabled && clickHandler) { pageLink.addEventListener("click", function (e) { e.preventDefault(); clickHandler(); }); } else { // Handles !enabled (includes isActive as clickHandler is null for it, and '...' which has no clickHandler) pageLink.addEventListener("click", (e) => e.preventDefault()); } parentElement.appendChild(pageLink); // Directly append to the