| | |
| |
|
| | |
| | function scrollToTop() { |
| | window.scrollTo({ top: 0, behavior: "smooth" }); |
| | } |
| |
|
| | function scrollToBottom() { |
| | window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }); |
| | } |
| |
|
| | |
| | async function fetchAPI(url, options = {}) { |
| | try { |
| | const response = await fetch(url, options); |
| |
|
| | |
| | if (response.status === 204) { |
| | return null; |
| | } |
| |
|
| | let responseData; |
| | try { |
| | responseData = await response.json(); |
| | } catch (e) { |
| | |
| | if (!response.ok) { |
| | |
| | throw new Error( |
| | `HTTP error! status: ${response.status} - ${response.statusText}` |
| | ); |
| | } |
| | |
| | |
| | console.warn("Response was not JSON for URL:", url); |
| | return await response.text(); |
| | } |
| |
|
| | if (!response.ok) { |
| | |
| | const message = |
| | responseData?.detail || |
| | `HTTP error! status: ${response.status} - ${response.statusText}`; |
| | throw new Error(message); |
| | } |
| |
|
| | return responseData; |
| | } catch (error) { |
| | |
| | console.error( |
| | "API Call Failed:", |
| | error.message, |
| | "URL:", |
| | url, |
| | "Options:", |
| | options |
| | ); |
| | |
| | throw error; |
| | } |
| | } |
| |
|
| | |
| | |
| |
|
| | |
| | let errorLogState = { |
| | currentPage: 1, |
| | pageSize: 10, |
| | logs: [], |
| | sort: { |
| | field: "id", |
| | order: "desc", |
| | }, |
| | search: { |
| | key: "", |
| | error: "", |
| | errorCode: "", |
| | startDate: "", |
| | endDate: "", |
| | }, |
| | }; |
| |
|
| | |
| | let pageSizeSelector; |
| | |
| | let tableBody; |
| | let paginationElement; |
| | let loadingIndicator; |
| | let noDataMessage; |
| | let errorMessage; |
| | let logDetailModal; |
| | let modalCloseBtns; |
| | let keySearchInput; |
| | let errorSearchInput; |
| | let errorCodeSearchInput; |
| | let startDateInput; |
| | let endDateInput; |
| | let searchBtn; |
| | let pageInput; |
| | let goToPageBtn; |
| | let selectAllCheckbox; |
| | let copySelectedKeysBtn; |
| | let deleteSelectedBtn; |
| | let sortByIdHeader; |
| | let sortIcon; |
| | let selectedCountSpan; |
| | let deleteConfirmModal; |
| | let closeDeleteConfirmModalBtn; |
| | let cancelDeleteBtn; |
| | let confirmDeleteBtn; |
| | let deleteConfirmMessage; |
| | let idsToDeleteGlobally = []; |
| | let currentConfirmCallback = null; |
| | let deleteAllLogsBtn; |
| |
|
| | |
| | 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; |
| | 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; |
| | loadErrorLogs(); |
| | }); |
| | } |
| | } |
| |
|
| | function initializeModalControls() { |
| | |
| | if (logDetailModal && modalCloseBtns) { |
| | modalCloseBtns.forEach((btn) => { |
| | btn.addEventListener("click", closeLogDetailModal); |
| | }); |
| | logDetailModal.addEventListener("click", function (event) { |
| | if (event.target === logDetailModal) { |
| | closeLogDetailModal(); |
| | } |
| | }); |
| | } |
| |
|
| | |
| | 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); |
| | } |
| | |
| | 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(); |
| |
|
| | |
| | loadErrorLogs(); |
| |
|
| | |
| | |
| | setupCopyButtons(); |
| | }); |
| |
|
| | |
| | function showDeleteConfirmModal(message, confirmCallback) { |
| | if (deleteConfirmModal && deleteConfirmMessage) { |
| | deleteConfirmMessage.textContent = message; |
| | currentConfirmCallback = confirmCallback; |
| | deleteConfirmModal.classList.add("show"); |
| | document.body.style.overflow = "hidden"; |
| | } |
| | } |
| | |
| | |
| | function hideDeleteConfirmModal() { |
| | if (deleteConfirmModal) { |
| | deleteConfirmModal.classList.remove("show"); |
| | document.body.style.overflow = ""; |
| | idsToDeleteGlobally = []; |
| | currentConfirmCallback = null; |
| | } |
| | } |
| | |
| | |
| | function handleConfirmDelete() { |
| | if (typeof currentConfirmCallback === 'function') { |
| | currentConfirmCallback(); |
| | } |
| | hideDeleteConfirmModal(); |
| | } |
| | |
| | |
| | function fallbackCopyTextToClipboard(text) { |
| | const textArea = document.createElement("textarea"); |
| | textArea.value = text; |
| |
|
| | |
| | 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; |
| | } |
| |
|
| | |
| | function handleCopyResult(buttonElement, success) { |
| | const originalIcon = buttonElement.querySelector("i").className; |
| | const iconElement = buttonElement.querySelector("i"); |
| | if (success) { |
| | iconElement.className = "fas fa-check text-success-500"; |
| | showNotification("已复制到剪贴板", "success", 2000); |
| | } else { |
| | iconElement.className = "fas fa-times text-danger-500"; |
| | showNotification("复制失败", "error", 3000); |
| | } |
| | setTimeout( |
| | () => { |
| | iconElement.className = originalIcon; |
| | }, |
| | success ? 2000 : 3000 |
| | ); |
| | } |
| |
|
| | |
| | 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 setupCopyButtons(containerSelector = "body") { |
| | |
| | const container = document.querySelector(containerSelector); |
| | if (!container) return; |
| |
|
| | const copyButtons = container.querySelectorAll(".copy-btn"); |
| | copyButtons.forEach((button) => { |
| | |
| | button.removeEventListener("click", handleCopyButtonClick); |
| | |
| | button.addEventListener("click", handleCopyButtonClick); |
| | }); |
| | } |
| |
|
| | |
| | function handleCopyButtonClick() { |
| | const button = this; |
| | const targetId = button.getAttribute("data-target"); |
| | const textToCopyDirect = button.getAttribute("data-copy-text"); |
| | 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; |
| | } |
| | } else { |
| | console.error( |
| | "No data-target or data-copy-text attribute found on button:", |
| | button |
| | ); |
| | showNotification("复制出错:未指定复制内容", "error"); |
| | return; |
| | } |
| |
|
| | if (textToCopy) { |
| | _performCopy(textToCopy, button); |
| | } else { |
| | console.warn( |
| | "No text found to copy for target:", |
| | targetId || "direct text" |
| | ); |
| | showNotification("没有内容可复制", "warning"); |
| | } |
| | } |
| |
|
| | |
| | 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; |
| |
|
| | |
| | |
| | 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"); |
| | if (logId) { |
| | logIdsToDelete.push(parseInt(logId)); |
| | } |
| | }); |
| |
|
| | if (logIdsToDelete.length === 0) { |
| | showNotification("没有选中的日志可删除", "warning"); |
| | return; |
| | } |
| |
|
| | if (logIdsToDelete.length === 0) { |
| | showNotification("没有选中的日志可删除", "warning"); |
| | return; |
| | } |
| |
|
| | |
| | idsToDeleteGlobally = logIdsToDelete; |
| | const message = `确定要删除选中的 ${logIdsToDelete.length} 条日志吗?此操作不可恢复!`; |
| | showDeleteConfirmModal(message, function() { |
| | performActualDelete(idsToDeleteGlobally); |
| | }); |
| | } |
| | |
| | |
| | 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, |
| | }; |
| |
|
| | try { |
| | |
| | await fetchAPI(url, options); |
| |
|
| | |
| | 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; |
| |
|
| | |
| | idsToDeleteGlobally = [parseInt(logId)]; |
| | |
| | const message = `确定要删除这条日志吗?此操作不可恢复!`; |
| | showDeleteConfirmModal(message, function() { |
| | performActualDelete([parseInt(logId)]); |
| | }); |
| | } |
| | |
| | |
| | function handleSortById() { |
| | if (errorLogState.sort.field === "id") { |
| | |
| | errorLogState.sort.order = |
| | errorLogState.sort.order === "asc" ? "desc" : "asc"; |
| | } else { |
| | |
| | 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 { |
| | |
| | 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 { |
| | |
| | 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) { |
| | |
| | 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)}`; |
| | } |
| |
|
| | |
| | const data = await fetchAPI(apiUrl); |
| |
|
| | |
| | |
| | if (data && Array.isArray(data.logs)) { |
| | errorLogState.logs = data.logs; |
| | renderErrorLogs(errorLogState.logs); |
| | updatePagination(errorLogState.logs.length, data.total || -1); |
| | } else { |
| | |
| | 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); |
| | } |
| | } |
| |
|
| | |
| | function _createLogRowHtml(log, sequentialId) { |
| | |
| | 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 ` |
| | <td class="text-center px-3 py-3 text-gray-700"> |
| | <input type="checkbox" class="row-checkbox form-checkbox h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" data-key="${fullKey}" data-log-id="${ |
| | log.id |
| | }"> |
| | </td> |
| | <td class="text-gray-700">${sequentialId}</td> |
| | <td class="relative group text-gray-700" title="${fullKey}"> |
| | ${maskedKey} |
| | <button class="copy-btn absolute top-1/2 right-2 transform -translate-y-1/2 bg-gray-200 hover:bg-gray-300 text-gray-600 p-1 rounded opacity-0 group-hover:opacity-100 transition-opacity text-xs" data-copy-text="${fullKey}" title="复制完整密钥"> |
| | <i class="far fa-copy"></i> |
| | </button> |
| | </td> |
| | <td class="text-gray-700">${log.error_type || "未知"}</td> |
| | <td class="error-code-content text-gray-700" title="${ |
| | log.error_code || "" |
| | }">${errorCodeContent}</td> |
| | <td class="text-gray-700">${log.model_name || "未知"}</td> |
| | <td class="text-gray-700">${formattedTime}</td> |
| | <td> |
| | <button class="btn-view-details mr-2 bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-sm transition-all duration-200" data-log-id="${log.id}"> |
| | <i class="fas fa-eye mr-1"></i>详情 |
| | </button> |
| | <button class="btn-delete-row bg-red-600 hover:bg-red-700 text-white px-2 py-1 rounded text-sm transition-all duration-200" data-log-id="${ |
| | log.id |
| | }" title="删除此日志"> |
| | <i class="fas fa-trash-alt"></i> |
| | </button> |
| | </td> |
| | `; |
| | } |
| |
|
| | |
| | function renderErrorLogs(logs) { |
| | if (!tableBody) return; |
| | tableBody.innerHTML = ""; |
| |
|
| | |
| | if (selectAllCheckbox) { |
| | selectAllCheckbox.checked = false; |
| | selectAllCheckbox.indeterminate = false; |
| | } |
| |
|
| | if (!logs || logs.length === 0) { |
| | |
| | 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); |
| | }); |
| |
|
| | |
| | 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); |
| | }); |
| | }); |
| |
|
| | |
| | setupCopyButtons("#errorLogsTable"); |
| | |
| | updateSelectedState(); |
| | } |
| |
|
| | |
| | async function showLogDetails(logId) { |
| | if (!logDetailModal) return; |
| |
|
| | |
| | |
| | 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"; |
| |
|
| | try { |
| | |
| | const logDetails = await fetchAPI(`/api/logs/errors/${logId}/details`); |
| |
|
| | |
| | if (!logDetails) { |
| | |
| | throw new Error("未找到日志详情"); |
| | } |
| |
|
| | |
| | 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); |
| | } |
| |
|
| | |
| | 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") { |
| | |
| | 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); |
| | console.warn("Could not parse request_msg as JSON:", e); |
| | } |
| | } |
| |
|
| | |
| | document.getElementById("modalGeminiKey").textContent = |
| | logDetails.gemini_key || "无"; |
| | document.getElementById("modalErrorType").textContent = |
| | logDetails.error_type || "未知"; |
| | document.getElementById("modalErrorLog").textContent = |
| | logDetails.error_log || "无"; |
| | document.getElementById("modalRequestMsg").textContent = |
| | formattedRequestMsg; |
| | document.getElementById("modalModelName").textContent = |
| | logDetails.model_name || "未知"; |
| | document.getElementById("modalRequestTime").textContent = formattedTime; |
| |
|
| | |
| | setupCopyButtons("#logDetailModal"); |
| | } catch (error) { |
| | console.error("获取日志详情失败:", error); |
| | |
| | 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 = "错误"; |
| | |
| | showNotification(`加载日志详情失败: ${error.message}`, "error", 5000); |
| | } |
| | } |
| |
|
| | |
| | function closeLogDetailModal() { |
| | if (logDetailModal) { |
| | logDetailModal.classList.remove("show"); |
| | |
| | document.body.style.overflow = ""; |
| | } |
| | } |
| |
|
| | |
| | function updatePagination(currentItemCount, totalItems) { |
| | if (!paginationElement) return; |
| | paginationElement.innerHTML = ""; |
| |
|
| | |
| | let totalPages = 1; |
| | if (totalItems >= 0) { |
| | totalPages = Math.max(1, Math.ceil(totalItems / errorLogState.pageSize)); |
| | } else if ( |
| | currentItemCount < errorLogState.pageSize && |
| | errorLogState.currentPage === 1 |
| | ) { |
| | |
| | totalPages = 1; |
| | } else { |
| | |
| | |
| | console.warn("Total item count unknown, pagination will be limited."); |
| | |
| | addPaginationLink( |
| | paginationElement, |
| | "«", |
| | errorLogState.currentPage > 1, |
| | () => { |
| | errorLogState.currentPage--; |
| | loadErrorLogs(); |
| | } |
| | ); |
| | addPaginationLink( |
| | paginationElement, |
| | errorLogState.currentPage.toString(), |
| | true, |
| | null, |
| | true |
| | ); |
| | addPaginationLink( |
| | paginationElement, |
| | "»", |
| | currentItemCount === errorLogState.pageSize, |
| | () => { |
| | errorLogState.currentPage++; |
| | loadErrorLogs(); |
| | } |
| | ); |
| | return; |
| | } |
| |
|
| | const maxPagesToShow = 5; |
| | let startPage = Math.max( |
| | 1, |
| | errorLogState.currentPage - Math.floor(maxPagesToShow / 2) |
| | ); |
| | let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1); |
| |
|
| | |
| | if (endPage === totalPages) { |
| | startPage = Math.max(1, endPage - maxPagesToShow + 1); |
| | } |
| |
|
| | |
| | addPaginationLink( |
| | paginationElement, |
| | "«", |
| | errorLogState.currentPage > 1, |
| | () => { |
| | errorLogState.currentPage--; |
| | loadErrorLogs(); |
| | } |
| | ); |
| |
|
| | |
| | if (startPage > 1) { |
| | addPaginationLink(paginationElement, "1", true, () => { |
| | errorLogState.currentPage = 1; |
| | loadErrorLogs(); |
| | }); |
| | if (startPage > 2) { |
| | addPaginationLink(paginationElement, "...", false); |
| | } |
| | } |
| |
|
| | |
| | for (let i = startPage; i <= endPage; i++) { |
| | addPaginationLink( |
| | paginationElement, |
| | i.toString(), |
| | true, |
| | () => { |
| | errorLogState.currentPage = i; |
| | loadErrorLogs(); |
| | }, |
| | i === errorLogState.currentPage |
| | ); |
| | } |
| |
|
| | |
| | if (endPage < totalPages) { |
| | if (endPage < totalPages - 1) { |
| | addPaginationLink(paginationElement, "...", false); |
| | } |
| | addPaginationLink(paginationElement, totalPages.toString(), true, () => { |
| | errorLogState.currentPage = totalPages; |
| | loadErrorLogs(); |
| | }); |
| | } |
| |
|
| | |
| | addPaginationLink( |
| | paginationElement, |
| | "»", |
| | errorLogState.currentPage < totalPages, |
| | () => { |
| | errorLogState.currentPage++; |
| | loadErrorLogs(); |
| | } |
| | ); |
| | } |
| |
|
| | |
| | function addPaginationLink( |
| | parentElement, |
| | text, |
| | enabled, |
| | clickHandler, |
| | isActive = false |
| | ) { |
| | |
| |
|
| | const pageLink = document.createElement("a"); |
| |
|
| | |
| | let baseClasses = |
| | "px-3 py-1 rounded-md text-sm transition duration-150 ease-in-out"; |
| |
|
| | if (isActive) { |
| | pageLink.className = `${baseClasses} active`; |
| | } else if (enabled) { |
| | pageLink.className = baseClasses; |
| | } else { |
| | |
| | pageLink.className = `${baseClasses} disabled`; |
| | } |
| |
|
| | pageLink.href = "#"; |
| | pageLink.innerHTML = text; |
| |
|
| | if (enabled && clickHandler) { |
| | pageLink.addEventListener("click", function (e) { |
| | e.preventDefault(); |
| | clickHandler(); |
| | }); |
| | } else { |
| | |
| | pageLink.addEventListener("click", (e) => e.preventDefault()); |
| | } |
| |
|
| | parentElement.appendChild(pageLink); |
| | } |
| |
|
| | |
| | function showLoading(show) { |
| | if (loadingIndicator) |
| | loadingIndicator.style.display = show ? "block" : "none"; |
| | } |
| |
|
| | function showNoData(show) { |
| | if (noDataMessage) noDataMessage.style.display = show ? "block" : "none"; |
| | } |
| |
|
| | function showError(show, message = "加载错误日志失败,请稍后重试。") { |
| | if (errorMessage) { |
| | errorMessage.style.display = show ? "block" : "none"; |
| | if (show) { |
| | |
| | const p = errorMessage.querySelector("p"); |
| | if (p) p.textContent = message; |
| | } |
| | } |
| | } |
| |
|
| | |
| | function showNotification(message, type = "success", duration = 3000) { |
| | const notificationElement = document.getElementById("notification"); |
| | if (!notificationElement) { |
| | console.error("Notification element with ID 'notification' not found."); |
| | return; |
| | } |
| |
|
| | |
| | notificationElement.textContent = message; |
| | |
| | notificationElement.classList.remove("success", "error", "warning", "info"); |
| | notificationElement.classList.add(type); |
| | notificationElement.className = `notification ${type} show`; |
| |
|
| | |
| | setTimeout(() => { |
| | notificationElement.classList.remove("show"); |
| | }, duration); |
| | } |
| |
|
| | |
| | |
| | |
| |
|