Update: Js 4 Log.html 80%
This commit is contained in:
360
web/static/js/chunk-SHK62ZJN.js
Normal file
360
web/static/js/chunk-SHK62ZJN.js
Normal file
@@ -0,0 +1,360 @@
|
||||
// frontend/js/components/ui.js
|
||||
var ModalManager = class {
|
||||
/**
|
||||
* Shows a generic modal by its ID.
|
||||
* @param {string} modalId The ID of the modal element to show.
|
||||
*/
|
||||
show(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.remove("hidden");
|
||||
} else {
|
||||
console.error(`Modal with ID "${modalId}" not found.`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Hides a generic modal by its ID.
|
||||
* @param {string} modalId The ID of the modal element to hide.
|
||||
*/
|
||||
hide(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.add("hidden");
|
||||
} else {
|
||||
console.error(`Modal with ID "${modalId}" not found.`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Shows a confirmation dialog. This is a versatile method for 'Are you sure?' style prompts.
|
||||
* It dynamically sets the title, message, and confirm action for a generic confirmation modal.
|
||||
* @param {object} options - The options for the confirmation modal.
|
||||
* @param {string} options.modalId - The ID of the confirmation modal element (e.g., 'resetModal', 'deleteConfirmModal').
|
||||
* @param {string} options.title - The title to display in the modal header.
|
||||
* @param {string} options.message - The message to display in the modal body. Can contain HTML.
|
||||
* @param {function} options.onConfirm - The callback function to execute when the confirm button is clicked.
|
||||
* @param {boolean} [options.disableConfirm=false] - Whether the confirm button should be initially disabled.
|
||||
*/
|
||||
showConfirm({ modalId, title, message, onConfirm, disableConfirm = false }) {
|
||||
const modalElement = document.getElementById(modalId);
|
||||
if (!modalElement) {
|
||||
console.error(`Confirmation modal with ID "${modalId}" not found.`);
|
||||
return;
|
||||
}
|
||||
const titleElement = modalElement.querySelector('[id$="ModalTitle"]');
|
||||
const messageElement = modalElement.querySelector('[id$="ModalMessage"]');
|
||||
const confirmButton = modalElement.querySelector('[id^="confirm"]');
|
||||
if (!titleElement || !messageElement || !confirmButton) {
|
||||
console.error(`Modal "${modalId}" is missing required child elements (title, message, or confirm button).`);
|
||||
return;
|
||||
}
|
||||
titleElement.textContent = title;
|
||||
messageElement.innerHTML = message;
|
||||
confirmButton.disabled = disableConfirm;
|
||||
const newConfirmButton = confirmButton.cloneNode(true);
|
||||
confirmButton.parentNode.replaceChild(newConfirmButton, confirmButton);
|
||||
newConfirmButton.onclick = () => onConfirm();
|
||||
this.show(modalId);
|
||||
}
|
||||
/**
|
||||
* Shows a result modal to indicate the outcome of an operation (success or failure).
|
||||
* @param {boolean} success - If true, displays a success icon and title; otherwise, shows failure indicators.
|
||||
* @param {string|Node} message - The message to display. Can be a simple string or a complex DOM Node for rich content.
|
||||
* @param {boolean} [autoReload=false] - If true, the page will automatically reload when the modal is closed.
|
||||
*/
|
||||
showResult(success, message, autoReload = false) {
|
||||
const modalElement = document.getElementById("resultModal");
|
||||
if (!modalElement) {
|
||||
console.error("Result modal with ID 'resultModal' not found.");
|
||||
return;
|
||||
}
|
||||
const titleElement = document.getElementById("resultModalTitle");
|
||||
const messageElement = document.getElementById("resultModalMessage");
|
||||
const iconElement = document.getElementById("resultIcon");
|
||||
const confirmButton = document.getElementById("resultModalConfirmBtn");
|
||||
if (!titleElement || !messageElement || !iconElement || !confirmButton) {
|
||||
console.error("Result modal is missing required child elements.");
|
||||
return;
|
||||
}
|
||||
titleElement.textContent = success ? "\u64CD\u4F5C\u6210\u529F" : "\u64CD\u4F5C\u5931\u8D25";
|
||||
if (success) {
|
||||
iconElement.innerHTML = '<i class="fas fa-check-circle text-success-500"></i>';
|
||||
iconElement.className = "text-6xl mb-3 text-success-500";
|
||||
} else {
|
||||
iconElement.innerHTML = '<i class="fas fa-times-circle text-danger-500"></i>';
|
||||
iconElement.className = "text-6xl mb-3 text-danger-500";
|
||||
}
|
||||
messageElement.innerHTML = "";
|
||||
if (typeof message === "string") {
|
||||
const messageDiv = document.createElement("div");
|
||||
messageDiv.innerText = message;
|
||||
messageElement.appendChild(messageDiv);
|
||||
} else if (message instanceof Node) {
|
||||
messageElement.appendChild(message);
|
||||
} else {
|
||||
const messageDiv = document.createElement("div");
|
||||
messageDiv.innerText = String(message);
|
||||
messageElement.appendChild(messageDiv);
|
||||
}
|
||||
confirmButton.onclick = () => this.closeResult(autoReload);
|
||||
this.show("resultModal");
|
||||
}
|
||||
/**
|
||||
* Closes the result modal.
|
||||
* @param {boolean} [reload=false] - If true, reloads the page after closing the modal.
|
||||
*/
|
||||
closeResult(reload = false) {
|
||||
this.hide("resultModal");
|
||||
if (reload) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Shows and initializes the progress modal for long-running operations.
|
||||
* @param {string} title - The title to display for the progress modal.
|
||||
*/
|
||||
showProgress(title) {
|
||||
const modal = document.getElementById("progressModal");
|
||||
if (!modal) {
|
||||
console.error("Progress modal with ID 'progressModal' not found.");
|
||||
return;
|
||||
}
|
||||
const titleElement = document.getElementById("progressModalTitle");
|
||||
const statusText = document.getElementById("progressStatusText");
|
||||
const progressBar = document.getElementById("progressBar");
|
||||
const progressPercentage = document.getElementById("progressPercentage");
|
||||
const progressLog = document.getElementById("progressLog");
|
||||
const closeButton = document.getElementById("progressModalCloseBtn");
|
||||
const closeIcon = document.getElementById("closeProgressModalBtn");
|
||||
if (!titleElement || !statusText || !progressBar || !progressPercentage || !progressLog || !closeButton || !closeIcon) {
|
||||
console.error("Progress modal is missing required child elements.");
|
||||
return;
|
||||
}
|
||||
titleElement.textContent = title;
|
||||
statusText.textContent = "\u51C6\u5907\u5F00\u59CB...";
|
||||
progressBar.style.width = "0%";
|
||||
progressPercentage.textContent = "0%";
|
||||
progressLog.innerHTML = "";
|
||||
closeButton.disabled = true;
|
||||
closeIcon.disabled = true;
|
||||
this.show("progressModal");
|
||||
}
|
||||
/**
|
||||
* Updates the progress bar and status text within the progress modal.
|
||||
* @param {number} processed - The number of items that have been processed.
|
||||
* @param {number} total - The total number of items to process.
|
||||
* @param {string} status - The current status message to display.
|
||||
*/
|
||||
updateProgress(processed, total, status) {
|
||||
const modal = document.getElementById("progressModal");
|
||||
if (!modal || modal.classList.contains("hidden")) return;
|
||||
const progressBar = document.getElementById("progressBar");
|
||||
const progressPercentage = document.getElementById("progressPercentage");
|
||||
const statusText = document.getElementById("progressStatusText");
|
||||
const closeButton = document.getElementById("progressModalCloseBtn");
|
||||
const closeIcon = document.getElementById("closeProgressModalBtn");
|
||||
const percentage = total > 0 ? Math.round(processed / total * 100) : 0;
|
||||
progressBar.style.width = `${percentage}%`;
|
||||
progressPercentage.textContent = `${percentage}%`;
|
||||
statusText.textContent = status;
|
||||
if (processed === total) {
|
||||
closeButton.disabled = false;
|
||||
closeIcon.disabled = false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Adds a log entry to the progress modal's log area.
|
||||
* @param {string} message - The log message to append.
|
||||
* @param {boolean} [isError=false] - If true, styles the log entry as an error.
|
||||
*/
|
||||
addProgressLog(message, isError = false) {
|
||||
const progressLog = document.getElementById("progressLog");
|
||||
if (!progressLog) return;
|
||||
const logEntry = document.createElement("div");
|
||||
logEntry.textContent = message;
|
||||
logEntry.className = isError ? "text-danger-600" : "text-gray-700";
|
||||
progressLog.appendChild(logEntry);
|
||||
progressLog.scrollTop = progressLog.scrollHeight;
|
||||
}
|
||||
/**
|
||||
* Closes the progress modal.
|
||||
* @param {boolean} [reload=false] - If true, reloads the page after closing.
|
||||
*/
|
||||
closeProgress(reload = false) {
|
||||
this.hide("progressModal");
|
||||
if (reload) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
};
|
||||
var UIPatterns = class {
|
||||
/**
|
||||
* Animates numerical values in elements from 0 to their target number.
|
||||
* The target number is read from the element's text content.
|
||||
* @param {string} selector - The CSS selector for the elements to animate (e.g., '.stat-value').
|
||||
* @param {number} [duration=1500] - The duration of the animation in milliseconds.
|
||||
*/
|
||||
animateCounters(selector = ".stat-value", duration = 1500) {
|
||||
const statValues = document.querySelectorAll(selector);
|
||||
statValues.forEach((valueElement) => {
|
||||
const finalValue = parseInt(valueElement.textContent, 10);
|
||||
if (isNaN(finalValue)) return;
|
||||
if (!valueElement.dataset.originalValue) {
|
||||
valueElement.dataset.originalValue = valueElement.textContent;
|
||||
}
|
||||
let startValue = 0;
|
||||
const startTime = performance.now();
|
||||
const updateCounter = (currentTime) => {
|
||||
const elapsedTime = currentTime - startTime;
|
||||
if (elapsedTime < duration) {
|
||||
const progress = elapsedTime / duration;
|
||||
const easeOutValue = 1 - Math.pow(1 - progress, 3);
|
||||
const currentValue = Math.floor(easeOutValue * finalValue);
|
||||
valueElement.textContent = currentValue;
|
||||
requestAnimationFrame(updateCounter);
|
||||
} else {
|
||||
valueElement.textContent = valueElement.dataset.originalValue;
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(updateCounter);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Toggles the visibility of a content section with a smooth height animation.
|
||||
* It expects a specific HTML structure where the header and content are within a common parent (e.g., a card).
|
||||
* The content element should have a `collapsed` class when hidden.
|
||||
* @param {HTMLElement} header - The header element that was clicked to trigger the toggle.
|
||||
*/
|
||||
toggleSection(header) {
|
||||
const card = header.closest(".stats-card");
|
||||
if (!card) return;
|
||||
const content = card.querySelector(".key-content");
|
||||
const toggleIcon = header.querySelector(".toggle-icon");
|
||||
if (!content || !toggleIcon) {
|
||||
console.error("Toggle section failed: Content or icon element not found.", { header });
|
||||
return;
|
||||
}
|
||||
const isCollapsed = content.classList.contains("collapsed");
|
||||
toggleIcon.classList.toggle("collapsed", !isCollapsed);
|
||||
if (isCollapsed) {
|
||||
content.classList.remove("collapsed");
|
||||
content.style.maxHeight = null;
|
||||
content.style.opacity = null;
|
||||
content.style.paddingTop = null;
|
||||
content.style.paddingBottom = null;
|
||||
content.style.overflow = "hidden";
|
||||
requestAnimationFrame(() => {
|
||||
const targetHeight = content.scrollHeight;
|
||||
content.style.maxHeight = `${targetHeight}px`;
|
||||
content.style.opacity = "1";
|
||||
content.style.paddingTop = "1rem";
|
||||
content.style.paddingBottom = "1rem";
|
||||
content.addEventListener("transitionend", function onExpansionEnd() {
|
||||
content.removeEventListener("transitionend", onExpansionEnd);
|
||||
if (!content.classList.contains("collapsed")) {
|
||||
content.style.maxHeight = "";
|
||||
content.style.overflow = "visible";
|
||||
}
|
||||
}, { once: true });
|
||||
});
|
||||
} else {
|
||||
const currentHeight = content.scrollHeight;
|
||||
content.style.maxHeight = `${currentHeight}px`;
|
||||
content.style.overflow = "hidden";
|
||||
requestAnimationFrame(() => {
|
||||
content.style.maxHeight = "0px";
|
||||
content.style.opacity = "0";
|
||||
content.style.paddingTop = "0";
|
||||
content.style.paddingBottom = "0";
|
||||
content.classList.add("collapsed");
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
var modalManager = new ModalManager();
|
||||
var uiPatterns = new UIPatterns();
|
||||
|
||||
// frontend/js/services/api.js
|
||||
var APIClientError = class extends Error {
|
||||
constructor(message, status, code, rawMessageFromServer) {
|
||||
super(message);
|
||||
this.name = "APIClientError";
|
||||
this.status = status;
|
||||
this.code = code;
|
||||
this.rawMessageFromServer = rawMessageFromServer;
|
||||
}
|
||||
};
|
||||
var apiPromiseCache = /* @__PURE__ */ new Map();
|
||||
async function apiFetch(url, options = {}) {
|
||||
const isGetRequest = !options.method || options.method.toUpperCase() === "GET";
|
||||
const cacheKey = isGetRequest && !options.noCache ? url : null;
|
||||
if (cacheKey && apiPromiseCache.has(cacheKey)) {
|
||||
return apiPromiseCache.get(cacheKey);
|
||||
}
|
||||
const token = localStorage.getItem("bearerToken");
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers
|
||||
};
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
const requestPromise = (async () => {
|
||||
try {
|
||||
const response = await fetch(url, { ...options, headers });
|
||||
if (response.status === 401) {
|
||||
if (cacheKey) apiPromiseCache.delete(cacheKey);
|
||||
localStorage.removeItem("bearerToken");
|
||||
if (window.location.pathname !== "/login") {
|
||||
window.location.href = "/login?error=\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u3002";
|
||||
}
|
||||
throw new APIClientError("Unauthorized", 401, "UNAUTHORIZED", "Session expired or token is invalid.");
|
||||
}
|
||||
if (!response.ok) {
|
||||
let errorData = null;
|
||||
let rawMessage = "";
|
||||
try {
|
||||
rawMessage = await response.text();
|
||||
if (rawMessage) {
|
||||
errorData = JSON.parse(rawMessage);
|
||||
}
|
||||
} catch (e) {
|
||||
errorData = { error: { code: "UNKNOWN_FORMAT", message: rawMessage || response.statusText } };
|
||||
}
|
||||
const code = errorData?.error?.code || "UNKNOWN_ERROR";
|
||||
const messageFromServer = errorData?.error?.message || rawMessage || "No message provided by server.";
|
||||
const error = new APIClientError(
|
||||
`API request failed: ${response.status}`,
|
||||
response.status,
|
||||
code,
|
||||
messageFromServer
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (cacheKey) apiPromiseCache.delete(cacheKey);
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
if (cacheKey) {
|
||||
apiPromiseCache.set(cacheKey, requestPromise);
|
||||
}
|
||||
return requestPromise;
|
||||
}
|
||||
async function apiFetchJson(url, options = {}) {
|
||||
try {
|
||||
const response = await apiFetch(url, options);
|
||||
const clonedResponse = response.clone();
|
||||
const jsonData = await clonedResponse.json();
|
||||
return jsonData;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
modalManager,
|
||||
uiPatterns,
|
||||
apiFetch,
|
||||
apiFetchJson
|
||||
};
|
||||
Reference in New Issue
Block a user