Update: Js 4 Log.html 80%
This commit is contained in:
@@ -488,9 +488,6 @@
|
||||
.m-0 {
|
||||
margin: calc(var(--spacing) * 0);
|
||||
}
|
||||
.m-2 {
|
||||
margin: calc(var(--spacing) * 2);
|
||||
}
|
||||
.mx-1 {
|
||||
margin-inline: calc(var(--spacing) * 1);
|
||||
}
|
||||
@@ -506,9 +503,6 @@
|
||||
.my-2 {
|
||||
margin-block: calc(var(--spacing) * 2);
|
||||
}
|
||||
.mt-0 {
|
||||
margin-top: calc(var(--spacing) * 0);
|
||||
}
|
||||
.mt-0\.5 {
|
||||
margin-top: calc(var(--spacing) * 0.5);
|
||||
}
|
||||
@@ -630,9 +624,6 @@
|
||||
width: calc(var(--spacing) * 6);
|
||||
height: calc(var(--spacing) * 6);
|
||||
}
|
||||
.h-0 {
|
||||
height: calc(var(--spacing) * 0);
|
||||
}
|
||||
.h-0\.5 {
|
||||
height: calc(var(--spacing) * 0.5);
|
||||
}
|
||||
@@ -717,9 +708,6 @@
|
||||
.w-0 {
|
||||
width: calc(var(--spacing) * 0);
|
||||
}
|
||||
.w-1 {
|
||||
width: calc(var(--spacing) * 1);
|
||||
}
|
||||
.w-1\/4 {
|
||||
width: calc(1/4 * 100%);
|
||||
}
|
||||
@@ -771,6 +759,9 @@
|
||||
.w-32 {
|
||||
width: calc(var(--spacing) * 32);
|
||||
}
|
||||
.w-40 {
|
||||
width: calc(var(--spacing) * 40);
|
||||
}
|
||||
.w-48 {
|
||||
width: calc(var(--spacing) * 48);
|
||||
}
|
||||
@@ -831,9 +822,6 @@
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
.flex-shrink {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -846,9 +834,6 @@
|
||||
.caption-bottom {
|
||||
caption-side: bottom;
|
||||
}
|
||||
.border-collapse {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.origin-center {
|
||||
transform-origin: center;
|
||||
}
|
||||
@@ -875,10 +860,6 @@
|
||||
--tw-translate-x: 100%;
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-1 {
|
||||
--tw-translate-y: calc(var(--spacing) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-1\/2 {
|
||||
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
@@ -1047,9 +1028,6 @@
|
||||
margin-block-end: calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)));
|
||||
}
|
||||
}
|
||||
.gap-x-1 {
|
||||
column-gap: calc(var(--spacing) * 1);
|
||||
}
|
||||
.gap-x-1\.5 {
|
||||
column-gap: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
@@ -1163,6 +1141,10 @@
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 1px;
|
||||
}
|
||||
.border-1 {
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 1px;
|
||||
}
|
||||
.border-2 {
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 2px;
|
||||
@@ -1198,9 +1180,6 @@
|
||||
.\!border-primary {
|
||||
border-color: var(--color-primary) !important;
|
||||
}
|
||||
.border-black {
|
||||
border-color: var(--color-black);
|
||||
}
|
||||
.border-black\/10 {
|
||||
border-color: color-mix(in srgb, #000 10%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
@@ -1228,9 +1207,6 @@
|
||||
.border-green-200 {
|
||||
border-color: var(--color-green-200);
|
||||
}
|
||||
.border-primary {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
.border-primary\/20 {
|
||||
border-color: var(--color-primary);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
@@ -1267,8 +1243,11 @@
|
||||
.border-zinc-300 {
|
||||
border-color: var(--color-zinc-300);
|
||||
}
|
||||
.border-zinc-700 {
|
||||
border-color: var(--color-zinc-700);
|
||||
.border-zinc-500\/30 {
|
||||
border-color: color-mix(in srgb, oklch(55.2% 0.016 285.938) 30%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
border-color: color-mix(in oklab, var(--color-zinc-500) 30%, transparent);
|
||||
}
|
||||
}
|
||||
.border-zinc-700\/50 {
|
||||
border-color: color-mix(in srgb, oklch(37% 0.013 285.805) 50%, transparent);
|
||||
@@ -1282,6 +1261,9 @@
|
||||
.border-b-border {
|
||||
border-bottom-color: var(--color-border);
|
||||
}
|
||||
.\!bg-primary {
|
||||
background-color: var(--color-primary) !important;
|
||||
}
|
||||
.bg-accent {
|
||||
background-color: var(--color-accent);
|
||||
}
|
||||
@@ -1342,9 +1324,6 @@
|
||||
.bg-gray-500 {
|
||||
background-color: var(--color-gray-500);
|
||||
}
|
||||
.bg-gray-950 {
|
||||
background-color: var(--color-gray-950);
|
||||
}
|
||||
.bg-gray-950\/5 {
|
||||
background-color: color-mix(in srgb, oklch(13% 0.028 261.692) 5%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
@@ -1559,10 +1538,6 @@
|
||||
--tw-gradient-position: to right in oklab;
|
||||
background-image: linear-gradient(var(--tw-gradient-stops));
|
||||
}
|
||||
.from-blue-500 {
|
||||
--tw-gradient-from: var(--color-blue-500);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.from-blue-500\/30 {
|
||||
--tw-gradient-from: color-mix(in srgb, oklch(62.3% 0.214 259.815) 30%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
@@ -1633,9 +1608,6 @@
|
||||
.px-8 {
|
||||
padding-inline: calc(var(--spacing) * 8);
|
||||
}
|
||||
.py-0 {
|
||||
padding-block: calc(var(--spacing) * 0);
|
||||
}
|
||||
.py-0\.5 {
|
||||
padding-block: calc(var(--spacing) * 0.5);
|
||||
}
|
||||
@@ -1684,9 +1656,6 @@
|
||||
.pr-20 {
|
||||
padding-right: calc(var(--spacing) * 20);
|
||||
}
|
||||
.pb-1 {
|
||||
padding-bottom: calc(var(--spacing) * 1);
|
||||
}
|
||||
.pb-1\.5 {
|
||||
padding-bottom: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
@@ -1808,6 +1777,9 @@
|
||||
.\!text-primary {
|
||||
color: var(--color-primary) !important;
|
||||
}
|
||||
.\!text-primary-foreground {
|
||||
color: var(--color-primary-foreground) !important;
|
||||
}
|
||||
.text-amber-300 {
|
||||
color: var(--color-amber-300);
|
||||
}
|
||||
@@ -1977,9 +1949,6 @@
|
||||
--tw-ordinal: ordinal;
|
||||
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
||||
}
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.opacity-0 {
|
||||
opacity: 0%;
|
||||
}
|
||||
@@ -2041,10 +2010,6 @@
|
||||
--tw-inset-shadow: inset 0 2px 4px var(--tw-inset-shadow-color, oklab(from rgb(0 0 0 / 0.05) l a b / 25%));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.inset-shadow-sm {
|
||||
--tw-inset-shadow: inset 0 2px 4px var(--tw-inset-shadow-color, rgb(0 0 0 / 0.05));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.ring-black {
|
||||
--tw-ring-color: var(--color-black);
|
||||
}
|
||||
@@ -2069,19 +2034,12 @@
|
||||
.ring-input {
|
||||
--tw-ring-color: var(--color-input);
|
||||
}
|
||||
.ring-zinc-500 {
|
||||
--tw-ring-color: var(--color-zinc-500);
|
||||
}
|
||||
.ring-zinc-500\/30 {
|
||||
--tw-ring-color: color-mix(in srgb, oklch(55.2% 0.016 285.938) 30%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
--tw-ring-color: color-mix(in oklab, var(--color-zinc-500) 30%, transparent);
|
||||
}
|
||||
}
|
||||
.outline {
|
||||
outline-style: var(--tw-outline-style);
|
||||
outline-width: 1px;
|
||||
}
|
||||
.blur {
|
||||
--tw-blur: blur(8px);
|
||||
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
||||
@@ -2253,6 +2211,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:\!bg-primary\/90 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-primary) !important;
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--color-primary) 90%, transparent) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-accent {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
@@ -3481,6 +3449,10 @@
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 1px;
|
||||
border-color: var(--color-border);
|
||||
border-color: color-mix(in srgb, oklch(55.2% 0.016 285.938) 30%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
border-color: color-mix(in oklab, var(--color-zinc-500) 30%, transparent);
|
||||
}
|
||||
background-color: var(--color-background);
|
||||
font-family: var(--font-sans);
|
||||
color: var(--color-foreground);
|
||||
@@ -5610,11 +5582,6 @@
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
@property --tw-outline-style {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: solid;
|
||||
}
|
||||
@property --tw-blur {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
@@ -5712,6 +5679,11 @@
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-outline-style {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: solid;
|
||||
}
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
@@ -5777,7 +5749,6 @@
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-outline-style: solid;
|
||||
--tw-blur: initial;
|
||||
--tw-brightness: initial;
|
||||
--tw-contrast: initial;
|
||||
@@ -5802,6 +5773,7 @@
|
||||
--tw-backdrop-sepia: initial;
|
||||
--tw-duration: initial;
|
||||
--tw-ease: initial;
|
||||
--tw-outline-style: solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
// 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 {
|
||||
apiFetch,
|
||||
apiFetchJson
|
||||
};
|
||||
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
|
||||
};
|
||||
@@ -111,281 +111,6 @@ var CustomSelect = class _CustomSelect {
|
||||
}
|
||||
};
|
||||
|
||||
// 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/components/taskCenter.js
|
||||
var TaskCenterManager = class {
|
||||
constructor() {
|
||||
@@ -810,8 +535,6 @@ var toastManager = new ToastManager();
|
||||
|
||||
export {
|
||||
CustomSelect,
|
||||
modalManager,
|
||||
uiPatterns,
|
||||
taskCenterManager,
|
||||
toastManager
|
||||
};
|
||||
@@ -1,9 +1,8 @@
|
||||
import {
|
||||
CustomSelect,
|
||||
modalManager,
|
||||
taskCenterManager,
|
||||
toastManager
|
||||
} from "./chunk-EZAP7GR4.js";
|
||||
} from "./chunk-U67KAGZP.js";
|
||||
import {
|
||||
debounce,
|
||||
escapeHTML,
|
||||
@@ -11,8 +10,9 @@ import {
|
||||
} from "./chunk-A4OOMLXK.js";
|
||||
import {
|
||||
apiFetch,
|
||||
apiFetchJson
|
||||
} from "./chunk-PLQL6WIO.js";
|
||||
apiFetchJson,
|
||||
modalManager
|
||||
} from "./chunk-SHK62ZJN.js";
|
||||
import "./chunk-JSBRDJBE.js";
|
||||
|
||||
// frontend/js/components/tagInput.js
|
||||
@@ -3,8 +3,9 @@ import {
|
||||
escapeHTML
|
||||
} from "./chunk-A4OOMLXK.js";
|
||||
import {
|
||||
apiFetchJson
|
||||
} from "./chunk-PLQL6WIO.js";
|
||||
apiFetchJson,
|
||||
modalManager
|
||||
} from "./chunk-SHK62ZJN.js";
|
||||
import {
|
||||
__commonJS,
|
||||
__toESM
|
||||
@@ -1767,7 +1768,7 @@ var FilterPopover = class {
|
||||
}
|
||||
_createPopoverHTML() {
|
||||
this.popoverElement = document.createElement("div");
|
||||
this.popoverElement.className = "hidden z-50 min-w-[12rem] rounded-md border bg-popover bg-white dark:bg-zinc-800 p-2 text-popover-foreground shadow-md";
|
||||
this.popoverElement.className = "hidden z-50 min-w-[12rem] rounded-md border-1 border-zinc-500/30 bg-popover bg-white dark:bg-zinc-900 p-2 text-popover-foreground shadow-md";
|
||||
this.popoverElement.innerHTML = `
|
||||
<div class="px-2 py-1.5 text-sm font-semibold">${this.title}</div>
|
||||
<div class="space-y-1 p-1">
|
||||
@@ -2103,6 +2104,132 @@ function initBatchActions(logsPage) {
|
||||
|
||||
// frontend/js/pages/logs/index.js
|
||||
var import_flatpickr = __toESM(require_flatpickr());
|
||||
|
||||
// frontend/js/pages/logs/logSettingsModal.js
|
||||
var LogSettingsModal = class {
|
||||
constructor({ onSave }) {
|
||||
this.modalId = "log-settings-modal";
|
||||
this.onSave = onSave;
|
||||
const modal = document.getElementById(this.modalId);
|
||||
if (!modal) {
|
||||
throw new Error(`Modal with id "${this.modalId}" not found.`);
|
||||
}
|
||||
this.elements = {
|
||||
modal,
|
||||
title: document.getElementById("log-settings-modal-title"),
|
||||
saveBtn: document.getElementById("log-settings-save-btn"),
|
||||
logLevelSelect: document.getElementById("log-level-select"),
|
||||
cleanupEnableToggle: document.getElementById("log-cleanup-enable"),
|
||||
cleanupSettingsPanel: document.getElementById("log-cleanup-settings"),
|
||||
cleanupRetentionInput: document.getElementById("log-cleanup-retention-days"),
|
||||
retentionDaysGroup: document.getElementById("retention-days-group"),
|
||||
retentionPresetBtns: document.querySelectorAll("#retention-days-group button[data-days]"),
|
||||
cleanupExecTimeInput: document.getElementById("log-cleanup-exec-time")
|
||||
// [NEW] 添加时间选择器元素
|
||||
};
|
||||
this.activePresetClasses = ["!bg-primary", "!text-primary-foreground", "!border-primary", "hover:!bg-primary/90"];
|
||||
this.inactivePresetClasses = ["modal-btn-secondary"];
|
||||
this._initEventListeners();
|
||||
}
|
||||
open(settingsData = {}) {
|
||||
this._populateForm(settingsData);
|
||||
modalManager.show(this.modalId);
|
||||
}
|
||||
close() {
|
||||
modalManager.hide(this.modalId);
|
||||
}
|
||||
_initEventListeners() {
|
||||
this.elements.saveBtn.addEventListener("click", this._handleSave.bind(this));
|
||||
this.elements.cleanupEnableToggle.addEventListener("change", (e) => {
|
||||
this.elements.cleanupSettingsPanel.classList.toggle("hidden", !e.target.checked);
|
||||
});
|
||||
this._initRetentionPresets();
|
||||
const closeAction = () => this.close();
|
||||
const closeTriggers = this.elements.modal.querySelectorAll(`[data-modal-close="${this.modalId}"]`);
|
||||
closeTriggers.forEach((trigger) => trigger.addEventListener("click", closeAction));
|
||||
this.elements.modal.addEventListener("click", (event) => {
|
||||
if (event.target === this.elements.modal) closeAction();
|
||||
});
|
||||
}
|
||||
_initRetentionPresets() {
|
||||
this.elements.retentionPresetBtns.forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const days = btn.dataset.days;
|
||||
this.elements.cleanupRetentionInput.value = days;
|
||||
this._updateActivePresetButton(days);
|
||||
});
|
||||
});
|
||||
this.elements.cleanupRetentionInput.addEventListener("input", (e) => {
|
||||
this._updateActivePresetButton(e.target.value);
|
||||
});
|
||||
}
|
||||
_updateActivePresetButton(currentValue) {
|
||||
this.elements.retentionPresetBtns.forEach((btn) => {
|
||||
if (btn.dataset.days === currentValue) {
|
||||
btn.classList.remove(...this.inactivePresetClasses);
|
||||
btn.classList.add(...this.activePresetClasses);
|
||||
} else {
|
||||
btn.classList.remove(...this.activePresetClasses);
|
||||
btn.classList.add(...this.inactivePresetClasses);
|
||||
}
|
||||
});
|
||||
}
|
||||
async _handleSave() {
|
||||
const data = this._collectFormData();
|
||||
if (data.auto_cleanup.enabled && (!data.auto_cleanup.retention_days || data.auto_cleanup.retention_days <= 0)) {
|
||||
alert("\u542F\u7528\u81EA\u52A8\u6E05\u7406\u65F6\uFF0C\u4FDD\u7559\u5929\u6570\u5FC5\u987B\u662F\u5927\u4E8E0\u7684\u6570\u5B57\u3002");
|
||||
return;
|
||||
}
|
||||
if (this.onSave) {
|
||||
this.elements.saveBtn.disabled = true;
|
||||
this.elements.saveBtn.textContent = "\u4FDD\u5B58\u4E2D...";
|
||||
try {
|
||||
await this.onSave(data);
|
||||
this.close();
|
||||
} catch (error) {
|
||||
console.error("Failed to save log settings:", error);
|
||||
} finally {
|
||||
this.elements.saveBtn.disabled = false;
|
||||
this.elements.saveBtn.textContent = "\u4FDD\u5B58\u8BBE\u7F6E";
|
||||
}
|
||||
}
|
||||
}
|
||||
// [MODIFIED] - 更新此方法以填充新的时间选择器
|
||||
_populateForm(data) {
|
||||
this.elements.logLevelSelect.value = data.log_level || "INFO";
|
||||
const cleanup = data.auto_cleanup || {};
|
||||
const isCleanupEnabled = cleanup.enabled || false;
|
||||
this.elements.cleanupEnableToggle.checked = isCleanupEnabled;
|
||||
this.elements.cleanupSettingsPanel.classList.toggle("hidden", !isCleanupEnabled);
|
||||
const retentionDays = cleanup.retention_days || "";
|
||||
this.elements.cleanupRetentionInput.value = retentionDays;
|
||||
this._updateActivePresetButton(retentionDays.toString());
|
||||
this.elements.cleanupExecTimeInput.value = cleanup.exec_time || "04:05";
|
||||
}
|
||||
// [MODIFIED] - 更新此方法以收集新的时间数据
|
||||
_collectFormData() {
|
||||
const parseIntOrNull = (value) => {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed === "") return null;
|
||||
const num = parseInt(trimmed, 10);
|
||||
return isNaN(num) ? null : num;
|
||||
};
|
||||
const isCleanupEnabled = this.elements.cleanupEnableToggle.checked;
|
||||
const formData = {
|
||||
log_level: this.elements.logLevelSelect.value,
|
||||
auto_cleanup: {
|
||||
enabled: isCleanupEnabled,
|
||||
interval: isCleanupEnabled ? "daily" : null,
|
||||
retention_days: isCleanupEnabled ? parseIntOrNull(this.elements.cleanupRetentionInput.value) : null,
|
||||
exec_time: isCleanupEnabled ? this.elements.cleanupExecTimeInput.value : "04:05"
|
||||
// [NEW] 收集时间数据
|
||||
}
|
||||
};
|
||||
return formData;
|
||||
}
|
||||
};
|
||||
|
||||
// frontend/js/pages/logs/index.js
|
||||
var dataStore = {
|
||||
groups: /* @__PURE__ */ new Map(),
|
||||
keys: /* @__PURE__ */ new Map()
|
||||
@@ -2133,7 +2260,8 @@ var LogsPage = class {
|
||||
errorFilters: document.getElementById("error-logs-filters"),
|
||||
systemControls: document.getElementById("system-logs-controls"),
|
||||
errorTemplate: document.getElementById("error-logs-template"),
|
||||
systemTemplate: document.getElementById("system-logs-template")
|
||||
systemTemplate: document.getElementById("system-logs-template"),
|
||||
settingsBtn: document.querySelector('button[aria-label="\u65E5\u5FD7\u8BBE\u7F6E"]')
|
||||
};
|
||||
this.initialized = !!this.elements.contentContainer;
|
||||
if (this.initialized) {
|
||||
@@ -2142,15 +2270,79 @@ var LogsPage = class {
|
||||
this.debouncedLoadAndRender = debounce(() => this.loadAndRenderLogs(), 300);
|
||||
this.fp = null;
|
||||
this.themeObserver = null;
|
||||
this.settingsModal = null;
|
||||
this.currentSettings = {};
|
||||
}
|
||||
}
|
||||
async init() {
|
||||
if (!this.initialized) return;
|
||||
this._initPermanentEventListeners();
|
||||
await this.loadCurrentSettings();
|
||||
this._initSettingsModal();
|
||||
await this.loadGroupsOnce();
|
||||
this.state.currentView = null;
|
||||
this.switchToView("error");
|
||||
}
|
||||
_initSettingsModal() {
|
||||
if (!this.elements.settingsBtn) return;
|
||||
this.settingsModal = new LogSettingsModal({
|
||||
onSave: this.handleSaveSettings.bind(this)
|
||||
});
|
||||
this.elements.settingsBtn.addEventListener("click", () => {
|
||||
const settingsForModal = {
|
||||
log_level: this.currentSettings.log_level,
|
||||
auto_cleanup: {
|
||||
enabled: this.currentSettings.log_auto_cleanup_enabled,
|
||||
retention_days: this.currentSettings.log_auto_cleanup_retention_days,
|
||||
exec_time: this.currentSettings.log_auto_cleanup_time,
|
||||
interval: "daily"
|
||||
}
|
||||
};
|
||||
this.settingsModal.open(settingsForModal);
|
||||
});
|
||||
}
|
||||
async loadCurrentSettings() {
|
||||
try {
|
||||
const { success, data } = await apiFetchJson("/admin/settings");
|
||||
if (success) {
|
||||
this.currentSettings = data;
|
||||
} else {
|
||||
console.error("Failed to load settings from server.");
|
||||
this.currentSettings = { log_auto_cleanup_time: "04:05" };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load log settings:", error);
|
||||
this.currentSettings = { log_auto_cleanup_time: "04:05" };
|
||||
}
|
||||
}
|
||||
async handleSaveSettings(settingsData) {
|
||||
const partialPayload = {
|
||||
"log_level": settingsData.log_level,
|
||||
"log_auto_cleanup_enabled": settingsData.auto_cleanup.enabled,
|
||||
"log_auto_cleanup_time": settingsData.auto_cleanup.exec_time
|
||||
};
|
||||
if (settingsData.auto_cleanup.enabled) {
|
||||
let retentionDays = settingsData.auto_cleanup.retention_days;
|
||||
if (retentionDays === null || retentionDays <= 0) {
|
||||
retentionDays = 30;
|
||||
}
|
||||
partialPayload.log_auto_cleanup_retention_days = retentionDays;
|
||||
}
|
||||
console.log("Sending PARTIAL settings update to /admin/settings:", partialPayload);
|
||||
try {
|
||||
const { success, message } = await apiFetchJson("/admin/settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(partialPayload)
|
||||
});
|
||||
if (!success) {
|
||||
throw new Error(message || "Failed to save settings");
|
||||
}
|
||||
Object.assign(this.currentSettings, partialPayload);
|
||||
} catch (error) {
|
||||
console.error("Error saving log settings:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
_initPermanentEventListeners() {
|
||||
this.elements.tabsContainer.addEventListener("click", (event) => {
|
||||
const tabItem = event.target.closest("[data-tab-target]");
|
||||
@@ -2574,7 +2766,7 @@ var LogsPage = class {
|
||||
}
|
||||
async enrichLogsWithKeyNames(logs) {
|
||||
const missingKeyIds = [...new Set(
|
||||
logs.filter((log) => log.KeyID && !dataStore.keys.has(log.KeyID)).map((log) => log.ID)
|
||||
logs.filter((log) => log.KeyID && !dataStore.keys.has(log.KeyID)).map((log) => log.KeyID)
|
||||
)];
|
||||
if (missingKeyIds.length === 0) return;
|
||||
try {
|
||||
@@ -1,14 +1,14 @@
|
||||
import {
|
||||
CustomSelect,
|
||||
modalManager,
|
||||
taskCenterManager,
|
||||
toastManager,
|
||||
uiPatterns
|
||||
} from "./chunk-EZAP7GR4.js";
|
||||
toastManager
|
||||
} from "./chunk-U67KAGZP.js";
|
||||
import {
|
||||
apiFetch,
|
||||
apiFetchJson
|
||||
} from "./chunk-PLQL6WIO.js";
|
||||
apiFetchJson,
|
||||
modalManager,
|
||||
uiPatterns
|
||||
} from "./chunk-SHK62ZJN.js";
|
||||
import "./chunk-JSBRDJBE.js";
|
||||
|
||||
// frontend/js/components/slidingTabs.js
|
||||
@@ -181,8 +181,8 @@ var pageModules = {
|
||||
// 键 'dashboard' 对应一个函数,该函数调用 import() 返回一个 Promise
|
||||
// esbuild 看到这个 import() 语法,就会自动将 dashboard.js 及其依赖打包成一个独立的 chunk 文件
|
||||
"dashboard": () => import("./dashboard-XFUWX3IN.js"),
|
||||
"keys": () => import("./keys-2IUHJHHE.js"),
|
||||
"logs": () => import("./logs-YPEOUZVC.js")
|
||||
"keys": () => import("./keys-KZSAIVIM.js"),
|
||||
"logs": () => import("./logs-WRHCX7IP.js")
|
||||
// 'settings': () => import('./pages/settings.js'), // 未来启用 settings 页面
|
||||
// 未来新增的页面,只需在这里添加一行映射,esbuild会自动处理
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user