From 86614c4905a5d81c079765092672270cdb13b94e Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 18 Aug 2026 02:38:43 +0800 Subject: [PATCH] refactor(frontend): replace toast system with zibll notyf --- .gitignore | 1 + .../css/build/frontend-base.min.css.disabled | 121 +--------- assets/css/frontend/enhancements/core-ui.css | 104 +-------- .../enhancements/modal-motion/dark-mode-2.css | 19 -- assets/css/frontend/notyf.css | 142 ++++++++++++ assets/css/frontend/print.css | 1 - assets/css/routes/profile/layout.css | 41 ---- assets/js/aksrt-runtime.js | 134 +---------- assets/js/author.js | 29 +-- assets/js/ban-appeal-page.js | 13 +- assets/js/comment-ajax.js | 27 +-- assets/js/comment-tools.js | 14 +- assets/js/core/ajax-list.js | 4 +- assets/js/core/article.js | 20 +- assets/js/core/comment-move-form.js | 12 +- assets/js/core/comments.js | 38 ++- assets/js/enhancements.js | 91 +------- assets/js/follow.js | 16 +- assets/js/header-userbox.js | 27 +-- assets/js/modal-auth.js | 8 +- assets/js/notyf.js | 60 +++++ assets/js/order-center.js | 14 +- assets/js/post-share.js | 31 +-- assets/js/posts-navs-page.js | 17 +- assets/js/report.js | 18 +- assets/js/routes/user-center.js | 84 +++---- assets/js/sign-register.js | 53 ++--- assets/js/user-badges.js | 10 +- page.php | 2 - src/Core/ScriptsPart1b.php | 10 +- src/Core/ScriptsPart1c.php | 23 +- templates/pages/page-profile.php | 3 - tests/widget-system/toast-contract.test.cjs | 218 ++++++++++++++++++ 33 files changed, 603 insertions(+), 802 deletions(-) create mode 100644 assets/css/frontend/notyf.css create mode 100644 assets/js/notyf.js create mode 100644 tests/widget-system/toast-contract.test.cjs 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 = $('
' + message + '
'); + var shouldClose = !id; + + if (id && $('#' + id).length) { + $('#' + id) + .find('.notyf') + .removeClass() + .addClass('notyf ' + type) + .html(message); + $notification = $('#' + id); + shouldClose = true; + } else { + $('.notyn').append($notification); + } + + if (shouldClose) { + window.setTimeout(function () { + closeNotyf($notification); + }, time); + } + } + + $(document.body).on('click', '.noty1', function () { + closeNotyf($(this)); + }); + + window.notyf = notyf; +})(jQuery, window, document); diff --git a/assets/js/order-center.js b/assets/js/order-center.js index 343c80c3..35e70824 100644 --- a/assets/js/order-center.js +++ b/assets/js/order-center.js @@ -8,10 +8,6 @@ var instances = new WeakMap(); - function toast(message, type) { - window.AksrtToast(message, type || "info"); - } - function request(path, body, method) { var options = { method: method || "POST" }; if (body !== undefined) options.body = JSON.stringify(body || {}); @@ -243,7 +239,7 @@ renderView(state, result.data); return result.data; }).catch(function (error) { - toast(errorMessage(error, "订单状态已变更,请刷新后查看")); + notyf(errorMessage(error, "订单状态已变更,请刷新后查看"), "danger"); return null; }); } @@ -260,12 +256,12 @@ window.location.assign(data.payment_url); return; } - toast(data.message || "支付成功", "success"); + notyf(data.message || "支付成功", "success"); refreshView(state); if (activeDetail(state, number)) openDetail(state, number); }).catch(function (error) { button.disabled = false; - toast(errorMessage(error, "网络错误,请稍后重试"), "error"); + notyf(errorMessage(error, "网络错误,请稍后重试"), "danger"); }); } @@ -281,14 +277,14 @@ request(orderPath(number, "/cancel"), {}, "POST").then(function (result) { var data = result.data || {}; invalidateOrder(number, data.order); - toast("订单已关闭", "success"); + notyf("订单已关闭", "success"); refreshView(state); if (activeDetail(state, number)) openDetail(state, number); }).catch(function (error) { button.disabled = false; button.removeAttribute("data-aksrt-confirm"); button.textContent = "关闭订单"; - toast(errorMessage(error, "网络错误,请稍后重试"), "error"); + notyf(errorMessage(error, "网络错误,请稍后重试"), "danger"); }); } diff --git a/assets/js/post-share.js b/assets/js/post-share.js index 56e95fcd..e6920194 100644 --- a/assets/js/post-share.js +++ b/assets/js/post-share.js @@ -10,23 +10,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 getMetaContent(selectors) { for (var i = 0; i < selectors.length; i += 1) { var node = document.querySelector(selectors[i]); @@ -208,7 +191,7 @@ var share = window.AksrtShare; if (!share || typeof share.openWeixinShare !== 'function') { - notify('微信分享模块未加载', 'error'); + notyf('微信分享模块未加载', 'danger'); return Promise.resolve({ mode: 'error' }); } @@ -230,7 +213,7 @@ qrImage: options.qrImage || null }).then(function (result) { if (result && result.message) { - notify(result.message, result.mode === 'error' ? 'error' : (result.mode === 'bridge' ? 'success' : 'info')); + notyf(result.message, result.mode === 'error' ? 'danger' : (result.mode === 'bridge' ? 'success' : 'info')); } return result; }); @@ -245,13 +228,13 @@ if (platform === 'copy') { return copyLink(payload.url).then(function () { - notify('链接已复制', 'success'); + notyf('链接已复制', 'success'); if (options && typeof options.closeDropdown === 'function') { options.closeDropdown(); } return { mode: 'copy' }; }).catch(function () { - notify('复制失败,请手动复制链接', 'error'); + notyf('复制失败,请手动复制链接', 'danger'); return { mode: 'error' }; }); } @@ -271,7 +254,7 @@ if (options && typeof options.closeDropdown === 'function') options.closeDropdown(); return Promise.resolve({ mode: 'poster' }); } - notify('海报模块未加载', 'error'); + notyf('海报模块未加载', 'danger'); return Promise.resolve({ mode: 'error' }); } @@ -304,9 +287,9 @@ event.preventDefault(); var url = node.getAttribute('data-share-url') || window.location.href; copyLink(url).then(function () { - notify('链接已复制', 'success'); + notyf('链接已复制', 'success'); }).catch(function () { - notify('复制失败,请手动复制链接', 'error'); + notyf('复制失败,请手动复制链接', 'danger'); }); }); } diff --git a/assets/js/posts-navs-page.js b/assets/js/posts-navs-page.js index aa81a3fc..0dcbfdcc 100644 --- a/assets/js/posts-navs-page.js +++ b/assets/js/posts-navs-page.js @@ -21,15 +21,6 @@ return '/wp-admin/admin-ajax.php'; } - function toast(msg, type) { - if (window.aksrt && typeof window.aksrt.toast === 'function') { - window.aksrt.toast(msg, type || 'info'); - return; - } - // eslint-disable-next-line no-alert - window.alert(msg); - } - function initModal() { var modal = qs('#post-navs-sz'); if (!modal) return; @@ -144,7 +135,7 @@ if (id) ids.push(id); }); if (!ids.length) { - toast('请选择需要显示的分类', 'danger'); + notyf('请选择需要显示的分类', 'danger'); return; } @@ -167,16 +158,16 @@ .then(function (r) { return r.json(); }) .then(function (data) { if (!data || data.error) { - toast((data && data.msg) || '保存失败', 'danger'); + notyf((data && data.msg) || '保存失败', 'danger'); return; } - toast(data.msg || '保存成功', 'success'); + notyf(data.msg || '保存成功', 'success'); if (data.reload) { window.setTimeout(function () { window.location.reload(); }, 400); } }) .catch(function () { - toast('网络错误', 'danger'); + notyf('网络错误', 'danger'); }) .finally(function () { if (btn) btn.disabled = false; diff --git a/assets/js/report.js b/assets/js/report.js index e6275ffa..7a721a3d 100644 --- a/assets/js/report.js +++ b/assets/js/report.js @@ -117,15 +117,15 @@ .then(function (result) { overlay.remove(); if (result.success) { - showToast(result.message || '举报已提交'); + notyf(result.message || '举报已提交'); } else { - showToast(result.message || '提交失败', 'error'); + notyf(result.message || '提交失败', 'danger'); } }) .catch(function () { submitBtn.disabled = false; submitBtn.textContent = '提交举报'; - showToast('网络错误,请重试', 'error'); + notyf('网络错误,请重试', 'danger'); }); }; btnRow.appendChild(submitBtn); @@ -140,18 +140,6 @@ reasonInput.focus(); } - 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); - } - function initReportButtons() { document.addEventListener('click', function (e) { var btn = e.target.closest('[data-aksrt-report]'); diff --git a/assets/js/routes/user-center.js b/assets/js/routes/user-center.js index b2bc0138..4ec42be7 100644 --- a/assets/js/routes/user-center.js +++ b/assets/js/routes/user-center.js @@ -7,7 +7,7 @@ * @package AKSRT */ -/* global aksrtUserCenter, showToast */ +/* global aksrtUserCenter, notyf */ (function() { 'use strict'; @@ -157,7 +157,7 @@ if (name === 'revenue' && typeof window.AKSRTAffiliateRevenueInit === 'function') window.AKSRTAffiliateRevenueInit(); if (name === 'revenue' && typeof window.AKSRTWithdrawInit === 'function') window.AKSRTWithdrawInit(); }).catch(function() { - if (typeof showToast === 'function') showToast(i18n.networkError || '加载失败,请重试'); + notyf(i18n.networkError || '加载失败,请重试', 'danger'); }); } @@ -220,7 +220,7 @@ if (typeof window.AKSRTAffiliateRevenueInit === 'function') window.AKSRTAffiliateRevenueInit(); if (typeof window.AKSRTWithdrawInit === 'function') window.AKSRTWithdrawInit(); }).catch(function() { - if (typeof showToast === 'function') showToast(i18n.networkError || '加载失败,请重试'); + notyf(i18n.networkError || '加载失败,请重试', 'danger'); }); return; } @@ -246,7 +246,7 @@ if (window.history.pushState) window.history.pushState({}, '', pageUrl.toString()); if (typeof window.AKSRTRevenueCenterInit === 'function') window.AKSRTRevenueCenterInit(creatorPanel); }).catch(function() { - if (typeof showToast === 'function') showToast(i18n.networkError || '加载失败,请重试'); + notyf(i18n.networkError || '加载失败,请重试', 'danger'); }); }); } @@ -283,7 +283,7 @@ btn.disabled = false; btn.textContent = i18n.saveBtn || '保存修改'; if (res.success) { - showToast(i18n.saved || '已保存'); + notyf(i18n.saved || '已保存'); } else { msg.className = 'profile-form__submit-msg is-error'; msg.textContent = res.data.message || (i18n.saveFailed || '保存失败'); @@ -350,17 +350,17 @@ } }, 220); } - showToast(i18n.unfavorited || '已取消收藏'); + notyf(i18n.unfavorited || '已取消收藏'); } else { btn.disabled = false; btn.classList.remove('is-loading'); - showToast((res && res.data && res.data.message) || (i18n.operationFailed || '操作失败')); + notyf((res && res.data && res.data.message) || (i18n.operationFailed || '操作失败'), 'danger'); } }) .catch(function() { btn.disabled = false; btn.classList.remove('is-loading'); - showToast(i18n.networkError || '网络错误'); + notyf(i18n.networkError || '网络错误', 'danger'); }); }); } @@ -383,11 +383,11 @@ if (!response || !response.success) throw new Error('delete-failed'); var item = btn.closest('.profile-list-item'); if (item) item.remove(); - if (typeof showToast === 'function') showToast('文章已删除'); + notyf('文章已删除'); }) .catch(function() { btn.disabled = false; - if (typeof showToast === 'function') showToast(i18n.operationFailed || '操作失败'); + notyf(i18n.operationFailed || '操作失败', 'danger'); }); }); } @@ -429,14 +429,14 @@ var row = btn.closest('[data-notification-id]'); if (row) row.classList.add('is-read'); btn.remove(); - showToast(i18n.markedRead || '已标记为已读'); + notyf(i18n.markedRead || '已标记为已读'); } else { btn.disabled = false; - showToast((res && res.data && res.data.message) || (i18n.operationFailed || '操作失败')); + notyf((res && res.data && res.data.message) || (i18n.operationFailed || '操作失败'), 'danger'); } }).catch(function() { btn.disabled = false; - showToast(i18n.networkError || '网络错误'); + notyf(i18n.networkError || '网络错误', 'danger'); }); return; } @@ -460,14 +460,14 @@ if (res && res.success) { var delRow = delBtn.closest('[data-notification-id]'); if (delRow) delRow.remove(); - showToast(i18n.notificationDeleted || '已删除通知'); + notyf(i18n.notificationDeleted || '已删除通知'); } else { delBtn.disabled = false; - showToast((res && res.data && res.data.message) || (i18n.operationFailed || '操作失败')); + notyf((res && res.data && res.data.message) || (i18n.operationFailed || '操作失败'), 'danger'); } }).catch(function() { delBtn.disabled = false; - showToast(i18n.networkError || '网络错误'); + notyf(i18n.networkError || '网络错误', 'danger'); }); }); @@ -482,14 +482,14 @@ row.classList.add('is-read'); }); list.querySelectorAll('.aksrt-notification-read-btn').forEach(function(b) { b.remove(); }); - showToast(i18n.allMarkedRead || '已全部标记为已读'); + notyf(i18n.allMarkedRead || '已全部标记为已读'); } else { markAllBtn.disabled = false; - showToast((res && res.data && res.data.message) || (i18n.operationFailed || '操作失败')); + notyf((res && res.data && res.data.message) || (i18n.operationFailed || '操作失败'), 'danger'); } }).catch(function() { markAllBtn.disabled = false; - showToast(i18n.networkError || '网络错误'); + notyf(i18n.networkError || '网络错误', 'danger'); }); }); } @@ -501,7 +501,7 @@ return input.value; }); if (!checked.length) { - showToast(i18n.selectNotifications || '请先选择要标记的通知'); + notyf(i18n.selectNotifications || '请先选择要标记的通知', 'warning'); return; } markSelectedBtn.disabled = true; @@ -516,11 +516,11 @@ var b = row ? row.querySelector('.aksrt-notification-read-btn') : null; if (b) b.remove(); }); - showToast(i18n.selectedMarkedRead || '已标记所选通知为已读'); + notyf(i18n.selectedMarkedRead || '已标记所选通知为已读'); markSelectedBtn.disabled = false; }).catch(function() { markSelectedBtn.disabled = false; - showToast(i18n.networkError || '网络错误'); + notyf(i18n.networkError || '网络错误', 'danger'); }); }); } @@ -532,7 +532,7 @@ return input.value; }); if (!checked.length) { - showToast(i18n.selectNotifications || '请先选择要删除的通知'); + notyf(i18n.selectNotifications || '请先选择要删除的通知', 'warning'); return; } if (!confirm(i18n.confirmBatchDelete || '确定批量删除所选通知?')) return; @@ -551,15 +551,15 @@ var row = list.querySelector('[data-notification-id="' + id + '"]'); if (row) row.remove(); }); - showToast(i18n.batchDeleted || '已删除所选通知'); + notyf(i18n.batchDeleted || '已删除所选通知'); } else { deleteSelectedBtn.disabled = false; - showToast((res && res.data && res.data.message) || (i18n.operationFailed || '操作失败')); + notyf((res && res.data && res.data.message) || (i18n.operationFailed || '操作失败'), 'danger'); } }) .catch(function() { deleteSelectedBtn.disabled = false; - showToast(i18n.networkError || '网络错误'); + notyf(i18n.networkError || '网络错误', 'danger'); }); }); } @@ -687,7 +687,7 @@ fetch(ajaxUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: data.toString() }) .then(function(r) { return r.json(); }).then(function(res) { if (res.success) { - showToast(res.data.message || (i18n.codeSent || '验证码已发送')); + notyf(res.data.message || (i18n.codeSent || '验证码已发送')); var sec = 60; self.classList.add('is-counting'); self.textContent = sec + 's 后重发'; @@ -703,13 +703,13 @@ } }, 1000); } else { - showToast(res.data.message || (i18n.sendFailed || '发送失败')); + notyf(res.data.message || (i18n.sendFailed || '发送失败'), 'danger'); self.disabled = false; self.textContent = getSendBtnIdleText(btnId); } }) .catch(function() { - showToast(i18n.networkError || '网络错误'); + notyf(i18n.networkError || '网络错误', 'danger'); self.disabled = false; self.textContent = getSendBtnIdleText(btnId); }); @@ -730,14 +730,14 @@ var orig = btn ? btn.textContent : ''; var reqs = fm.querySelectorAll('[required]'); for (var i = 0; i < reqs.length; i++) { - if (!reqs[i].value.trim()) { reqs[i].focus(); showToast(i18n.fillRequired || '请填写必填项'); return; } + if (!reqs[i].value.trim()) { reqs[i].focus(); notyf(i18n.fillRequired || '请填写必填项', 'warning'); return; } } if (fid === 'pwd-form') { var pwdNew = document.getElementById('pwd-new'); var pwdConfirm = document.getElementById('pwd-new-confirm'); if (pwdNew && pwdConfirm && pwdNew.value !== pwdConfirm.value) { pwdConfirm.focus(); - showToast(i18n.passwordMismatch || '两次输入的新密码不一致'); + notyf(i18n.passwordMismatch || '两次输入的新密码不一致', 'warning'); return; } } @@ -750,16 +750,16 @@ .then(function(r) { if (btn) { btn.disabled = false; btn.textContent = orig; } if (r.success) { - showToast(r.data.message || ''); + notyf(r.data.message || ''); closeModal(fm.closest('.aksrt-modal')); if (r.data.redirect_to) setTimeout(function() { window.location.href = r.data.redirect_to; }, 2500); } else { - showToast(r.data.message || (i18n.operationFailed || '操作失败')); + notyf(r.data.message || (i18n.operationFailed || '操作失败'), 'danger'); } }) .catch(function() { if (btn) { btn.disabled = false; btn.textContent = orig; } - showToast(i18n.networkError || '网络错误'); + notyf(i18n.networkError || '网络错误', 'danger'); }); }); } @@ -790,7 +790,7 @@ .then(function(r) { return r.json(); }) .then(function(d) { if (d.success) { - showToast(i18n.unbindSuccess || '解除成功'); + notyf(i18n.unbindSuccess || '解除成功'); setTimeout(function() { var u = new URL(window.location); u.searchParams.set('tab', 'settings'); @@ -798,13 +798,13 @@ window.location.href = u.toString(); }, 1000); } else { - showToast(i18n.unbindFailed || '解除失败'); + notyf(i18n.unbindFailed || '解除失败', 'danger'); self.disabled = false; self.textContent = i18n.unbind || '解除'; } }) .catch(function() { - showToast(i18n.networkError || '网络错误'); + notyf(i18n.networkError || '网络错误', 'danger'); self.disabled = false; self.textContent = i18n.unbind || '解除'; }); @@ -844,11 +844,11 @@ var phone = phoneEl ? String(phoneEl.value || '').trim() : ''; var code = codeEl ? String(codeEl.value || '').trim() : ''; if (!/^1[3-9]\d{9}$/.test(phone)) { - showToast('请输入正确的手机号'); + notyf('请输入正确的手机号', 'warning'); return; } if (!code) { - showToast('请输入短信验证码'); + notyf('请输入短信验证码', 'warning'); return; } var submitBtn = form.querySelector('[type="submit"]'); @@ -892,12 +892,12 @@ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, json: j }; }); }) .then(function (res) { if (res.ok && res.json && res.json.success) { - showToast((res.json.message) || '绑定成功'); + notyf((res.json.message) || '绑定成功'); setTimeout(function () { window.location.reload(); }, 600); return; } resetCaptcha(); - showToast((res.json && (res.json.message || res.json.error)) || '绑定失败'); + notyf((res.json && (res.json.message || res.json.error)) || '绑定失败', 'danger'); if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = '确认绑定'; @@ -905,7 +905,7 @@ }) .catch(function () { resetCaptcha(); - showToast('网络错误'); + notyf('网络错误', 'danger'); if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = '确认绑定'; diff --git a/assets/js/sign-register.js b/assets/js/sign-register.js index e39e432d..46ac6b82 100644 --- a/assets/js/sign-register.js +++ b/assets/js/sign-register.js @@ -56,7 +56,7 @@ const phone = phoneInput ? phoneInput.value.trim() : '' if (!/^1[3-9]\d{9}$/.test(phone)) { - showToast('请输入正确的手机号', 'warning') + notyf('请输入正确的手机号', 'warning') if (phoneInput) phoneInput.focus() return } @@ -91,13 +91,13 @@ .then(r => r.json().then(data => ({ ok: r.ok, data }))) .then(({ ok, data }) => { if (ok && data.success) { - showToast(data.message || '验证码已发送', 'success') + notyf(data.message || '验证码已发送', 'success') const seconds = (data.data && data.data.remaining_time) || cooldown sessionStorage.setItem(COUNTDOWN_KEY, String(Date.now() + seconds * 1000)) startCountdown(btn, seconds) } else { const msg = (data.data && data.data.message) || data.message || '发送失败' - showToast(msg, 'error') + notyf(msg, 'danger') try { const captchaState = window.AKSRTCaptchaState if (captchaState && captchaState.scenes && captchaState.scenes[scene]) { @@ -115,7 +115,7 @@ } }) .catch(() => { - showToast('网络错误,请重试', 'error') + notyf('网络错误,请重试', 'danger') btn.disabled = false btn.textContent = original }) @@ -187,15 +187,15 @@ const ref = form.querySelector('[name="ref"]')?.value?.trim() if (!phone || !code || !password) { - showToast('请填写完整信息', 'warning') + notyf('请填写完整信息', 'warning') return } if (password !== confirm) { - showToast('两次密码不一致', 'warning') + notyf('两次密码不一致', 'warning') return } if (password.length < 6) { - showToast('密码至少 6 位', 'warning') + notyf('密码至少 6 位', 'warning') return } @@ -216,13 +216,13 @@ .then(r => r.json().then(data => ({ ok: r.ok, data }))) .then(({ ok, data }) => { if (ok && data.success) { - showToast(data.message || '注册成功', 'success') - if (data.notice) showToast(data.notice, 'warning') + notyf(data.message || '注册成功', 'success') + if (data.notice) notyf(data.notice, 'warning') setTimeout(() => { window.location.href = data.redirect || '/' }, 500) } else { - showToast(data.message || '注册失败', 'error') + notyf(data.message || '注册失败', 'danger') try { if (captchaState && captchaState.scenes && captchaState.scenes.register) { captchaState.scenes.register.reset() @@ -235,7 +235,7 @@ } }) .catch(() => { - showToast('网络错误', 'error') + notyf('网络错误', 'danger') try { if (captchaState && captchaState.scenes && captchaState.scenes.register) { captchaState.scenes.register.reset() @@ -261,7 +261,7 @@ const loginMode = form.querySelector('[name="login_mode"]')?.value || 'password' if (!phone) { - showToast('请输入手机号', 'warning') + notyf('请输入手机号', 'warning') return } @@ -275,7 +275,7 @@ if (loginMode === 'code') { const code = form.querySelector('[name="code"]')?.value?.trim() if (!code) { - showToast('请输入验证码', 'warning') + notyf('请输入验证码', 'warning') if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = '登录' } return } @@ -283,7 +283,7 @@ body = { phone, code } } else { if (!password) { - showToast('请输入密码', 'warning') + notyf('请输入密码', 'warning') if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = '登录' } return } @@ -302,12 +302,12 @@ .then(r => r.json().then(data => ({ ok: r.ok, data }))) .then(({ ok, data }) => { if (ok && data.success) { - showToast('登录成功', 'success') + notyf('登录成功', 'success') setTimeout(() => { window.location.href = data.data?.redirect || '/' }, 500) } else { - showToast(data.message || '登录失败', 'error') + notyf(data.message || '登录失败', 'danger') if (captchaState && captchaState.scenes && captchaState.scenes.login) { captchaState.scenes.login.reset() } @@ -318,7 +318,7 @@ } }) .catch(() => { - showToast('网络错误', 'error') + notyf('网络错误', 'danger') if (submitBtn) { submitBtn.disabled = false submitBtn.textContent = '登录' @@ -388,7 +388,7 @@ const confirm = form.querySelector('[name="password_confirm"]') if (password && confirm && password.value !== confirm.value) { e.preventDefault() - showToast('两次密码不一致', 'warning') + notyf('两次密码不一致', 'warning') confirm.focus() return } @@ -396,22 +396,6 @@ }) } - function showToast(message, type) { - if (typeof window.aksrtShowToast === 'function') { - window.aksrtShowToast(message, type) - return - } - const toast = document.createElement('div') - toast.className = 'aksrt-auth-toast aksrt-auth-toast--' + (type || 'info') - toast.textContent = message - document.body.appendChild(toast) - setTimeout(() => toast.classList.add('aksrt-auth-toast--show'), 10) - setTimeout(() => { - toast.classList.remove('aksrt-auth-toast--show') - setTimeout(() => toast.remove(), 300) - }, 3000) - } - // 初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initAuthPages) @@ -424,6 +408,5 @@ init: initAuthPages, sendSmsCode, switchAuthTab, - showToast, } })() diff --git a/assets/js/user-badges.js b/assets/js/user-badges.js index 93a299a7..d9043fad 100644 --- a/assets/js/user-badges.js +++ b/assets/js/user-badges.js @@ -20,10 +20,6 @@ .replace(/'/g, '''); } - function toast(message, type) { - window.AksrtToast(message, type || 'info'); - } - function api(path, options) { return window.AksrtRequest.request(path, options || {}).then(function (response) { return { @@ -177,19 +173,19 @@ }).then(function (result) { if (state.session && state.session.closed) return; if (!result.ok) { - toast((result.json && (result.json.message || (result.json.data && result.json.data.message))) || '佩戴失败', 'error'); + notyf((result.json && (result.json.message || (result.json.data && result.json.data.message))) || '佩戴失败', 'danger'); if (button) button.disabled = false; return; } if (result.json && result.json.data) state.data = result.json.data; state.detailSlug = slug; window.AksrtModalRuntime.invalidate(['badges', 'identity', 'badges:user:' + state.userId]); - toast((result.json && result.json.message) || '已佩戴徽章', 'success'); + notyf((result.json && result.json.message) || '已佩戴徽章', 'success'); renderInto(state); syncHeaderIcon(state); }).catch(function (error) { if (error && error.name === 'AbortError') return; - toast('网络错误', 'error'); + notyf('网络错误', 'danger'); if (button) button.disabled = false; }).finally(function () { state.wearing = false; diff --git a/page.php b/page.php index a3c41b0f..aa44c131 100644 --- a/page.php +++ b/page.php @@ -113,7 +113,5 @@ $show_sidebar = function_exists('aksrt_is_show_sidebar') ? aksrt_is_show_sidebar -
- -
- -
$render_context]); ?> diff --git a/tests/widget-system/toast-contract.test.cjs b/tests/widget-system/toast-contract.test.cjs new file mode 100644 index 00000000..c41073f3 --- /dev/null +++ b/tests/widget-system/toast-contract.test.cjs @@ -0,0 +1,218 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); +const vm = require('node:vm'); + +const root = path.resolve(__dirname, '..', '..'); +const read = (file) => fs.readFileSync(path.join(root, file), 'utf8'); + +class FakeElement { + constructor(classes = [], id = '') { + this.classes = new Set(classes); + this.id = id; + this.children = []; + this.parent = null; + this.innerHTML = ''; + this.delegated = null; + } + + append(child) { + child.parent = this; + this.children.push(child); + } +} + +function descendants(element) { + return element.children.flatMap((child) => [child, ...descendants(child)]); +} + +function createRuntime() { + const body = new FakeElement(['body']); + const document = { body }; + const timers = []; + const window = { + setTimeout(callback, delay) { + timers.push({ callback, delay }); + return timers.length; + }, + }; + + class FakeQuery { + constructor(elements) { + this.elements = elements; + this.length = elements.length; + } + + append(value) { + let elements = value instanceof FakeQuery ? value.elements : []; + if (typeof value === 'string' && value === '
') { + elements = [new FakeElement(['notyn'])]; + } + this.elements.forEach((parent) => elements.forEach((element) => parent.append(element))); + return this; + } + + addClass(classes) { + this.elements.forEach((element) => classes.split(/\s+/).forEach((name) => element.classes.add(name))); + return this; + } + + removeClass() { + this.elements.forEach((element) => element.classes.clear()); + return this; + } + + find(selector) { + return new FakeQuery(this.elements.flatMap(descendants).filter((element) => matches(element, selector))); + } + + html(value) { + this.elements.forEach((element) => { element.innerHTML = value; }); + return this; + } + + remove() { + this.elements.forEach((element) => { + if (!element.parent) return; + element.parent.children = element.parent.children.filter((child) => child !== element); + element.parent = null; + }); + return this; + } + + on(event, selector, handler) { + this.elements.forEach((element) => { element.delegated = { event, selector, handler }; }); + return this; + } + } + + function matches(element, selector) { + if (selector.startsWith('.')) return element.classes.has(selector.slice(1)); + if (selector.startsWith('#')) return element.id === selector.slice(1); + return false; + } + + function parseNotification(html) { + const id = (html.match(/ id="([^"]+)"/) || [])[1] || ''; + const content = html.match(/
([\s\S]*)<\/div><\/div>$/); + assert.ok(content, 'notification markup should follow the Zibll contract'); + const wrapper = new FakeElement(['noty1'], id); + const message = new FakeElement(['notyf', content[1]]); + message.innerHTML = content[2]; + wrapper.append(message); + return wrapper; + } + + function jQuery(value) { + if (value === document.body || value instanceof FakeElement) return new FakeQuery([value]); + if (typeof value === 'string' && value.startsWith('
matches(element, value))); + } + return new FakeQuery([]); + } + + vm.runInNewContext(read('assets/js/notyf.js'), { jQuery, window, document }); + return { body, timers, window }; +} + +test('notyf follows Zibll defaults, type normalization, and timing', () => { + const runtime = createRuntime(); + + runtime.window.notyf('完成'); + const container = runtime.body.children[0]; + const first = container.children[0]; + assert.ok(container.classes.has('notyn')); + assert.ok(first.classes.has('noty1')); + assert.ok(first.children[0].classes.has('success')); + assert.equal(first.children[0].innerHTML, '完成'); + assert.equal(runtime.timers[0].delay, 5000); + + runtime.window.notyf('失败', 'error', 2); + const second = container.children[1]; + assert.ok(second.children[0].classes.has('danger')); + assert.equal(runtime.timers[1].delay, 2000); +}); + +test('fixed-id notifications stay open until an in-place update', () => { + const runtime = createRuntime(); + + runtime.window.notyf('处理中', 'load', '', 'request-state'); + const container = runtime.body.children[0]; + const notification = container.children[0]; + assert.equal(runtime.timers.length, 0); + assert.equal(notification.id, 'request-state'); + assert.ok(notification.children[0].classes.has('load')); + + runtime.window.notyf('完成', 'success', 3, 'request-state'); + assert.equal(container.children.length, 1); + assert.equal(notification.children[0].innerHTML, '完成'); + assert.ok(notification.children[0].classes.has('success')); + assert.equal(runtime.timers[0].delay, 3000); +}); + +test('clicking a notification uses the Zibll exit class and removal delay', () => { + const runtime = createRuntime(); + runtime.window.notyf('可关闭'); + const notification = runtime.body.children[0].children[0]; + const delegated = runtime.body.delegated; + + assert.deepEqual({ event: delegated.event, selector: delegated.selector }, { event: 'click', selector: '.noty1' }); + delegated.handler.call(notification); + assert.ok(notification.classes.has('notyn-out')); + assert.equal(runtime.timers.at(-1).delay, 1000); + runtime.timers.at(-1).callback(); + assert.equal(runtime.body.children[0].children.length, 0); +}); + +test('notyf CSS and WordPress enqueue match the reference contract', () => { + const css = read('assets/css/frontend/notyf.css'); + const scripts = read('src/Core/ScriptsPart1c.php'); + + assert.match(css, /\.notyn\s*\{[\s\S]*?bottom:\s*6em;[\s\S]*?max-width:\s*320px;[\s\S]*?z-index:\s*10000000;/); + assert.match(css, /\.notyf\s*\{[\s\S]*?min-width:\s*200px;[\s\S]*?padding:\s*1em 3em 1em 3\.4em;/); + assert.match(css, /rgba\(15, 147, 249, 0\.7\)[\s\S]*?rgba\(61, 189, 249, 0\.8\)/); + assert.match(css, /rgba\(58, 162, 54, 0\.8\)[\s\S]*?rgba\(89, 247, 131, 0\.8\)/); + assert.match(css, /rgba\(253, 170, 71, 0\.7\)[\s\S]*?rgba\(247, 154, 13, 0\.8\)/); + assert.match(css, /rgba\(253, 69, 28, 0\.7\)[\s\S]*?rgba\(251, 110, 75, 0\.8\)/); + assert.match(css, /content:\s*'\\f058'/); + assert.match(css, /content:\s*'\\f0a1'/); + assert.match(css, /content:\s*'\\f05a'/); + assert.match(css, /content:\s*'\\f057'/); + assert.match(css, /\.notyf\.load::before[\s\S]*?huan-rotate 1s/); + + assert.match(scripts, /'aksrt-notyf',[\s\S]*?'\/assets\/js\/notyf\.js',[\s\S]*?\['jquery'\]/); + assert.match(scripts, /'aksrt-runtime',[\s\S]*?\['aksrt-rehydrate', 'aksrt-notyf'\]/); + assert.match(scripts, /'\/assets\/css\/frontend\/notyf\.css',[\s\S]*?'aksrt-font-awesome-5-v4-shims'/); +}); + +test('legacy frontend toast APIs and selectors are fully removed', () => { + const files = [ + 'assets/js', + 'assets/css/frontend', + 'assets/css/routes', + 'assets/css/build/frontend-base.min.css.disabled', + 'page.php', + 'templates/pages/page-profile.php', + ]; + const legacy = /AksrtToast|aksrtToast|aksrtShowToast|aksrt\.(?:toast|notyf)|showToast|toast-container|toast-item|aksrt-toast/; + + function sourceFiles(target) { + const absolute = path.join(root, target); + if (!fs.statSync(absolute).isDirectory()) return [absolute]; + return fs.readdirSync(absolute, { withFileTypes: true }).flatMap((entry) => { + if (entry.name === 'admin' || entry.name.endsWith('.min.js')) return []; + const child = path.join(absolute, entry.name); + return entry.isDirectory() ? sourceFiles(path.relative(root, child)) : [child]; + }); + } + + const scanned = files.flatMap(sourceFiles); + const offenders = scanned.filter((file) => legacy.test(fs.readFileSync(file, 'utf8'))); + assert.deepEqual(offenders.map((file) => path.relative(root, file)), []); +});