diff --git a/.gitignore b/.gitignore index 88dea163..5873c490 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ /tests/widget-system/* !/tests/widget-system/post-visual-contract.test.cjs !/tests/widget-system/lazyload-contract.test.cjs +!/tests/widget-system/toast-contract.test.cjs !/tests/visual/docker-compose.zibll.yml !/tests/visual/zibll-Caddyfile !/tests/visual/README.md diff --git a/assets/css/build/frontend-base.min.css.disabled b/assets/css/build/frontend-base.min.css.disabled index 0c121953..a7cabda6 100644 --- a/assets/css/build/frontend-base.min.css.disabled +++ b/assets/css/build/frontend-base.min.css.disabled @@ -3556,105 +3556,17 @@ html.dark .aksrt-card-submenu__title { /** * AKSRT Enhancements — 全局 UI 增强 * - * 包含:Toast 通知、骨架屏、悬停动效、暗色过渡、滚动感知 + * 包含:骨架屏、悬停动效、暗色过渡、滚动感知 * 所有选择器使用 aksrt- 前缀避免冲突 */ -/* ============================================================ - * 1. Toast 通知系统 - * ============================================================ */ -.aksrt-toast-container { - position: fixed; - bottom: 80px; - right: 16px; - max-width: 360px; - z-index: 10000; - display: flex; - flex-direction: column-reverse; - gap: 8px; - pointer-events: none; -} - -.aksrt-toast { - pointer-events: auto; - display: flex; - align-items: center; - gap: 10px; - padding: 12px 16px; - border-radius: var(--main-radius, 8px); - color: #fff; - font-size: 14px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); - backdrop-filter: saturate(5) blur(20px); - -webkit-backdrop-filter: saturate(5) blur(20px); - transform: translateX(120%); - opacity: 0; - animation: aksrt-toast-in 0.35s cubic-bezier(0.35, 0.71, 0.46, 1.08) forwards; - cursor: pointer; - transition: transform 0.3s, opacity 0.3s; -} - -.aksrt-toast.is-dismissing { - transform: translateX(120%); - opacity: 0; - transition: transform 0.3s ease-in, opacity 0.3s ease-in; -} - -.aksrt-toast__icon { - font-size: 18px; - flex-shrink: 0; -} - -.aksrt-toast__text { - flex: 1; - min-width: 0; - line-height: 1.4; -} - -/* Toast 类型 — 对齐 zibll 纯色 */ -.aksrt-toast--success { - background: rgba(16, 185, 129, 0.9); -} - -.aksrt-toast--info { - background: rgba(59, 130, 246, 0.9); -} - -.aksrt-toast--warning { - background: rgba(245, 158, 11, 0.9); -} - -.aksrt-toast--danger, -.aksrt-toast--error { - background: rgba(239, 68, 68, 0.9); -} - -.aksrt-toast--loading { - background: rgba(107, 114, 128, 0.9); -} - -.aksrt-toast--loading .aksrt-toast__icon { - animation: aksrt-spin 1s linear infinite; -} - -@keyframes aksrt-toast-in { - from { - transform: translateX(120%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - @keyframes aksrt-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } /* ============================================================ - * 2. 骨架屏加载动画 + * 1. 骨架屏加载动画 * ============================================================ */ .aksrt-skeleton { background: linear-gradient( @@ -3726,7 +3638,7 @@ html.dark .aksrt-card-submenu__title { } /* ============================================================ - * 3. 悬停交互动效 + * 2. 悬停交互动效 * ============================================================ */ /* 卡片放大 */ @@ -3805,7 +3717,7 @@ html.dark .aksrt-card-submenu__title { } /* ============================================================ - * 4. 暗色模式过渡 (View Transitions API) + * 3. 暗色模式过渡 (View Transitions API) * ============================================================ */ ::view-transition-old(root), ::view-transition-new(root) { @@ -3839,14 +3751,10 @@ html.dark .aksrt-card-submenu__title { --aksrt-blur-bg: var(--surface-1); } - .aksrt-toast { - backdrop-filter: none; - -webkit-backdrop-filter: none; - } } /* ============================================================ - * 5. 滚动方向感知 + * 4. 滚动方向感知 * ============================================================ */ /* 向下滚动时隐藏导航栏 */ @@ -5231,12 +5139,6 @@ html.dark .aksrt-lang-switch__dropdown { * 响应式 * ============================================================ */ @media (max-width: 640px) { - .aksrt-toast-container { - right: 8px; - left: 8px; - max-width: none; - } - .aksrt-lightbox__nav { width: 36px; height: 36px; @@ -5287,10 +5189,6 @@ html.dark .aksrt-lang-switch__dropdown { animation: none !important; } - .aksrt-toast { - animation: none !important; - } - [data-aksrt-animate] { opacity: 1 !important; transform: none !important; @@ -5449,15 +5347,6 @@ input:focus, textarea:focus, select:focus, transform-origin: top; } -/* --- Toast 通知动效 --- */ -.aksrt-toast { - animation: aksrtToastIn 0.35s var(--ease-out) forwards; -} - -.aksrt-toast--out { - animation: aksrtToastOut 0.3s var(--ease) forwards; -} - /* --- 骨架屏动效 --- */ .aksrt-skeleton { animation: aksrtSkeletonShimmer 1.5s infinite linear; diff --git a/assets/css/frontend/enhancements/core-ui.css b/assets/css/frontend/enhancements/core-ui.css index be8f7452..50acb819 100644 --- a/assets/css/frontend/enhancements/core-ui.css +++ b/assets/css/frontend/enhancements/core-ui.css @@ -1,87 +1,10 @@ /** * AKSRT Enhancements — 全局 UI 增强 * - * 包含:Toast 通知、骨架屏、悬停动效、暗色过渡、滚动感知 + * 包含:骨架屏、悬停动效、暗色过渡、滚动感知 * 所有选择器使用 aksrt- 前缀避免冲突 */ -/* ============================================================ - * 1. Toast 通知系统 - * ============================================================ */ -.aksrt-toast-container { - position: fixed; - bottom: 80px; - right: 16px; - max-width: 360px; - z-index: 10000; - display: flex; - flex-direction: column-reverse; - gap: 8px; - pointer-events: none; -} - -.aksrt-toast { - pointer-events: auto; - display: flex; - align-items: center; - gap: 10px; - padding: 12px 16px; - border-radius: var(--main-radius, 8px); - color: #fff; - font-size: 14px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); - backdrop-filter: saturate(5) blur(20px); - -webkit-backdrop-filter: saturate(5) blur(20px); - transform: translateX(120%); - opacity: 0; - animation: aksrt-toast-in 0.35s cubic-bezier(0.35, 0.71, 0.46, 1.08) forwards; - cursor: pointer; - transition: transform 0.3s, opacity 0.3s; -} - -.aksrt-toast.is-dismissing { - transform: translateX(120%); - opacity: 0; - transition: transform 0.3s ease-in, opacity 0.3s ease-in; -} - -.aksrt-toast__icon { - font-size: 18px; - flex-shrink: 0; -} - -.aksrt-toast__text { - flex: 1; - min-width: 0; - line-height: 1.4; -} - -/* Toast 类型 — 对齐 zibll 纯色 */ -.aksrt-toast--success { - background: rgba(16, 185, 129, 0.9); -} - -.aksrt-toast--info { - background: rgba(59, 130, 246, 0.9); -} - -.aksrt-toast--warning { - background: rgba(245, 158, 11, 0.9); -} - -.aksrt-toast--danger, -.aksrt-toast--error { - background: rgba(239, 68, 68, 0.9); -} - -.aksrt-toast--loading { - background: rgba(107, 114, 128, 0.9); -} - -.aksrt-toast--loading .aksrt-toast__icon { - animation: aksrt-spin 1s linear infinite; -} - .aksrt-modal-auth__error.is-success { background: rgba(16, 185, 129, 0.08); border-color: rgba(16, 185, 129, 0.22); @@ -103,24 +26,13 @@ line-height: 1.6; } -@keyframes aksrt-toast-in { - from { - transform: translateX(120%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - @keyframes aksrt-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } /* ============================================================ - * 2. 骨架屏加载动画 + * 1. 骨架屏加载动画 * ============================================================ */ .aksrt-skeleton { background: linear-gradient( @@ -192,7 +104,7 @@ } /* ============================================================ - * 3. 悬停交互动效 + * 2. 悬停交互动效 * ============================================================ */ /* 卡片放大 */ @@ -271,7 +183,7 @@ } /* ============================================================ - * 4. 暗色模式过渡 (View Transitions API) + * 3. 暗色模式过渡 (View Transitions API) * ============================================================ */ ::view-transition-old(root), ::view-transition-new(root) { @@ -307,14 +219,10 @@ body.dark-theme::view-transition-new(root) { --aksrt-blur-bg: var(--surface-1); } - .aksrt-toast { - backdrop-filter: none; - -webkit-backdrop-filter: none; - } } /* ============================================================ - * 5. 滚动方向感知 + * 4. 滚动方向感知 * ============================================================ */ /* 向下滚动时隐藏导航栏 */ @@ -348,7 +256,7 @@ body.aksrt-body-scroll .aksrt-show-on-scroll { } /* ============================================================ - * 6. 图片灯箱 + * 5. 图片灯箱 * ============================================================ */ .aksrt-lightbox { position: fixed; diff --git a/assets/css/frontend/enhancements/modal-motion/dark-mode-2.css b/assets/css/frontend/enhancements/modal-motion/dark-mode-2.css index 71d1fc7b..ceb3e48c 100644 --- a/assets/css/frontend/enhancements/modal-motion/dark-mode-2.css +++ b/assets/css/frontend/enhancements/modal-motion/dark-mode-2.css @@ -3,12 +3,6 @@ * 响应式 * ============================================================ */ @media (max-width: 640px) { - .aksrt-toast-container { - right: 8px; - left: 8px; - max-width: none; - } - .aksrt-lightbox__nav { width: 36px; height: 36px; @@ -59,10 +53,6 @@ animation: none !important; } - .aksrt-toast { - animation: none !important; - } - [data-aksrt-animate] { opacity: 1 !important; transform: none !important; @@ -230,15 +220,6 @@ input:focus, textarea:focus, select:focus, transform-origin: top; } -/* --- Toast 通知动效 --- */ -.aksrt-toast { - animation: aksrtToastIn 0.35s var(--ease-out) forwards; -} - -.aksrt-toast--out { - animation: aksrtToastOut 0.3s var(--ease) forwards; -} - /* --- 骨架屏动效 --- */ .aksrt-skeleton { animation: aksrtSkeletonShimmer 1.5s infinite linear; diff --git a/assets/css/frontend/notyf.css b/assets/css/frontend/notyf.css new file mode 100644 index 00000000..a4f15ce3 --- /dev/null +++ b/assets/css/frontend/notyf.css @@ -0,0 +1,142 @@ +/** + * Zibll-compatible frontend notifications. + * + * @package AKSRT + */ + +.notyn { + position: fixed; + bottom: 6em; + right: 0; + max-width: 320px; + z-index: 10000000; +} + +.notyf { + margin-bottom: 0.6em; + color: #fff; + float: right; + min-width: 200px; + padding: 1em 3em 1em 3.4em; + box-shadow: -3px 3px 6px rgba(0, 0, 0, 0.1); + transform: translateX(1.5em); + transition: 0.3s; + -webkit-backdrop-filter: saturate(5) blur(20px); + backdrop-filter: saturate(5) blur(20px); + -webkit-animation: notyf 0.4s cubic-bezier(0.35, 0.71, 0.46, 1.08); + animation: notyf 0.4s cubic-bezier(0.35, 0.71, 0.46, 1.08); +} + +.notyn-out .notyf { + transform: translateX(110%); + opacity: 0.5; + transition: 0.5s; +} + +@-webkit-keyframes notyf { + 0% { + transform: translateX(110%); + opacity: 0; + } + + 100% { + transform: translateX(1.5em); + opacity: 1; + } +} + +@keyframes notyf { + 0% { + transform: translateX(110%); + opacity: 0; + } + + 100% { + transform: translateX(1.5em); + opacity: 1; + } +} + +.notyf::before { + font: normal normal normal 16px/1 FontAwesome; + left: 0.8em; + font-size: 1.5em; + position: absolute; + top: 0; + bottom: 0; + margin: auto; + height: 20px; +} + +.notyf.success { + background: linear-gradient(90deg, rgba(15, 147, 249, 0.7), rgba(61, 189, 249, 0.8)); +} + +.notyf.success::before { + content: '\f058'; +} + +.notyf.info { + background: linear-gradient(90deg, rgba(58, 162, 54, 0.8), rgba(89, 247, 131, 0.8)); +} + +.notyf.info::before { + content: '\f0a1'; +} + +.notyf.warning, +.notyf.load { + background: linear-gradient(90deg, rgba(253, 170, 71, 0.7), rgba(247, 154, 13, 0.8)); +} + +.notyf.warning::before { + content: '\f05a'; +} + +.notyf.danger { + background: linear-gradient(90deg, rgba(253, 69, 28, 0.7), rgba(251, 110, 75, 0.8)); +} + +.notyf.danger::before { + content: '\f057'; +} + +.notyf .fa-close { + float: right; + padding: 0.6em; + margin: -0.4em; +} + +.notyf.load::before { + content: ''; + width: 0.7em; + height: 0.7em; + display: inline-block; + border: 0.08em solid transparent; + border-width: calc(1px + 0.03em); + border-radius: 50%; + border-top-color: var(--this-color); + border-bottom-color: var(--this-color); + -webkit-animation: huan-rotate 1s cubic-bezier(0.7, 0.1, 0.31, 0.9) infinite; + animation: huan-rotate 1s cubic-bezier(0.7, 0.1, 0.31, 0.9) infinite; +} + +@-webkit-keyframes huan-rotate { + 0% { + transform: rotate(0); + } + + 100% { + transform: rotate(360deg); + } +} + +@keyframes huan-rotate { + 0% { + transform: rotate(0); + } + + 100% { + transform: rotate(360deg); + } +} diff --git a/assets/css/frontend/print.css b/assets/css/frontend/print.css index ed44655b..85279a09 100644 --- a/assets/css/frontend/print.css +++ b/assets/css/frontend/print.css @@ -13,7 +13,6 @@ .footer-tabbar, .float-right, .float-btn, - .aksrt-toast-container, .resource-mobile-fab, .resource-mobile-download-bar, .sidebar, diff --git a/assets/css/routes/profile/layout.css b/assets/css/routes/profile/layout.css index e772e37e..c1c2a060 100644 --- a/assets/css/routes/profile/layout.css +++ b/assets/css/routes/profile/layout.css @@ -5,7 +5,6 @@ * 通过 class-aksrt-scripts.php 在 is_page_template('templates/pages/page-profile.php') 时加载。 * * 覆盖: - * - Toast 通知容器(仅 profile 页) * - 公开个人主页头 * - 仪表盘标签 + 数据表 * - 个人资料编辑表单 + 联系方式行 @@ -14,46 +13,6 @@ * - 密码强度条 */ -/* ==================== Toast(profile 页专用) ==================== */ -#toast-container { - position: fixed; - bottom: calc(24px + env(safe-area-inset-bottom, 0px)); - right: calc(24px + env(safe-area-inset-right, 0px)); - z-index: 99999; - display: flex; - flex-direction: column-reverse; - gap: 10px; - pointer-events: none; -} -.toast-item { - pointer-events: auto; - display: flex; - align-items: center; - gap: 8px; - padding: 10px 20px; - background: rgba(0, 0, 0, 0.78); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - color: #fff; - font-size: 14px; - font-weight: 500; - border-radius: 100px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); - animation: toastIn 0.35s cubic-bezier(0.22, 1, 0.36, 1) forwards; - white-space: nowrap; -} -.toast-item.fade-out { - animation: toastOut 0.4s ease forwards; -} -@keyframes toastIn { - 0% { opacity: 0; transform: translateY(16px) scale(0.92); } - 100% { opacity: 1; transform: translateY(0) scale(1); } -} -@keyframes toastOut { - 0% { opacity: 1; transform: translateY(0) scale(1); } - 100% { opacity: 0; transform: translateY(8px) scale(0.95); } -} - /* 用户主页 / 用户中心统一使用站点内容宽度,避免旧 page/dashboard 容器收窄。 * main 自身同时带 content-shell + aksrt-profile,不能写成 .aksrt-profile .content-shell。 */ .aksrt-profile.content-shell, diff --git a/assets/js/aksrt-runtime.js b/assets/js/aksrt-runtime.js index 1f2d79e6..bcfc0ad0 100644 --- a/assets/js/aksrt-runtime.js +++ b/assets/js/aksrt-runtime.js @@ -3,7 +3,6 @@ * * Unified core primitives for the entire frontend: * aksrt.tbquire(deps, cb) — lazy script/CSS loader - * aksrt.notyf(msg, opts) — unified toast (same-ID update) * aksrt.confirm(msg, opts) — i18n confirm dialog * aksrt.scrollTopTo(target, off) — smart scroll with header offset * aksrt.loading(el, show) — element-level loading overlay @@ -93,123 +92,6 @@ return p; }; - // ══════════════════════════════════════════════════════════════ - // §10.10 — notyf: unified toast with same-ID update - // ══════════════════════════════════════════════════════════════ - - var _notyfContainer = null; - var _notyfMap = {}; - var NOTYF_DURATION = 5000; - - function getNotyfContainer() { - if (_notyfContainer && _notyfContainer.parentNode) return _notyfContainer; - _notyfContainer = document.createElement('div'); - _notyfContainer.className = 'aksrt-notyf'; - _notyfContainer.setAttribute('role', 'alert'); - _notyfContainer.setAttribute('aria-live', 'polite'); - document.body.appendChild(_notyfContainer); - return _notyfContainer; - } - - function createNotyfItem(msg, opts) { - var el = document.createElement('div'); - el.className = 'aksrt-notyf__item aksrt-notyf--' + (opts.type || 'info'); - if (opts.type === 'load') el.classList.add('aksrt-notyf--loading'); - - var iconMap = { - success: '', - danger: '', - error: '', - warning: '', - load: '', - info: '' - }; - - el.innerHTML = - '' + (iconMap[opts.type] || iconMap.info) + '' + - '' + escapeHtml(msg) + '' + - ''; - - el.querySelector('.aksrt-notyf__close').addEventListener('click', function () { - removeNotyf(el, opts.id); - }); - - return el; - } - - function removeNotyf(el, id) { - el.classList.add('aksrt-notyf--out'); - setTimeout(function () { - if (el.parentNode) el.parentNode.removeChild(el); - }, 300); - if (id && _notyfMap[id]) { - clearTimeout(_notyfMap[id].timer); - delete _notyfMap[id]; - } - } - - aksrt.notyf = function (msg, opts) { - if (typeof opts === 'string') opts = { type: opts }; - opts = opts || {}; - var type = opts.type || 'info'; - var duration = type === 'load' ? 0 : (opts.duration || NOTYF_DURATION); - var id = opts.id || null; - - var container = getNotyfContainer(); - - if (id && _notyfMap[id]) { - var existing = _notyfMap[id]; - clearTimeout(existing.timer); - existing.el.className = 'aksrt-notyf__item aksrt-notyf--' + type; - existing.el.querySelector('.aksrt-notyf__text').textContent = msg; - var iconSpan = existing.el.querySelector('.aksrt-notyf__icon'); - var iconMap2 = { - success: '', - danger: '', - error: '', - warning: '', - load: '', - info: '' - }; - iconSpan.innerHTML = iconMap2[type] || iconMap2.info; - if (duration > 0) { - existing.timer = setTimeout(function () { removeNotyf(existing.el, id); }, duration); - } - return existing.el; - } - - var el = createNotyfItem(msg, { type: type, id: id }); - container.appendChild(el); - - requestAnimationFrame(function () { - el.classList.add('aksrt-notyf--in'); - }); - - var timer = null; - if (duration > 0) { - timer = setTimeout(function () { removeNotyf(el, id); }, duration); - } - - if (id) { - _notyfMap[id] = { el: el, timer: timer }; - } - - return el; - }; - - aksrt.notyf.close = function (id) { - if (_notyfMap[id]) removeNotyf(_notyfMap[id].el, id); - }; - - // Backward compat: wire window.aksrtToast to notyf - window.aksrtToast = function (msg, type) { - aksrt.notyf(msg, { type: type || 'info', duration: 2200 }); - }; - // Alias used by several modules that call aksrt.toast(...) - aksrt.toast = function (msg, type) { - return aksrt.notyf(msg, typeof type === 'string' ? { type: type, duration: 2200 } : (type || { duration: 2200 })); - }; - // ══════════════════════════════════════════════════════════════ // §10.10 — confirm: i18n confirm dialog + [data-confirm] // ══════════════════════════════════════════════════════════════ @@ -551,7 +433,7 @@ if (action === 'like' || action === 'favorite') { try { if (localStorage.getItem(storageKey) && !btn.classList.contains('is-active')) { - aksrt.notyf(__('already_done', '已操作'), 'warning'); + notyf(__('already_done', '已操作'), 'warning'); return; } } catch (ex) { /* storage unavailable */ } @@ -562,7 +444,7 @@ var ajaxUrl = (window.aksrtData && aksrtData.ajaxUrl) || '/wp-admin/admin-ajax.php'; if (!(window.aksrtData && aksrtData.nonce)) { btn.classList.remove('is-loading'); - aksrt.notyf(__('action_failed', '操作失败'), 'danger'); + notyf(__('action_failed', '操作失败'), 'danger'); return; } var params = new URLSearchParams(); @@ -602,19 +484,19 @@ } catch (ex) { /* */ } if (json.data && json.data.message) { - aksrt.notyf(json.data.message, 'success'); + notyf(json.data.message, 'success'); } if (json.data && json.data.goto) { window.location.href = json.data.goto; } } else { - aksrt.notyf((json.data && json.data.message) || __('action_failed', '操作失败'), 'danger'); + notyf((json.data && json.data.message) || __('action_failed', '操作失败'), 'danger'); } }) .catch(function (err) { btn.classList.remove('is-loading'); - aksrt.notyf(err.message || __('network_error', '网络错误'), 'danger'); + notyf(err.message || __('network_error', '网络错误'), 'danger'); }); } @@ -651,7 +533,7 @@ kind: 'generic', }); } else if (json.data && json.data.message) { - aksrt.notyf(json.data.message, 'success'); + notyf(json.data.message, 'success'); } if (json.data && json.data.goto) { window.location.href = json.data.goto; @@ -660,12 +542,12 @@ window.location.reload(); } } else { - aksrt.notyf((json.data && json.data.message) || __('action_failed', '操作失败'), 'danger'); + notyf((json.data && json.data.message) || __('action_failed', '操作失败'), 'danger'); } }) .catch(function (err) { btn.classList.remove('is-loading'); - aksrt.notyf(err.message || __('network_error', '网络错误'), 'danger'); + notyf(err.message || __('network_error', '网络错误'), 'danger'); }); } diff --git a/assets/js/author.js b/assets/js/author.js index 89db5a0c..df0fc8fc 100644 --- a/assets/js/author.js +++ b/assets/js/author.js @@ -12,23 +12,6 @@ (function () { 'use strict'; - function notify(message, type) { - if (!message) return; - if (typeof window.AksrtToast === 'function') { - window.AksrtToast(message, type || 'info'); - return; - } - if (typeof window.showToast === 'function') { - window.showToast(message, type || 'info'); - return; - } - if (typeof window.aksrtToast === 'function') { - window.aksrtToast(message, type || 'info'); - return; - } - window.alert(message); - } - function updateTabUrl(root, name, defaultTab) { if (!window.history || typeof window.history.replaceState !== 'function') return; var url = new URL(window.location.href); @@ -149,7 +132,7 @@ if (!file) return; if (upload.maxSize && file.size > upload.maxSize) { - notify(upload.tooLargeText || '图片大小不能超过 2MB', 'warning'); + notyf(upload.tooLargeText || '图片大小不能超过 2MB', 'warning'); input.value = ''; return; } @@ -180,7 +163,7 @@ .then(function (payload) { if (payload && payload.success && payload.url) { applyImage(payload.url); - notify(upload.successText || '背景图已更新', 'success'); + notyf(upload.successText || '背景图已更新', 'success'); return; } throw payload || { message: upload.errorText || '上传失败' }; @@ -190,7 +173,7 @@ ? error.data.previous_url : ''; resetImage(previousUrl); - notify((error && error.message) || upload.networkErrorText || '网络错误,请重试', 'error'); + notyf((error && error.message) || upload.networkErrorText || '网络错误,请重试', 'danger'); }) .finally(function () { setBusy(false); @@ -222,7 +205,7 @@ var file = input.files && input.files[0]; if (!file) return; if (upload.maxSize && file.size > upload.maxSize) { - notify(upload.tooLargeText || '图片大小不能超过 2MB', 'warning'); + notyf(upload.tooLargeText || '图片大小不能超过 2MB', 'warning'); input.value = ''; return; } @@ -246,10 +229,10 @@ var url = payload.url || (payload.data && payload.data.url) || ''; if (!url) throw payload; if (image) image.src = url; - notify(payload.message || upload.successText || '头像已更新', 'success'); + notyf(payload.message || upload.successText || '头像已更新', 'success'); }) .catch(function (error) { - notify((error && error.message) || upload.errorText || '上传失败', 'error'); + notyf((error && error.message) || upload.errorText || '上传失败', 'danger'); }) .finally(function () { setBusy(false); diff --git a/assets/js/ban-appeal-page.js b/assets/js/ban-appeal-page.js index bb8ed011..74c750e8 100644 --- a/assets/js/ban-appeal-page.js +++ b/assets/js/ban-appeal-page.js @@ -9,13 +9,6 @@ if (window._win && window._win.ajax_url) return window._win.ajax_url; return '/wp-admin/admin-ajax.php'; } - function toast(msg) { - if (window.aksrt && typeof window.aksrt.toast === 'function') { - window.aksrt.toast(msg); - return; - } - window.alert(msg); - } function init() { var form = document.querySelector('.aksrt-ban-appeal-form'); if (!form) return; @@ -29,13 +22,13 @@ .then(function (r) { return r.json(); }) .then(function (data) { if (!data || data.error) { - toast((data && data.msg) || '提交失败'); + notyf((data && data.msg) || '提交失败', 'danger'); return; } - toast(data.msg || '提交成功'); + notyf(data.msg || '提交成功'); if (data.reload) window.setTimeout(function () { location.reload(); }, 400); }) - .catch(function () { toast('网络错误'); }) + .catch(function () { notyf('网络错误', 'danger'); }) .finally(function () { if (btn) btn.disabled = false; }); }); } diff --git a/assets/js/comment-ajax.js b/assets/js/comment-ajax.js index 9e3d3951..46b8aeed 100644 --- a/assets/js/comment-ajax.js +++ b/assets/js/comment-ajax.js @@ -34,13 +34,6 @@ }).then(function (r) { return r.json(); }); } - function notify(message, type) { - if (typeof window.AksrtToast === 'function') { window.AksrtToast(message, type || 'info'); return; } - if (typeof window.aksrtToast === 'function') { window.aksrtToast(message, type || 'info'); return; } - if (typeof window.showToast === 'function') { window.showToast(message, type || 'info'); return; } - if (type === 'error') { window.alert(message); } - } - function getCommentId(el) { return parseInt(el.getAttribute('data-comment-id') || (el.id || '').replace('comment-', ''), 10) || 0; } @@ -77,7 +70,7 @@ restPost('/comment/like', { comment_id: commentId, active: nextActive }) .then(function (result) { - if (!result.success) { notify((result.data && result.data.message) || result.message || '操作失败', 'error'); return; } + if (!result.success) { notyf((result.data && result.data.message) || result.message || '操作失败', 'danger'); return; } var active = !!(result.data && result.data.active); var count = (result.data && result.data.count) || 0; btn.setAttribute('data-active', active ? '1' : '0'); @@ -91,7 +84,7 @@ if (count > 0) { countEl.removeAttribute('hidden'); } else { countEl.setAttribute('hidden', ''); } } }) - .catch(function () { notify('网络错误', 'error'); }) + .catch(function () { notyf('网络错误', 'danger'); }) .finally(function () { btn.dataset.busy = ''; }); } @@ -120,10 +113,10 @@ } }, 300); } else { - notify((result.data && result.data.message) || result.message || '删除失败', 'error'); + notyf((result.data && result.data.message) || result.message || '删除失败', 'danger'); } }) - .catch(function () { notify('网络错误', 'error'); }); + .catch(function () { notyf('网络错误', 'danger'); }); }; if (window.aksrt && aksrt.confirm) { @@ -138,15 +131,15 @@ restPost('/comment/pin', { comment_id: commentId, pinned: nextPinned }) .then(function (result) { - if (!result.success) { notify((result.data && result.data.message) || result.message || '操作失败', 'error'); return; } + if (!result.success) { notyf((result.data && result.data.message) || result.message || '操作失败', 'danger'); return; } var pinned = !!(result.data && result.data.pinned); btn.setAttribute('data-pinned', pinned ? '1' : '0'); btn.innerHTML = ' ' + (pinned ? '取消置顶' : '置顶'); commentEl.classList.toggle('aksrt-comment--pinned', pinned); syncPinnedBadge(commentEl, pinned); - notify(result.message || (pinned ? '评论已置顶。' : '已取消置顶。'), 'success'); + notyf(result.message || (pinned ? '评论已置顶。' : '已取消置顶。'), 'success'); }) - .catch(function () { notify('网络错误', 'error'); }); + .catch(function () { notyf('网络错误', 'danger'); }); } function syncPinnedBadge(commentEl, pinned) { @@ -175,11 +168,11 @@ var note = commentEl.querySelector('.awaiting-moderation'); if (note) note.remove(); } - notify(result.message || '操作成功。', 'success'); + notyf(result.message || '操作成功。', 'success'); } else { - notify((result.data && result.data.message) || result.message || '操作失败', 'error'); + notyf((result.data && result.data.message) || result.message || '操作失败', 'danger'); } }) - .catch(function () { notify('网络错误', 'error'); }); + .catch(function () { notyf('网络错误', 'danger'); }); } })(); diff --git a/assets/js/comment-tools.js b/assets/js/comment-tools.js index b720efbc..fedf4f02 100644 --- a/assets/js/comment-tools.js +++ b/assets/js/comment-tools.js @@ -4,12 +4,6 @@ (function () { 'use strict'; - function notify(msg) { - if (window.aksrt && typeof window.aksrt.toast === 'function') { - window.aksrt.toast(msg, 'info'); - } - } - function findTextarea(from) { var form = from && from.closest ? from.closest('form') : null; if (form) { @@ -67,7 +61,7 @@ var ta = box ? box.querySelector('textarea') : null; var code = ta ? ta.value : ''; if (!code || code.length < 2) { - notify('请输入代码'); + notyf('请输入代码', 'warning'); return; } insertAtCursor(findTextarea(codeBtn), '[code]\n' + code + '\n[/code]\n'); @@ -84,7 +78,7 @@ var input = ibox ? ibox.querySelector('.input-image-url') : null; var url = input ? (input.value || '').trim() : ''; if (!/^https?:\/\//i.test(url)) { - notify('请输入有效图片地址'); + notyf('请输入有效图片地址', 'warning'); return; } insertAtCursor(findTextarea(imgBtn), '[img=' + url + ']'); @@ -107,12 +101,12 @@ .then(function (json) { var u = (json && (json.url || json.source_url || (json.data && (json.data.url || json.data.source_url)))) || ''; if (!u) { - notify((json && json.message) || '上传失败'); + notyf((json && json.message) || '上传失败', 'danger'); return; } insertAtCursor(findTextarea(upBtn), '[img=' + u + ']'); }) - .catch(function () { notify('上传失败'); }); + .catch(function () { notyf('上传失败', 'danger'); }); }); fileInput.click(); return; diff --git a/assets/js/core/ajax-list.js b/assets/js/core/ajax-list.js index 081f257c..6f12bb31 100644 --- a/assets/js/core/ajax-list.js +++ b/assets/js/core/ajax-list.js @@ -347,9 +347,7 @@ container.querySelectorAll(".ajax-item.is-fading").forEach(function (n) { n.classList.remove("is-fading"); }); - if (typeof window.aksrtToast === "function") { - window.aksrtToast((err && err.message) || "加载失败"); - } + notyf((err && err.message) || "加载失败", "danger"); dispatch(container, "post_ajax.error", { link: link, error: err }); throw err; }); diff --git a/assets/js/core/article.js b/assets/js/core/article.js index b8dd91c7..86495df2 100644 --- a/assets/js/core/article.js +++ b/assets/js/core/article.js @@ -58,14 +58,6 @@ } } - function showToast(msg, type) { - if (window.aksrt && aksrt.notyf) { - aksrt.notyf(msg, { type: type || "info", duration: 2200 }); - } else { - console.log("[AKSRT Toast]", type, msg); - } - } - function initArticleTools() { var buttons = document.querySelectorAll("[data-article-tool]"); if (!buttons.length) return; @@ -80,9 +72,7 @@ if (tool === "pdf") { window.print(); - if (typeof showToast === "function") { - showToast("请在打印对话框中选择“另存为 PDF”", "info"); - } + notyf("请在打印对话框中选择“另存为 PDF”", "info"); } }); }); @@ -232,9 +222,9 @@ setButtonBusy(likeBtn, true); requestInteraction("aksrt_toggle_like", postId, nextState).then(function (data) { applyLikeState(!!data.active, parseInt(data.count, 10) || 0); - showToast(data.active ? "感谢你的点赞" : "已取消点赞", "success"); + notyf(data.active ? "感谢你的点赞" : "已取消点赞", "success"); }).catch(function (error) { - showToast(error && error.message ? error.message : "点赞失败", "error"); + notyf(error && error.message ? error.message : "点赞失败", "danger"); }).finally(function () { setButtonBusy(likeBtn, false); }); @@ -257,9 +247,9 @@ setButtonBusy(favBtn, true); requestInteraction("aksrt_toggle_favorite", postId, nextState).then(function (data) { applyFavoriteState(!!data.active, parseInt(data.count, 10) || 0); - showToast(data.active ? "收藏成功" : "已取消收藏", "success"); + notyf(data.active ? "收藏成功" : "已取消收藏", "success"); }).catch(function (error) { - showToast(error && error.message ? error.message : "收藏失败", "error"); + notyf(error && error.message ? error.message : "收藏失败", "danger"); }).finally(function () { setButtonBusy(favBtn, false); }); diff --git a/assets/js/core/comment-move-form.js b/assets/js/core/comment-move-form.js index 5293b613..1a3fd75b 100644 --- a/assets/js/core/comment-move-form.js +++ b/assets/js/core/comment-move-form.js @@ -22,16 +22,6 @@ return d.restNonce || d.nonce || ''; } - function notify(msg, type) { - if (window.aksrt && typeof window.aksrt.toast === 'function') { - window.aksrt.toast(msg, type || 'info'); - return; - } - if (window.notyf) { - try { window.notyf[type === 'error' ? 'error' : 'success'](msg); } catch (e) {} - } - } - function setActionText(text) { var el = document.querySelector('#' + respondId + ' .action-text'); if (!el) return; @@ -147,7 +137,7 @@ if (submit) submit.disabled = false; }) .catch(function () { - notify('加载评论失败', 'error'); + notyf('加载评论失败', 'danger'); if (textarea) { textarea.disabled = false; textarea.value = ''; diff --git a/assets/js/core/comments.js b/assets/js/core/comments.js index da9f2c20..8cd9e9de 100644 --- a/assets/js/core/comments.js +++ b/assets/js/core/comments.js @@ -4,14 +4,6 @@ (function () { "use strict"; - function showToast(msg, type) { - if (window.aksrt && aksrt.notyf) { - aksrt.notyf(msg, { type: type || "info", duration: 2200 }); - } else { - console.log("[AKSRT Toast]", type, msg); - } - } - function initCommentSystem() { var form = document.querySelector("#commentform"); if (!form) return; @@ -125,9 +117,9 @@ if (responseText.indexOf('审核') !== -1 || responseText.indexOf('moderation') !== -1 || responseText.includes('awaiting-moderation')) { - showToast('评论已提交,等待管理员审核', 'info'); + notyf('评论已提交,等待管理员审核', 'info'); } else { - showToast('评论发布成功', 'success'); + notyf('评论发布成功', 'success'); } // 清空文本框 @@ -153,7 +145,7 @@ var errorMsg = extractErrorMessage(responseText); var waitSeconds = extractWaitTime(errorMsg); - showToast(errorMsg, 'warning'); + notyf(errorMsg, 'warning'); if (waitSeconds > 0) { startCountdown(waitSeconds); @@ -167,7 +159,7 @@ // 情况3: HTTP 403 权限不足(地区/IP/关键词等) if (statusCode === 403) { var forbiddenMsg = extractErrorMessage(responseText); - showToast(forbiddenMsg, 'error'); + notyf(forbiddenMsg, 'danger'); setLoading(false); return false; } @@ -175,12 +167,12 @@ // 情况4: 包含WordPress错误信息 var errorMatch = responseText.match(/
]*>([^<]*(?:错误|失败|error|Error)[^<]*)<\/p>/i); if (errorMatch) { - showToast(errorMatch[1].trim(), 'error'); + notyf(errorMatch[1].trim(), 'danger'); setLoading(false); return false; } // 默认情况:可能是成功或其他 - showToast('评论已提交', 'success'); + notyf('评论已提交', 'success'); var textarea2 = form.querySelector('#comment'); if (textarea2) textarea2.value = ''; @@ -194,7 +186,7 @@ } catch (e) { console.error('[AKSRT Comment] 解析响应失败:', e); - showToast('响应解析异常,请重试', 'error'); + notyf('响应解析异常,请重试', 'danger'); setLoading(false); return false; } @@ -239,7 +231,7 @@ if (editId) editId.remove(); restoreRespondPosition(); setLoading(false); - showToast(json.message || '评论已更新', 'success'); + notyf(json.message || '评论已更新', 'success'); var respondEl = document.getElementById('respond'); if (respondEl && window.AksrtFixedInput) window.AksrtFixedInput(respondEl, 'hide'); return; @@ -289,7 +281,7 @@ } if (json && json.code) { - showToast(json.message || '评论提交失败', 'error'); + notyf(json.message || '评论提交失败', 'danger'); try { var failScene = window.AKSRTCaptchaState && window.AKSRTCaptchaState.scenes && @@ -303,7 +295,7 @@ return; } - showToast('评论已提交', 'success'); + notyf('评论已提交', 'success'); setLoading(false); setTimeout(function () { reloadCommentList(); }, 500); }) @@ -352,9 +344,9 @@ }); if (needsModeration) { - showToast('评论已提交,等待管理员审核', 'info'); + notyf('评论已提交,等待管理员审核', 'info'); } else { - showToast('评论发布成功', 'success'); + notyf('评论发布成功', 'success'); } if (window.AksrtFrontend) AksrtFrontend.rehydrate(newComment, 'ajax'); @@ -397,9 +389,9 @@ console.error('[AKSRT Comment] 提交失败:', error); if (error.message === 'Failed to fetch' || error.name === 'TypeError') { - showToast('网络连接失败,请检查网络后重试', 'error'); + notyf('网络连接失败,请检查网络后重试', 'danger'); } else { - showToast(error.message || '评论提交失败,请重试', 'error'); + notyf(error.message || '评论提交失败,请重试', 'danger'); } setLoading(false); @@ -439,7 +431,7 @@ performSubmit(); } else { // 验证失败 - showToast('人机验证未通过,请重试', 'warning'); + notyf('人机验证未通过,请重试', 'warning'); setLoading(false); } }); diff --git a/assets/js/enhancements.js b/assets/js/enhancements.js index e9cd4f4e..a0e82b78 100644 --- a/assets/js/enhancements.js +++ b/assets/js/enhancements.js @@ -1,7 +1,7 @@ /** * AKSRT Enhancements — 全局 UI 增强 * - * 包含:Toast 通知、图片灯箱、滚动方向感知 + * 包含:图片灯箱、滚动方向感知 * * @package AKSRT */ @@ -10,90 +10,7 @@ 'use strict'; /* ============================================================ - * 1. Toast 通知系统 - * ============================================================ */ - var toastContainer = null; - - function ensureToastContainer() { - if (!toastContainer || !document.body.contains(toastContainer)) { - toastContainer = document.createElement('div'); - toastContainer.className = 'aksrt-toast-container'; - document.body.appendChild(toastContainer); - } - return toastContainer; - } - - /** - * 显示 Toast 通知 - * - * @param {string} message 消息文本 - * @param {string} type 类型: success, info, warning, danger/error, loading - * @param {number} duration 显示时长(ms),0 表示不自动关闭 - * @returns {Object} { dismiss: Function, update: Function } - */ - function showToast(message, type, duration) { - type = type || 'success'; - if (type === 'error') type = 'danger'; - duration = duration !== undefined ? duration : 3500; - if (duration > 0 && duration < 100) duration = duration * 1000; - - var container = ensureToastContainer(); - - var icons = { - success: 'fas fa-check-circle', - info: 'fas fa-bell', - warning: 'fas fa-exclamation-circle', - danger: 'fas fa-times-circle', - loading: 'fas fa-spinner', - }; - - var el = document.createElement('div'); - el.className = 'aksrt-toast aksrt-toast--' + type; - el.innerHTML = - '' + - '' + message + ''; - - container.appendChild(el); - - var timer = null; - - function dismiss() { - if (timer) clearTimeout(timer); - el.classList.add('is-dismissing'); - setTimeout(function () { - if (el.parentNode) el.remove(); - }, 300); - } - - function update(newMessage, newType) { - if (newType) { - el.className = 'aksrt-toast aksrt-toast--' + newType; - var iconEl = el.querySelector('.aksrt-toast__icon i'); - if (iconEl) iconEl.className = icons[newType] || icons.info; - } - var textEl = el.querySelector('.aksrt-toast__text'); - if (textEl) textEl.textContent = newMessage; - - if (newType !== 'loading' && duration > 0) { - if (timer) clearTimeout(timer); - timer = setTimeout(dismiss, duration); - } - } - - el.addEventListener('click', dismiss); - - if (type !== 'loading' && duration > 0) { - timer = setTimeout(dismiss, duration); - } - - return { dismiss: dismiss, update: update }; - } - - // 暴露到全局 - window.AksrtToast = showToast; - - /* ============================================================ - * 2. 图片灯箱 — zoom / thumbnails / slideshow / fullscreen / + * 1. 图片灯箱 — zoom / thumbnails / slideshow / fullscreen / * mousewheel / origin animation / smart download * ============================================================ */ var lightboxEl = null; @@ -493,7 +410,7 @@ }; /* ============================================================ - * 3. 滚动方向感知 + * 2. 滚动方向感知 * ============================================================ */ // Scroll direction is now handled by aksrt-runtime.js body scroll bus // (body.scroll-down / body.scroll-up / body.body-scroll). @@ -521,7 +438,7 @@ } /* ============================================================ - * 4. 导航溢出折叠 + * 3. 导航溢出折叠 * 由 assets/js/core/nav.js(zibll 1:1 navFolding)负责。 * 此处不再重复实现,避免两套逻辑争抢 .aksrt-nav-more 节点 * 触发 NotFoundError: insertBefore (moreLi 已被对方 remove)。 diff --git a/assets/js/follow.js b/assets/js/follow.js index 44fd4042..eaaebb3c 100644 --- a/assets/js/follow.js +++ b/assets/js/follow.js @@ -40,11 +40,11 @@ var isFollowing = result.data.is_following; updateFollowButton(btn, isFollowing, result.data.follower_count); } else { - showToast(result.message || '操作失败', 'error'); + notyf(result.message || '操作失败', 'danger'); } }) .catch(function () { - showToast('网络错误,请重试', 'error'); + notyf('网络错误,请重试', 'danger'); }) .finally(function () { btn.disabled = false; @@ -108,18 +108,6 @@ } } - function showToast(msg, type) { - var toast = document.createElement('div'); - toast.textContent = msg; - toast.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);padding:12px 24px;border-radius:6px;color:#fff;font-size:14px;z-index:10000;transition:opacity 0.3s;'; - toast.style.background = type === 'error' ? '#e74c3c' : '#27ae60'; - document.body.appendChild(toast); - setTimeout(function () { - toast.style.opacity = '0'; - setTimeout(function () { toast.remove(); }, 300); - }, 3000); - } - if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initFollowButtons); } else { diff --git a/assets/js/header-userbox.js b/assets/js/header-userbox.js index c04d2dd1..47afc3ff 100644 --- a/assets/js/header-userbox.js +++ b/assets/js/header-userbox.js @@ -29,27 +29,6 @@ return (window.aksrtData && window.aksrtData.restNonce) || ''; } - function toast(msg, type) { - type = type || 'info'; - // Site-wide notification widget (aksrt-runtime notyf). - if (window.aksrt && typeof window.aksrt.notyf === 'function') { - window.aksrt.notyf(msg, { type: type, duration: 2200 }); - return; - } - if (typeof window.aksrtToast === 'function') { - window.aksrtToast(msg, type); - return; - } - if (typeof window.AksrtToast === 'function') { - window.AksrtToast(msg, type); - return; - } - // Last resort: non-blocking console (never native alert for UX). - if (window.console && typeof window.console.log === 'function') { - window.console.log('[aksrt]', type, msg); - } - } - function openModal(id) { var modal = document.getElementById(id); if (!modal) return; @@ -110,11 +89,11 @@ .then(function (res) { if (!res.ok) { var msg = (res.data && (res.data.message || res.data.msg)) || '签到失败'; - toast(msg, 'danger'); + notyf(msg, 'danger'); btn.disabled = false; return; } - toast((res.data && res.data.message) || '签到成功', 'success'); + notyf((res.data && res.data.message) || '签到成功', 'success'); btn.classList.add('is-done'); btn.disabled = true; btn.setAttribute('disabled', 'disabled'); @@ -127,7 +106,7 @@ '' + signedLabel + ''; }) .catch(function () { - toast('网络错误', 'danger'); + notyf('网络错误', 'danger'); btn.disabled = false; }); }); diff --git a/assets/js/modal-auth.js b/assets/js/modal-auth.js index fd210ed2..bc467a1c 100644 --- a/assets/js/modal-auth.js +++ b/assets/js/modal-auth.js @@ -382,11 +382,7 @@ updateNavUser(data.user); } - if (window.aksrt && aksrt.notyf) { - aksrt.notyf(data.message || '登录成功', 'success'); - } else if (typeof window.aksrtToast === 'function') { - window.aksrtToast(data.message || '登录成功', 'success'); - } + notyf(data.message || '登录成功', 'success'); var redirect = modal.dataset.redirect || data.redirect; if (redirect && redirect !== window.location.href) { @@ -589,7 +585,7 @@ e.preventDefault(); e.stopImmediatePropagation(); agree.closest('label, .aksrt-modal-auth__agreement').classList.add('ani', 'shake'); - if (window.aksrt && aksrt.notyf) aksrt.notyf('请先同意用户协议', 'warning'); + notyf('请先同意用户协议', 'warning'); setTimeout(function () { agree.closest('label, .aksrt-modal-auth__agreement').classList.remove('ani', 'shake'); }, 400); diff --git a/assets/js/notyf.js b/assets/js/notyf.js new file mode 100644 index 00000000..75befa7a --- /dev/null +++ b/assets/js/notyf.js @@ -0,0 +1,60 @@ +/** + * Zibll-compatible frontend notifications. + * + * @package AKSRT + */ + +(function ($, window, document) { + 'use strict'; + + function closeNotyf($element) { + $element.addClass('notyn-out'); + window.setTimeout(function () { + $element.remove(); + }, 1000); + } + + function notyf(message, type, time, id) { + var $body = $(document.body); + + if (!$('.notyn').length) { + $body.append('
'); + } + + type = type || 'success'; + if (type === 'error') { + type = 'danger'; + } + + time = time || 5000; + time = time < 100 ? time * 1000 : time; + + var idAttribute = id ? ' id="' + id + '"' : ''; + var $notification = $('