|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|