// frontend/js/components/customSelect.js var CustomSelect = class _CustomSelect { constructor(container) { this.container = container; this.trigger = this.container.querySelector(".custom-select-trigger"); this.panel = this.container.querySelector(".custom-select-panel"); if (!this.trigger || !this.panel) { console.warn("CustomSelect cannot initialize: missing .custom-select-trigger or .custom-select-panel.", this.container); return; } this.nativeSelect = this.container.querySelector("select"); this.triggerText = this.trigger.querySelector("span"); this.template = this.panel.querySelector(".custom-select-option-template"); if (typeof _CustomSelect.openInstance === "undefined") { _CustomSelect.openInstance = null; _CustomSelect.initGlobalListener(); } if (this.nativeSelect) { this.generateOptions(); this.updateTriggerText(); } this.bindEvents(); } static initGlobalListener() { document.addEventListener("click", (event) => { if (_CustomSelect.openInstance && !_CustomSelect.openInstance.container.contains(event.target)) { _CustomSelect.openInstance.close(); } }); } generateOptions() { this.panel.querySelectorAll(":scope > *:not(.custom-select-option-template)").forEach((child) => child.remove()); Array.from(this.nativeSelect.options).forEach((option) => { let item; if (this.template) { item = this.template.cloneNode(true); item.classList.remove("custom-select-option-template"); item.removeAttribute("hidden"); } else { item = document.createElement("a"); item.href = "#"; item.className = "block px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-600"; } item.classList.add("custom-select-option"); item.textContent = option.textContent; item.dataset.value = option.value; if (option.selected) { item.classList.add("is-selected"); } this.panel.appendChild(item); }); } bindEvents() { this.trigger.addEventListener("click", (event) => { if (this.trigger.classList.contains("is-disabled")) { return; } event.stopPropagation(); if (_CustomSelect.openInstance && _CustomSelect.openInstance !== this) { _CustomSelect.openInstance.close(); } this.toggle(); }); if (this.nativeSelect) { this.panel.addEventListener("click", (event) => { event.preventDefault(); const option = event.target.closest(".custom-select-option"); if (option) { this.selectOption(option); } }); } } selectOption(optionEl) { const selectedValue = optionEl.dataset.value; if (this.nativeSelect.value !== selectedValue) { this.nativeSelect.value = selectedValue; this.nativeSelect.dispatchEvent(new Event("change", { bubbles: true })); } this.updateTriggerText(); this.panel.querySelectorAll(".custom-select-option").forEach((el) => el.classList.remove("is-selected")); optionEl.classList.add("is-selected"); this.close(); } updateTriggerText() { if (!this.nativeSelect || !this.triggerText) return; const selectedOption = this.nativeSelect.options[this.nativeSelect.selectedIndex]; if (selectedOption) { this.triggerText.textContent = selectedOption.textContent; } } toggle() { this.panel.classList.toggle("hidden"); if (this.panel.classList.contains("hidden")) { if (_CustomSelect.openInstance === this) { _CustomSelect.openInstance = null; } } else { _CustomSelect.openInstance = this; } } open() { this.panel.classList.remove("hidden"); _CustomSelect.openInstance = this; } close() { this.panel.classList.add("hidden"); if (_CustomSelect.openInstance === this) { _CustomSelect.openInstance = null; } } }; // 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 = ''; iconElement.className = "text-6xl mb-3 text-success-500"; } else { iconElement.innerHTML = ''; 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() { this.tasks = []; this.activePolls = /* @__PURE__ */ new Map(); this.heartbeatInterval = null; this.MINIMUM_TASK_DISPLAY_TIME_MS = 800; this.hasUnreadCompletedTasks = false; this.isAnimating = false; this.countdownTimer = null; this.trigger = document.getElementById("task-hub-trigger"); this.panel = document.getElementById("task-hub-panel"); this.countdownBar = document.getElementById("task-hub-countdown-bar"); this.countdownRing = document.getElementById("task-hub-countdown-ring"); this.indicator = document.getElementById("task-hub-indicator"); this.clearBtn = document.getElementById("task-hub-clear-btn"); this.taskListContainer = document.getElementById("task-list-container"); this.emptyState = document.getElementById("task-list-empty"); } // [THE FINAL, DEFINITIVE VERSION] init() { if (!this.trigger || !this.panel) { console.warn("Task Center UI core elements not found. Initialization skipped."); return; } this.trigger.addEventListener("click", (event) => { event.stopPropagation(); if (this.isAnimating) return; if (this.panel.classList.contains("hidden")) { this._handleUserInteraction(); this.openPanel(); } else { this.closePanel(); } }); document.addEventListener("click", (event) => { if (!this.panel.classList.contains("hidden") && !this.isAnimating && !this.panel.contains(event.target) && !this.trigger.contains(event.target)) { this.closePanel(); } }); this.trigger.addEventListener("mouseenter", this._stopCountdown.bind(this)); this.panel.addEventListener("mouseenter", this._handleUserInteraction.bind(this)); const handleMouseLeave = () => { if (!this.panel.classList.contains("hidden")) { this._startCountdown(); } }; this.panel.addEventListener("mouseleave", handleMouseLeave); this.trigger.addEventListener("mouseleave", handleMouseLeave); this.clearBtn?.addEventListener("click", this.clearCompletedTasks.bind(this)); this.taskListContainer.addEventListener("click", (event) => { const toggleHeader = event.target.closest("[data-task-toggle]"); if (!toggleHeader) return; this._handleUserInteraction(); const taskItem = toggleHeader.closest(".task-list-item"); const content = taskItem.querySelector("[data-task-content]"); if (!content) return; const isCollapsed = content.classList.contains("collapsed"); toggleHeader.classList.toggle("expanded", isCollapsed); if (isCollapsed) { content.classList.remove("collapsed"); content.style.maxHeight = `${content.scrollHeight}px`; content.style.opacity = "1"; content.addEventListener("transitionend", () => { if (!content.classList.contains("collapsed")) content.style.maxHeight = "none"; }, { once: true }); } else { content.style.maxHeight = `${content.scrollHeight}px`; requestAnimationFrame(() => { content.style.maxHeight = "0px"; content.style.opacity = "0"; content.classList.add("collapsed"); }); } }); this._render(); this._startHeartbeat(); console.log("Task Center UI Initialized [Multi-Task Heartbeat Polling Architecture - IGNITED]."); } async startTask(taskDefinition) { try { const initialTaskData = await taskDefinition.start(); if (!initialTaskData || !initialTaskData.id) throw new Error("Task definition did not return a valid initial task object."); const newTask = { id: initialTaskData.id, definition: taskDefinition, data: initialTaskData, timestamp: /* @__PURE__ */ new Date(), startTime: Date.now() }; if (!initialTaskData.is_running) { console.log(`[TaskCenter] Task ${newTask.id} completed synchronously. Skipping poll.`); taskDefinition.renderToastNarrative(newTask.data, {}, toastManager); this.tasks.unshift(newTask); this._render(); this._handleTaskCompletion(newTask); return; } this.tasks.unshift(newTask); this.activePolls.set(newTask.id, newTask); this._render(); this.openPanel(); taskDefinition.renderToastNarrative(newTask.data, {}, toastManager); this._updateIndicatorState(); } catch (error) { console.error("Failed to start task:", error); toastManager.show(`\u4EFB\u52A1\u542F\u52A8\u5931\u8D25: ${error.message}`, "error"); } } _startHeartbeat() { if (this.heartbeatInterval) return; this.heartbeatInterval = setInterval(this._tick.bind(this), 1500); } _stopHeartbeat() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; } } async _tick() { if (this.activePolls.size === 0) { return; } for (const taskId of [...this.activePolls.keys()]) { const task = this.activePolls.get(taskId); if (!task) continue; try { const response = await task.definition.poll(taskId); if (!response.success || !response.data) throw new Error(response.message || "Polling failed"); const oldData = { ...task.data }; task.data = response.data; this._updateTaskItemInHistory(task.id, task.data); task.definition.renderToastNarrative(task.data, oldData, toastManager); if (!task.data.is_running) { this._handleTaskCompletion(task); } } catch (error) { console.error(`Polling for task ${taskId} failed:`, error); task.data.error = error.message; this._updateTaskItemInHistory(task.id, task.data); this._handleTaskCompletion(task); } } } _handleTaskCompletion(task) { this.activePolls.delete(task.id); this._updateIndicatorState(); const toastId = `task-${task.id}`; const finalize = async () => { await toastManager.dismiss(toastId, !task.data.error); this._updateTaskItemInDom(task); this.hasUnreadCompletedTasks = true; this._updateIndicatorState(); if (task.data.error) { if (task.definition.onError) task.definition.onError(task.data); } else { if (task.definition.onSuccess) task.definition.onSuccess(task.data); } }; const elapsedTime = Date.now() - task.startTime; const remainingTime = this.MINIMUM_TASK_DISPLAY_TIME_MS - elapsedTime; if (remainingTime > 0) { setTimeout(finalize, remainingTime); } else { finalize(); } } // [REFACTORED for robustness] _updateIndicatorState() { const hasRunningTasks = this.activePolls.size > 0; const shouldBeVisible = hasRunningTasks || this.hasUnreadCompletedTasks; this.indicator.classList.toggle("hidden", !shouldBeVisible); } // [REFACTORED for robustness] clearCompletedTasks() { this.tasks = this.tasks.filter((task) => this.activePolls.has(task.id)); this.hasUnreadCompletedTasks = false; this._render(); } // [NEW SAFETY METHOD] _updateTaskItemInHistory(taskId, newData) { const taskInHistory = this.tasks.find((t) => t.id === taskId); if (taskInHistory) { taskInHistory.data = newData; } } // --- 渲染与DOM操作 --- _render() { this.taskListContainer.innerHTML = this.tasks.map((task) => this._createTaskItemHtml(task)).join(""); const hasTasks = this.tasks.length > 0; this.taskListContainer.classList.toggle("hidden", !hasTasks); this.emptyState.classList.toggle("hidden", hasTasks); this._updateIndicatorState(); } _createTaskItemHtml(task) { const innerHtml = task.definition.renderTaskCenterItem(task.data, task.timestamp, this._formatTimeAgo); return `
${innerHtml}
`; } _updateTaskItemInDom(task) { const item = this.taskListContainer.querySelector(`[data-task-id="${task.id}"]`); if (item) { item.innerHTML = task.definition.renderTaskCenterItem(task.data, task.timestamp, this._formatTimeAgo); } } // --- 核心面板开关逻辑 --- openPanel() { if (this.isAnimating || !this.panel.classList.contains("hidden")) return; this.isAnimating = true; this.panel.classList.remove("hidden"); this.panel.classList.add("animate-panel-in"); setTimeout(() => { this.panel.classList.remove("animate-panel-in"); this.isAnimating = false; this._startCountdown(); }, 150); } closePanel() { if (this.isAnimating || this.panel.classList.contains("hidden")) return; this._stopCountdown(); this.isAnimating = true; this.panel.classList.add("animate-panel-out"); setTimeout(() => { this.panel.classList.remove("animate-panel-out"); this.panel.classList.add("hidden"); this.isAnimating = false; }, 50); } // --- [新增] 倒计时管理方法 --- /** * 启动或重启倒计时和进度条动画 * @private */ _startCountdown() { this._stopCountdown(); this.countdownBar.classList.add("w-full", "duration-[4950ms]"); this.countdownRing.style.transition = "none"; this.countdownRing.style.strokeDashoffset = "72.26"; void this.countdownRing.offsetHeight; this.countdownRing.style.transition = "stroke-dashoffset 4.95s linear"; this.countdownRing.style.strokeDashoffset = "0"; this.countdownTimer = setTimeout(() => { this.closePanel(); }, 4950); } /** * 停止倒计时并重置进度条 * @private */ _stopCountdown() { if (this.countdownTimer) { clearTimeout(this.countdownTimer); this.countdownTimer = null; } this.countdownBar.classList.remove("w-full"); this.countdownRing.style.transition = "none"; this.countdownRing.style.strokeDashoffset = "72.26"; } // [NEW] A central handler for any action that confirms the user has seen the panel. _handleUserInteraction() { this._stopCountdown(); if (this.hasUnreadCompletedTasks) { this.hasUnreadCompletedTasks = false; this._updateIndicatorState(); } } _formatTimeAgo(date) { if (!date) return ""; const seconds = Math.floor((/* @__PURE__ */ new Date() - new Date(date)) / 1e3); if (seconds < 2) return "\u521A\u521A"; if (seconds < 60) return `${seconds}\u79D2\u524D`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}\u5206\u949F\u524D`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}\u5C0F\u65F6\u524D`; const days = Math.floor(hours / 24); return `${days}\u5929\u524D`; } }; var ToastManager = class { constructor() { this.container = document.getElementById("toast-container"); if (!this.container) { this.container = document.createElement("div"); this.container.id = "toast-container"; this.container.className = "fixed bottom-4 right-4 z-[100] w-full max-w-sm space-y-3"; document.body.appendChild(this.container); } this.activeToasts = /* @__PURE__ */ new Map(); } /** * 显示一个 Toast 通知 * @param {string} message - The message to display. * @param {string} [type='info'] - 'info', 'success', or 'error'. * @param {number} [duration=4000] - Duration in milliseconds. */ show(message, type = "info", duration = 4e3) { const toastElement = this._createToastHtml(message, type); this.container.appendChild(toastElement); requestAnimationFrame(() => { toastElement.classList.remove("opacity-0", "translate-y-2"); toastElement.classList.add("opacity-100", "translate-y-0"); }); setTimeout(() => { toastElement.classList.remove("opacity-100", "translate-y-0"); toastElement.classList.add("opacity-0", "translate-y-2"); toastElement.addEventListener("transitionend", () => toastElement.remove(), { once: true }); }, duration); } // [NEW] 创建或更新一个带进度条的Toast showProgressToast(toastId, title, message, progress) { if (this.activeToasts.has(toastId)) { const toastElement = this.activeToasts.get(toastId); const messageEl = toastElement.querySelector(".toast-message"); const progressBar = toastElement.querySelector(".toast-progress-bar"); messageEl.textContent = `${message} - ${Math.round(progress)}%`; anime({ targets: progressBar, width: `${progress}%`, duration: 400, easing: "easeOutQuad" }); } else { const toastElement = this._createProgressToastHtml(toastId, title, message, progress); this.container.appendChild(toastElement); this.activeToasts.set(toastId, toastElement); requestAnimationFrame(() => { toastElement.classList.remove("opacity-0", "translate-x-full"); toastElement.classList.add("opacity-100", "translate-x-0"); }); } } // [NEW] 移除一个进度Toast dismiss(toastId, success = null) { return new Promise((resolve) => { if (!this.activeToasts.has(toastId)) { resolve(); return; } const toastElement = this.activeToasts.get(toastId); const performFadeOut = () => { toastElement.classList.remove("opacity-100", "translate-x-0"); toastElement.classList.add("opacity-0", "translate-x-full"); toastElement.addEventListener("transitionend", () => { toastElement.remove(); this.activeToasts.delete(toastId); resolve(); }, { once: true }); }; if (success === null) { performFadeOut(); } else { const iconContainer = toastElement.querySelector(".toast-icon"); const messageEl = toastElement.querySelector(".toast-message"); if (success) { const progressBar = toastElement.querySelector(".toast-progress-bar"); messageEl.textContent = "\u5DF2\u5B8C\u6210"; anime({ targets: progressBar, width: "100%", duration: 300, easing: "easeOutQuad", complete: () => { iconContainer.innerHTML = ``; iconContainer.className = `toast-icon bg-green-500`; setTimeout(performFadeOut, 900); } }); } else { iconContainer.innerHTML = ``; iconContainer.className = `toast-icon bg-red-500`; messageEl.textContent = "\u5931\u8D25"; setTimeout(performFadeOut, 1200); } } }); } _createToastHtml(message, type) { const icons = { info: { class: "bg-blue-500", icon: "fa-info-circle" }, success: { class: "bg-green-500", icon: "fa-check-circle" }, error: { class: "bg-red-500", icon: "fa-exclamation-triangle" } }; const typeInfo = icons[type] || icons.info; const toast = document.createElement("div"); toast.className = "toast-item opacity-0 translate-y-2 transition-all duration-300 ease-out"; toast.innerHTML = `

${this._capitalizeFirstLetter(type)}

${message}

`; return toast; } // [NEW] 创建带进度条Toast的HTML结构 _createProgressToastHtml(toastId, title, message, progress) { const toast = document.createElement("div"); toast.className = "toast-item opacity-0 translate-x-full transition-all duration-300 ease-out"; toast.dataset.toastId = toastId; toast.innerHTML = `

${title}

${message} - ${Math.round(progress)}%

`; return toast; } _capitalizeFirstLetter(string) { return string.charAt(0).toUpperCase() + string.slice(1); } }; var taskCenterManager = new TaskCenterManager(); var toastManager = new ToastManager(); export { CustomSelect, modalManager, uiPatterns, taskCenterManager, toastManager };