Files
gemini-banlancer/web/static/js/chunk-U67KAGZP.js
2025-11-26 20:36:25 +08:00

541 lines
20 KiB
JavaScript

// 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/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 `<div class="task-list-item" data-task-id="${task.id}">${innerHtml}</div>`;
}
_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 = `<i class="fas fa-check-circle text-white"></i>`;
iconContainer.className = `toast-icon bg-green-500`;
setTimeout(performFadeOut, 900);
}
});
} else {
iconContainer.innerHTML = `<i class="fas fa-times-circle text-white"></i>`;
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 = `
<div class="toast-icon ${typeInfo.class}">
<i class="fas ${typeInfo.icon}"></i>
</div>
<div class="toast-content">
<p class="toast-title">${this._capitalizeFirstLetter(type)}</p>
<p class="toast-message">${message}</p>
</div>
`;
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 = `
<div class="toast-icon bg-blue-500">
<i class="fas fa-spinner animate-spin"></i>
</div>
<div class="toast-content">
<p class="toast-title">${title}</p>
<p class="toast-message">${message} - ${Math.round(progress)}%</p>
<div class="w-full bg-slate-200 dark:bg-zinc-700 rounded-full h-1 mt-1.5 overflow-hidden">
<div class="toast-progress-bar bg-blue-500 h-1 rounded-full" style="width: ${progress}%;"></div>
</div>
</div>
`;
return toast;
}
_capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
};
var taskCenterManager = new TaskCenterManager();
var toastManager = new ToastManager();
export {
CustomSelect,
taskCenterManager,
toastManager
};