diff --git a/resources/js/admin/admin-ui.js b/resources/js/admin/admin-ui.js new file mode 100644 index 0000000..879b47e --- /dev/null +++ b/resources/js/admin/admin-ui.js @@ -0,0 +1,211 @@ +// 后台全局 UI 能力,提供 Toast 状态工厂和 adminDialog 弹窗入口。 + +/** + * 创建后台 Toast 状态工厂。 + * + * @param {Array} initialToasts + * @returns {object} + */ +function createAdminToastStore(initialToasts = []) { + return { + toasts: [], + visibleIds: [], + nextToastId: Date.now(), + + /** + * 初始化 Toast 列表,并挂载全局调用方法。 + * + * @returns {void} + */ + boot() { + initialToasts.forEach((toast) => this.push(toast)); + + window.adminToast = { + show: (message, type = "success", title = null, duration = 4200) => this.push({ + message, + type, + title, + duration, + }), + success: (message, title = "操作成功", duration = 4200) => this.push({ + message, + type: "success", + title, + duration, + }), + error: (message, title = "操作失败", duration = 4200) => this.push({ + message, + type: "error", + title, + duration, + }), + }; + }, + + /** + * 追加一条 Toast,并在指定时长后自动关闭。 + * + * @param {object} toast + * @returns {void} + */ + push(toast) { + if (!toast?.message) { + return; + } + + const normalizedToast = { + id: this.nextToastId++, + type: toast.type === "error" ? "error" : "success", + title: toast.title, + message: toast.message, + duration: Number(toast.duration) > 0 ? Number(toast.duration) : 4200, + }; + + this.toasts.push(normalizedToast); + this.visibleIds.push(normalizedToast.id); + + window.setTimeout(() => { + this.remove(normalizedToast.id); + }, normalizedToast.duration); + }, + + /** + * 关闭指定 Toast,并在退场动画后清理节点。 + * + * @param {number} toastId + * @returns {void} + */ + remove(toastId) { + this.visibleIds = this.visibleIds.filter((visibleId) => visibleId !== toastId); + + window.setTimeout(() => { + this.toasts = this.toasts.filter((toast) => toast.id !== toastId); + }, 220); + }, + }; +} + +/** + * 创建后台全局弹窗组件。 + * + * @returns {{alert: Function, confirm: Function, close: Function}|null} + */ +function createAdminDialog() { + const overlay = document.getElementById("admin-dialog-overlay"); + const elIcon = document.getElementById("admin-dialog-icon"); + const elTitle = document.getElementById("admin-dialog-title"); + const elMsg = document.getElementById("admin-dialog-msg"); + const elBtns = document.getElementById("admin-dialog-btns"); + + if (!overlay || !elIcon || !elTitle || !elMsg || !elBtns) { + return null; + } + + /** + * 关闭当前弹窗。 + * + * @returns {void} + */ + function close() { + overlay.style.display = "none"; + } + + overlay.addEventListener("click", (event) => { + if (event.target === overlay) { + close(); + } + }); + + /** + * 创建弹窗按钮。 + * + * @param {string} label + * @param {string} color + * @param {Function|null} onClick + * @returns {HTMLButtonElement} + */ + function makeButton(label, color, onClick) { + const button = document.createElement("button"); + button.textContent = label; + button.style.cssText = `padding:9px 24px; border-radius:8px; border:none; cursor:pointer; + font-size:14px; font-weight:700; color:#fff; background:${color}; + transition:opacity .15s; box-shadow:0 3px 10px rgba(0,0,0,.12);`; + button.addEventListener("mouseover", () => { + button.style.opacity = ".82"; + }); + button.addEventListener("mouseout", () => { + button.style.opacity = "1"; + }); + button.addEventListener("click", () => { + close(); + if (onClick) { + onClick(); + } + }); + + return button; + } + + /** + * 弹出提示框。 + * + * @param {string} message + * @param {string} title + * @param {string} icon + * @param {Function|null} onOk + * @returns {void} + */ + function alert(message, title = "提示", icon = "ℹ️", onOk = null) { + elIcon.textContent = icon; + elTitle.textContent = title; + elMsg.innerHTML = message; + elBtns.innerHTML = ""; + elBtns.appendChild(makeButton("确定", "#4f46e5", onOk)); + overlay.style.display = "flex"; + } + + /** + * 弹出确认框。 + * + * @param {string} message + * @param {string} title + * @param {Function|null} onConfirm + * @param {string} icon + * @returns {void} + */ + function confirm(message, title = "确认操作", onConfirm = null, icon = "⚠️") { + elIcon.textContent = icon; + elTitle.textContent = title; + elMsg.innerHTML = message; + elBtns.innerHTML = ""; + elBtns.appendChild(makeButton("确定", "#4f46e5", onConfirm)); + elBtns.appendChild(makeButton("取消", "#94a3b8", null)); + overlay.style.display = "flex"; + } + + return { + alert, + confirm, + close, + }; +} + +/** + * DOM 就绪后初始化后台全局弹窗。 + * + * @returns {void} + */ +function bootAdminDialog() { + const adminDialog = createAdminDialog(); + if (adminDialog) { + window.adminDialog = adminDialog; + } +} + +window.createAdminToastStore = createAdminToastStore; + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", bootAdminDialog, { once: true }); +} else { + bootAdminDialog(); +} diff --git a/resources/js/app.js b/resources/js/app.js index 94ac488..d5a8967 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,4 +1,5 @@ import './bootstrap'; +import './admin/admin-ui.js'; import { bindAdminAiProvidersControls } from './admin/ai-providers.js'; import { bindAdminAutoactControls } from './admin/autoact.js'; import { bindAdminFishingEventsControls } from './admin/fishing-events.js'; diff --git a/resources/views/admin/layouts/app.blade.php b/resources/views/admin/layouts/app.blade.php index 2c53ded..0f3c2b4 100644 --- a/resources/views/admin/layouts/app.blade.php +++ b/resources/views/admin/layouts/app.blade.php @@ -295,180 +295,6 @@ } } -