From 34150f335831c56ccd4edb6446c106c95caab8b9 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 5 Aug 2026 00:06:37 +0800 Subject: [PATCH 1/2] refactor(modal): remove legacy compatibility paths Co-Authored-By: Claude Fable 5 --- .../{remote-modal.css => modal-runtime.css} | 75 +++++++------ assets/css/frontend/vip-modal.css | 38 ++----- assets/css/frontend/widget-batch3f.css | 2 +- assets/js/aksrt-runtime.js | 70 ++---------- assets/js/core/modal-runtime.js | 101 +++++++----------- assets/js/core/modal-triggers.js | 42 ++++++++ assets/js/core/remote-modal.js | 77 ------------- assets/js/enhancements2.js | 2 +- assets/js/order-center.js | 21 +--- assets/js/pay-vip.js | 35 ++---- assets/js/user-badges.js | 45 +++----- inc/helpers/aksrt-ajax-list.php | 20 ++-- src/Content/VipServiceModal.php | 4 +- src/Core/ScriptsPart1b.php | 6 +- src/Core/ScriptsPart1c.php | 6 +- src/Credits/Signin.php | 2 +- src/User/Badges.php | 20 ++-- templates/parts/user/author-hero.php | 2 +- 18 files changed, 188 insertions(+), 380 deletions(-) rename assets/css/frontend/{remote-modal.css => modal-runtime.css} (79%) create mode 100644 assets/js/core/modal-triggers.js delete mode 100644 assets/js/core/remote-modal.js diff --git a/assets/css/frontend/remote-modal.css b/assets/css/frontend/modal-runtime.css similarity index 79% rename from assets/css/frontend/remote-modal.css rename to assets/css/frontend/modal-runtime.css index 92a2fdd2..6c481ebc 100644 --- a/assets/css/frontend/remote-modal.css +++ b/assets/css/frontend/modal-runtime.css @@ -1,17 +1,17 @@ /** - * Remote modal + ajax list primitives (shared). + * Modal runtime + ajax list primitives (shared). * * @package AKSRT */ /* ---- scroll lock ---- */ -html.aksrt-remote-modal-open, -body.aksrt-remote-modal-open { +html.aksrt-modal-runtime-open, +body.aksrt-modal-runtime-open { overflow: hidden; } -/* ---- remote modal shell ---- */ -.aksrt-remote-modal { +/* ---- modal runtime shell ---- */ +.aksrt-modal-runtime { position: fixed; inset: 0; z-index: 100050; @@ -24,20 +24,20 @@ body.aksrt-remote-modal-open { pointer-events: none; } -.aksrt-remote-modal.is-open { +.aksrt-modal-runtime.is-open { opacity: 1; visibility: visible; pointer-events: auto; } -.aksrt-remote-modal__overlay { +.aksrt-modal-runtime__overlay { position: absolute; inset: 0; background: rgba(15, 18, 28, 0.48); backdrop-filter: blur(2px); } -.aksrt-remote-modal__dialog { +.aksrt-modal-runtime__dialog { position: relative; z-index: 1; width: min(440px, calc(100vw - 32px)); @@ -51,15 +51,15 @@ body.aksrt-remote-modal-open { color: var(--color-ink, #1f2937); } -.aksrt-remote-modal.is-open .aksrt-remote-modal__dialog { +.aksrt-modal-runtime.is-open .aksrt-modal-runtime__dialog { transform: translateY(0) scale(1); } -.aksrt-remote-modal--bottom { +.aksrt-modal-runtime--bottom { align-items: flex-end; } -.aksrt-remote-modal--bottom .aksrt-remote-modal__dialog { +.aksrt-modal-runtime--bottom .aksrt-modal-runtime__dialog { width: 100%; max-width: 100%; border-radius: 16px 16px 0 0; @@ -67,11 +67,11 @@ body.aksrt-remote-modal-open { transform: translateY(24px); } -.aksrt-remote-modal--bottom.is-open .aksrt-remote-modal__dialog { +.aksrt-modal-runtime--bottom.is-open .aksrt-modal-runtime__dialog { transform: translateY(0); } -.aksrt-remote-modal__header { +.aksrt-modal-runtime__header { display: flex; align-items: center; justify-content: space-between; @@ -79,32 +79,32 @@ body.aksrt-remote-modal-open { padding: 16px 18px 0; } -.aksrt-remote-modal__title { +.aksrt-modal-runtime__title { margin: 0; font-size: 17px; line-height: 1.35; } -.aksrt-remote-modal__content { +.aksrt-modal-runtime__content { padding: 18px 18px 16px; } -.aksrt-remote-modal__loading, -.aksrt-remote-modal__error-body { +.aksrt-modal-runtime__loading, +.aksrt-modal-runtime__error-body { padding: 28px 8px; text-align: center; color: var(--color-ink-2, #6b7280); font-size: 14px; } -.aksrt-remote-modal__skeleton { +.aksrt-modal-runtime__skeleton { display: grid; gap: 12px; padding: 8px 2px; } -.aksrt-remote-modal__skeleton-line, -.aksrt-remote-modal__skeleton-block { +.aksrt-modal-runtime__skeleton-line, +.aksrt-modal-runtime__skeleton-block { display: block; border-radius: 8px; background: linear-gradient(90deg, rgba(148, 163, 184, 0.16), rgba(148, 163, 184, 0.3), rgba(148, 163, 184, 0.16)); @@ -112,26 +112,26 @@ body.aksrt-remote-modal-open { animation: aksrt-modal-skeleton 1.2s ease-in-out infinite; } -.aksrt-remote-modal__skeleton-line { +.aksrt-modal-runtime__skeleton-line { width: 72%; height: 12px; } -.aksrt-remote-modal__skeleton-line.is-wide { +.aksrt-modal-runtime__skeleton-line.is-wide { width: 92%; height: 18px; } -.aksrt-remote-modal__skeleton-line.is-short { +.aksrt-modal-runtime__skeleton-line.is-short { width: 44%; } -.aksrt-remote-modal__skeleton-block { +.aksrt-modal-runtime__skeleton-block { width: 100%; min-height: 84px; } -.aksrt-remote-modal__retry { +.aksrt-modal-runtime__retry { cursor: pointer; } @@ -140,8 +140,8 @@ body.aksrt-remote-modal-open { 100% { background-position: -100% 0; } } -.aksrt-remote-modal .touch.border-title, -.aksrt-remote-modal .border-title { +.aksrt-modal-runtime .touch.border-title, +.aksrt-modal-runtime .border-title { font-size: 16px; font-weight: 700; margin: 0 0 14px; @@ -151,8 +151,7 @@ body.aksrt-remote-modal-open { gap: 6px; } -.aksrt-remote-modal .abs-close, -.aksrt-remote-modal .aksrt-remote-modal__close { +.aksrt-modal-runtime .aksrt-modal-runtime__close { position: absolute; top: 10px; right: 10px; @@ -167,17 +166,17 @@ body.aksrt-remote-modal-open { line-height: 1; } -.aksrt-remote-modal .but-average.modal-buts, -.aksrt-remote-modal .modal-buts { +.aksrt-modal-runtime .but-average.modal-buts, +.aksrt-modal-runtime .modal-buts { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 16px; } -.aksrt-remote-modal .but-average.modal-buts .but, -.aksrt-remote-modal .modal-buts .but, -.aksrt-remote-modal .modal-buts button { +.aksrt-modal-runtime .but-average.modal-buts .but, +.aksrt-modal-runtime .modal-buts .but, +.aksrt-modal-runtime .modal-buts button { min-height: 40px; border-radius: 10px; border: 1px solid var(--color-line, #e5e7eb); @@ -187,9 +186,9 @@ body.aksrt-remote-modal-open { cursor: pointer; } -.aksrt-remote-modal .modal-buts .but.c-blue, -.aksrt-remote-modal .modal-buts button.c-blue, -.aksrt-remote-modal .modal-buts .btn-primary { +.aksrt-modal-runtime .modal-buts .but.c-blue, +.aksrt-modal-runtime .modal-buts button.c-blue, +.aksrt-modal-runtime .modal-buts .btn-primary { background: var(--color-primary, #f04494); border-color: color-mix(in srgb, var(--color-primary, #f04494) 70%, #000 0%); color: var(--color-on-primary, #fff); @@ -351,7 +350,7 @@ body.aksrt-remote-modal-open { } @media (max-width: 768px) { - .aksrt-remote-modal:not(.aksrt-remote-modal--bottom) .aksrt-remote-modal__dialog { + .aksrt-modal-runtime:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog { width: calc(100vw - 20px); } } diff --git a/assets/css/frontend/vip-modal.css b/assets/css/frontend/vip-modal.css index bd68c1ce..ef6e0b58 100644 --- a/assets/css/frontend/vip-modal.css +++ b/assets/css/frontend/vip-modal.css @@ -2,26 +2,7 @@ * VIP pay modal + hover cards (zibll payvip-modal / vip-card). */ -.aksrt-vip-modal-open { - overflow: hidden; -} - -.aksrt-vip-modal-overlay { - position: fixed; - inset: 0; - z-index: 1200; - display: none; - align-items: center; - justify-content: center; - padding: 16px; - background: rgba(0, 0, 0, 0.5); -} - -.aksrt-vip-modal-overlay.is-open { - display: flex; -} - -.aksrt-vip-modal-dialog { +.aksrt-vip-modal-panel { width: min(560px, 100%); max-height: min(90vh, 760px); overflow: auto; @@ -31,11 +12,11 @@ } /* VIP now uses the shared modal runtime host. */ -.aksrt-remote-modal.aksrt-vip-modal-shell { +.aksrt-modal-runtime.aksrt-vip-modal-shell { z-index: 1200; } -.aksrt-remote-modal.aksrt-vip-modal-shell .aksrt-remote-modal__dialog { +.aksrt-modal-runtime.aksrt-vip-modal-shell .aksrt-modal-runtime__dialog { width: min(560px, 100%); max-height: min(90vh, 760px); background: var(--main-bg-color, #fff); @@ -43,7 +24,7 @@ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.22); } -.aksrt-remote-modal.aksrt-vip-modal-shell .aksrt-remote-modal__content { +.aksrt-modal-runtime.aksrt-vip-modal-shell .aksrt-modal-runtime__content { padding: 0; } @@ -52,8 +33,7 @@ padding: 18px 16px 16px; } -.payvip-modal .close, -.aksrt-vip-modal-close { +.aksrt-modal-close { position: absolute; top: 8px; right: 12px; @@ -121,7 +101,7 @@ /* Modal-only equity (admin HTML; no forced bullets) */ .payvip-modal .v-equity, -.aksrt-vip-modal-dialog .v-equity { +.aksrt-vip-modal-panel .v-equity { display: block; list-style: none; margin: 8px 0 0; @@ -130,7 +110,7 @@ line-height: 1.6; } .payvip-modal .v-equity > li, -.aksrt-vip-modal-dialog .v-equity > li { +.aksrt-vip-modal-panel .v-equity > li { display: block; margin: 0 0 6px; padding: 0; @@ -138,8 +118,8 @@ } .payvip-modal .v-equity > li::before, .payvip-modal .v-equity > li::after, -.aksrt-vip-modal-dialog .v-equity > li::before, -.aksrt-vip-modal-dialog .v-equity > li::after { +.aksrt-vip-modal-panel .v-equity > li::before, +.aksrt-vip-modal-panel .v-equity > li::after { content: none !important; display: none !important; } diff --git a/assets/css/frontend/widget-batch3f.css b/assets/css/frontend/widget-batch3f.css index 9f1b0d1d..9609e788 100644 --- a/assets/css/frontend/widget-batch3f.css +++ b/assets/css/frontend/widget-batch3f.css @@ -166,7 +166,7 @@ font-size: 16px; margin-bottom: 14px; } -.aksrt-checkin-modal__head .aksrt-remote-modal__close { +.aksrt-checkin-modal__head .aksrt-modal-runtime__close { margin-left: auto; border: 0; background: transparent; diff --git a/assets/js/aksrt-runtime.js b/assets/js/aksrt-runtime.js index 349575ba..85fa4183 100644 --- a/assets/js/aksrt-runtime.js +++ b/assets/js/aksrt-runtime.js @@ -5,7 +5,6 @@ * 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.refreshModal(opts) — remote content modal * aksrt.scrollTopTo(target, off) — smart scroll with header offset * aksrt.loading(el, show) — element-level loading overlay * aksrt.Drawer(opts) — mobile fullscreen drawer + route @@ -313,68 +312,6 @@ el.appendChild(mask); }; - // ══════════════════════════════════════════════════════════════ - // §10.10 — refreshModal: remote content modal - // ══════════════════════════════════════════════════════════════ - - function closeRefreshModal() { - if (window.AksrtModalRuntime && typeof window.AksrtModalRuntime.close === 'function') { - window.AksrtModalRuntime.close('refresh-modal'); - } - } - - aksrt.refreshModal = function (opts) { - opts = opts || {}; - var runtime = window.AksrtModalRuntime; - if (!runtime || typeof runtime.open !== 'function' || typeof runtime.openRemote !== 'function') { - return Promise.reject(new Error('modal runtime unavailable')); - } - - var base = { - title: opts.title || '', - className: opts.class || '', - bottom: opts.mobile_from_bottom === true, - mobileBottom: opts.mobile_from_bottom === true, - cache: opts.cache !== false, - cacheTtl: opts.cacheTtl || 15000, - cacheTags: opts.cacheTags || opts.tags || [], - kind: opts.kind || 'generic', - }; - - if (opts.content !== undefined) { - return Promise.resolve(runtime.open(Object.assign({}, base, { html: opts.content }))) - .catch(function () { return null; }); - } - - if (opts.remote) { - return runtime.openRemote(Object.assign({}, base, { - url: opts.remote, - method: opts.method || 'GET', - })); - } - - if (opts.action) { - var ajaxUrl = (window.aksrtData && window.aksrtData.ajaxUrl) || '/wp-admin/admin-ajax.php'; - var params = new URLSearchParams(); - params.set('action', opts.action); - if (window.aksrtData && window.aksrtData.nonce) params.set('nonce', window.aksrtData.nonce); - if (opts.data) { - Object.keys(opts.data).forEach(function (key) { params.set(key, opts.data[key]); }); - } - return runtime.openRemote(Object.assign({}, base, { - url: ajaxUrl, - method: 'POST', - cache: false, - headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, - body: params.toString(), - })); - } - - return Promise.resolve(runtime.open(base)); - }; - - aksrt.refreshModal.close = closeRefreshModal; - // ══════════════════════════════════════════════════════════════ // §10.11 — Drawer: mobile fullscreen slide-in + route // ══════════════════════════════════════════════════════════════ @@ -707,7 +644,12 @@ btn.classList.remove('is-loading'); if (json && json.success) { if (json.data && json.data.show_modal) { - aksrt.refreshModal({ content: json.data.show_modal, mobile_from_bottom: true }); + window.AksrtModalRuntime.open({ + html: json.data.show_modal, + bottom: true, + mobileBottom: true, + kind: 'generic', + }); } else if (json.data && json.data.message) { aksrt.notyf(json.data.message, 'success'); } diff --git a/assets/js/core/modal-runtime.js b/assets/js/core/modal-runtime.js index c5150ccd..7440dbbe 100644 --- a/assets/js/core/modal-runtime.js +++ b/assets/js/core/modal-runtime.js @@ -1,7 +1,7 @@ /** * Canonical frontend modal runtime. * - * Keeps the existing remote-modal shell while adding cancellable loaders, + * Owns the modal shell while adding cancellable loaders, * short in-memory caching, request deduplication, retryable errors, focus * restoration, and modal-scoped rehydration. * @@ -10,7 +10,7 @@ (function (window, document) { "use strict"; - var ROOT_ID = "aksrt-remote-modal-root"; + var ROOT_ID = "aksrt-modal-runtime-root"; var active = null; var sequence = 0; var loaders = Object.create(null); @@ -24,7 +24,7 @@ if (root) return root; root = document.createElement("div"); root.id = ROOT_ID; - root.className = "aksrt-remote-modal-host"; + root.className = "aksrt-modal-runtime-host"; document.body.appendChild(root); return root; } @@ -39,31 +39,28 @@ } function lockScroll(lock) { - document.documentElement.classList.toggle("aksrt-remote-modal-open", !!lock); - document.body.classList.toggle("aksrt-remote-modal-open", !!lock); + document.documentElement.classList.toggle("aksrt-modal-runtime-open", !!lock); + document.body.classList.toggle("aksrt-modal-runtime-open", !!lock); } function skeleton(options) { return (options && options.skeleton) || - '
' + - '' + - '' + - '' + - '' + + '
' + + '' + + '' + + '' + + '' + '
'; } function telemetryStart(kind) { if (kind === false) return ""; - var telemetry = window.AksrtModalTelemetry; - if (!telemetry || typeof telemetry.start !== "function") return ""; - return telemetry.start(kind || "unknown"); + return window.AksrtModalTelemetry.start(kind || "generic"); } function telemetryMark(session, event) { - var telemetry = window.AksrtModalTelemetry; - if (!session || !session.telemetryId || !telemetry || typeof telemetry.mark !== "function") return; - telemetry.mark(session.telemetryId, event); + if (!session || !session.telemetryId) return; + window.AksrtModalTelemetry.mark(session.telemetryId, event); } function setState(session, status, error) { @@ -81,7 +78,7 @@ } function closeSelector() { - return "[data-aksrt-remote-modal-close], [data-aksrt-modal-close], [data-dismiss='modal'], .aksrt-remote-modal__close, .aksrt-vip-modal-close, .payvip-modal .close"; + return "[data-aksrt-modal-close], .aksrt-modal-runtime__close"; } function bindChrome(session) { @@ -93,11 +90,11 @@ close("control"); return; } - if (event.target === shell || event.target.classList.contains("aksrt-remote-modal__overlay")) { + if (event.target === shell || event.target.classList.contains("aksrt-modal-runtime__overlay")) { close("overlay"); return; } - var retry = event.target.closest("[data-aksrt-remote-modal-retry], [data-aksrt-modal-retry]"); + var retry = event.target.closest("[data-aksrt-modal-retry]"); if (retry) { event.preventDefault(); refresh(); @@ -138,31 +135,29 @@ var initialHtml = options.html !== undefined ? options.html : skeleton(options); var titleId = "aksrt-modal-title-" + String(sequence + 1); var titleHtml = options.title - ? '

' + escapeHtml(options.title) + '

' + ? '

' + escapeHtml(options.title) + '

' : ""; shell.className = - "aksrt-remote-modal" + - (bottom ? " aksrt-remote-modal--bottom" : "") + + "aksrt-modal-runtime" + + (bottom ? " aksrt-modal-runtime--bottom" : "") + (options.className ? " " + options.className : ""); shell.setAttribute("role", "dialog"); shell.setAttribute("aria-modal", "true"); if (options.title) shell.setAttribute("aria-labelledby", titleId); shell.setAttribute("tabindex", "-1"); shell.innerHTML = - '
' + - '
' + + '
' + + '
' + titleHtml + - '
' + initialHtml + "
"; + '
' + initialHtml + "
"; var session = { id: String(++sequence), shell: shell, - content: shell.querySelector(".aksrt-remote-modal__content"), + content: shell.querySelector(".aksrt-modal-runtime__content"), options: options, - state: window.AksrtModalState && typeof window.AksrtModalState.create === "function" - ? window.AksrtModalState.create(options.kind || "generic") - : { status: "idle", error: null }, + state: window.AksrtModalState.create(options.kind || "generic"), focusReturn: document.activeElement, controller: null, flight: null, @@ -184,9 +179,6 @@ (nodes.length ? nodes[0] : shell).focus(); } catch (error) {} - document.dispatchEvent(new CustomEvent("aksrt:remote-modal:open", { - detail: { shell: shell, options: options, session: session }, - })); document.dispatchEvent(new CustomEvent("aksrt:modal:open", { detail: { shell: shell, options: options, session: session }, })); @@ -203,9 +195,7 @@ function rehydrate(session) { if (!session || !session.content) return; - if (window.AksrtFrontend && typeof window.AksrtFrontend.rehydrate === "function") { - window.AksrtFrontend.rehydrate(session.content, "modal"); - } + window.AksrtFrontend.rehydrate(session.content, "modal"); } function abortSessionRequest(session) { @@ -226,9 +216,6 @@ if (!preserveRequest) abortSessionRequest(session); session.shell.classList.remove("is-open"); if (!preserveRequest) lockScroll(false); - document.dispatchEvent(new CustomEvent("aksrt:remote-modal:close", { - detail: { shell: session.shell, reason: reason || "close", session: session }, - })); document.dispatchEvent(new CustomEvent("aksrt:modal:close", { detail: { shell: session.shell, reason: reason || "close", session: session }, })); @@ -249,8 +236,6 @@ if (json) { if (typeof json.html === "string") return json.html; if (json.data && typeof json.data.html === "string") return json.data.html; - if (json.data && typeof json.data.show_modal === "string") return json.data.show_modal; - if (typeof json.data === "string") return json.data; } return typeof result.text === "string" ? result.text : ""; } @@ -266,11 +251,11 @@ setState(session, unauthorized ? "unauthorized" : "error", error); telemetryMark(session, unauthorized ? "unauthorized" : "error"); session.content.innerHTML = - '
提示
' + - '
' + escapeHtml(errorMessage(error)) + + '
提示
' + + '
' + escapeHtml(errorMessage(error)) + '
'; + '' + + '
'; rehydrate(session); if (typeof session.options.onError === "function") session.options.onError(error, session); } @@ -282,7 +267,7 @@ setState(session, "empty"); telemetryMark(session, "first-content"); telemetryMark(session, "ready"); - session.content.innerHTML = '
暂无内容
'; + session.content.innerHTML = '
暂无内容
'; return; } session.content.innerHTML = html; @@ -311,21 +296,14 @@ if (requestOptions.body === undefined) delete requestOptions.body; if (!requestOptions.signal) delete requestOptions.signal; - var request = window.AksrtRequest && typeof window.AksrtRequest.request === "function" - ? function () { return window.AksrtRequest.request(session.options.url, requestOptions); } - : function () { - return window.fetch(session.options.url, requestOptions).then(function (response) { - return response.text().then(function (text) { return { ok: response.ok, status: response.status, text: text, json: null }; }); - }); - }; + var request = function () { + return window.AksrtRequest.request(session.options.url, requestOptions); + }; var method = String(session.options.method || "GET").toUpperCase(); var cacheable = method === "GET" && session.options.cache !== false; var cacheKey = session.options.cacheKey || - method + ":" + - (window.AksrtRequest && typeof window.AksrtRequest.url === "function" - ? window.AksrtRequest.url(session.options.url, session.options.query) - : session.options.url); + method + ":" + window.AksrtRequest.url(session.options.url, session.options.query); var cache = window.AksrtModalCache; var acquireOptions = { ttl: Number(session.options.cacheTtl || 15000), @@ -339,7 +317,7 @@ }, }; - session.flight = cacheable && cache && typeof cache.acquire === "function" + session.flight = cacheable ? cache.acquire(cacheKey, request, acquireOptions) : { promise: request(), release: function () {} }; @@ -367,9 +345,7 @@ function sessionKey(options) { if (!options || !options.url) return ""; var method = String(options.method || "GET").toUpperCase(); - var requestUrl = window.AksrtRequest && typeof window.AksrtRequest.url === "function" - ? window.AksrtRequest.url(options.url, options.query) - : String(options.url); + var requestUrl = window.AksrtRequest.url(options.url, options.query); return method + ":" + requestUrl; } @@ -400,10 +376,7 @@ } function invalidate(tags) { - if (window.AksrtModalCache && typeof window.AksrtModalCache.invalidate === "function") { - return window.AksrtModalCache.invalidate(tags); - } - return 0; + return window.AksrtModalCache.invalidate(tags); } window.AksrtModalRuntime = { diff --git a/assets/js/core/modal-triggers.js b/assets/js/core/modal-triggers.js new file mode 100644 index 00000000..375a2582 --- /dev/null +++ b/assets/js/core/modal-triggers.js @@ -0,0 +1,42 @@ +/** + * Declarative modal triggers. + * + * The only modal entry point is AksrtModalRuntime. Triggers carry an + * explicit URL and never create a second modal implementation. + * + * @package AKSRT + */ +(function (window, document) { + "use strict"; + + function openFromTrigger(element) { + var runtime = window.AksrtModalRuntime; + var url = element.getAttribute("data-aksrt-modal-url") || ""; + if (!url || url === "#") return; + + var ttl = parseInt(element.getAttribute("data-aksrt-modal-cache-ttl") || "15000", 10); + var tags = (element.getAttribute("data-aksrt-modal-cache-tags") || "").split(",").map(function (tag) { + return tag.trim(); + }).filter(Boolean); + + runtime.openRemote({ + url: url, + method: element.getAttribute("data-aksrt-modal-method") || "GET", + className: element.getAttribute("data-aksrt-modal-class") || "modal-mini full-sm", + height: parseInt(element.getAttribute("data-aksrt-modal-height") || "0", 10) || 0, + mobileBottom: element.getAttribute("data-aksrt-modal-mobile-bottom") === "true", + bottom: element.getAttribute("data-aksrt-modal-bottom") === "true", + kind: element.getAttribute("data-aksrt-modal-kind") || "generic", + cacheKey: element.getAttribute("data-aksrt-modal-cache-key") || "", + cacheTtl: ttl > 0 ? ttl : 15000, + cacheTags: tags, + }); + } + + document.addEventListener("click", function (event) { + var trigger = event.target.closest('[data-aksrt-modal="remote"]'); + if (!trigger) return; + event.preventDefault(); + openFromTrigger(trigger); + }); +})(window, document); diff --git a/assets/js/core/remote-modal.js b/assets/js/core/remote-modal.js deleted file mode 100644 index e115c9b5..00000000 --- a/assets/js/core/remote-modal.js +++ /dev/null @@ -1,77 +0,0 @@ -/** - * AKSRT Remote Modal compatibility bridge. - * - * The lifecycle implementation lives in AksrtModalRuntime. This file keeps - * the legacy trigger contract and the AksrtRemoteModal public alias stable. - * - * @package AKSRT - */ -(function (window, document) { - "use strict"; - - function runtime() { - return window.AksrtModalRuntime || null; - } - - function fromTrigger(element) { - var modalRuntime = runtime(); - if (!modalRuntime || !element) return Promise.reject(new Error("modal runtime unavailable")); - - var url = element.getAttribute("data-remote") || - element.getAttribute("data-aksrt-remote") || - element.getAttribute("href") || ""; - if (!url || url === "#") return Promise.resolve(null); - - var ttl = parseInt(element.getAttribute("data-modal-cache-ttl") || "15000", 10); - var tags = (element.getAttribute("data-modal-cache-tags") || "").split(",").map(function (tag) { - return tag.trim(); - }).filter(Boolean); - - return modalRuntime.openRemote({ - url: url, - method: element.getAttribute("data-method") || "GET", - className: element.getAttribute("data-class") || element.getAttribute("data-aksrt-class") || "modal-mini full-sm", - height: parseInt(element.getAttribute("data-height") || element.getAttribute("data-aksrt-height") || "0", 10) || 0, - mobileBottom: element.getAttribute("mobile-bottom") !== "false", - bottom: element.getAttribute("data-bottom") === "true", - kind: element.getAttribute("data-modal-kind") || "generic", - cacheKey: element.getAttribute("data-modal-cache-key") || "", - cacheTtl: ttl > 0 ? ttl : 15000, - cacheTags: tags, - }); - } - - function bind() { - if (window.__aksrtRemoteModalBound) return; - if (!runtime()) return; - window.__aksrtRemoteModalBound = true; - - document.addEventListener("click", function (event) { - var trigger = event.target.closest( - '[data-toggle="AksrtRemoteModal"], [data-toggle="RefreshModal"], [data-aksrt-remote-modal], a.aksrt-remote-modal-link' - ); - if (!trigger || trigger.getAttribute("data-aksrt-remote-modal") === "off") return; - event.preventDefault(); - fromTrigger(trigger); - }); - } - - function expose() { - var modalRuntime = runtime(); - if (!modalRuntime) return; - window.AksrtRemoteModal = { - open: modalRuntime.open, - openRemote: modalRuntime.openRemote, - close: modalRuntime.close, - refresh: modalRuntime.refresh, - fromTrigger: fromTrigger, - getActive: modalRuntime.getActive, - }; - bind(); - } - - expose(); - if (!window.AksrtModalRuntime) { - document.addEventListener("aksrt:modal-runtime:ready", expose, { once: true }); - } -})(window, document); diff --git a/assets/js/enhancements2.js b/assets/js/enhancements2.js index 110ef7c3..273edb5d 100644 --- a/assets/js/enhancements2.js +++ b/assets/js/enhancements2.js @@ -302,7 +302,7 @@ initParallax(); initFloatLabels(); initSigninCalendar(); - document.addEventListener('aksrt:remote-modal:open', function () { + document.addEventListener('aksrt:modal:open', function () { // Remote modal may inject calendar markup after open. window.setTimeout(initSigninCalendar, 30); }); diff --git a/assets/js/order-center.js b/assets/js/order-center.js index 941b14c9..9927f199 100644 --- a/assets/js/order-center.js +++ b/assets/js/order-center.js @@ -16,21 +16,10 @@ var detailOrderNumber = ""; function toast(message, type) { - if (typeof window.AksrtToast === "function") { - window.AksrtToast(message, type || "info"); - return; - } - if (typeof window.aksrtNotify === "function") { - window.aksrtNotify(message, type || "info"); - return; - } - window.alert(message); + window.AksrtToast(message, type || "info"); } function request(path, body, method) { - if (!window.AksrtRequest || typeof window.AksrtRequest.request !== "function") { - return Promise.resolve({ success: false, message: "请求适配器未加载" }); - } var options = { method: method || "POST" }; if (body !== undefined) options.body = JSON.stringify(body || {}); return window.AksrtRequest.request(path, options).then(function (response) { @@ -219,9 +208,7 @@ button.disabled = true; request(orderPath(number, "/pay"), method ? { payment_method: method } : {}).then(function (result) { if (!result.success) throw new Error(result.message || "支付失败"); - if (window.AksrtModalRuntime && typeof window.AksrtModalRuntime.invalidate === "function") { - window.AksrtModalRuntime.invalidate(["orders", "vip"]); - } + window.AksrtModalRuntime.invalidate(["orders", "vip"]); var data = result.data || {}; if (data.redirect && data.payment_url) { window.location.assign(data.payment_url); @@ -246,9 +233,7 @@ button.disabled = true; request(orderPath(number, "/cancel")).then(function (result) { if (!result.success) throw new Error(result.message || "关闭订单失败"); - if (window.AksrtModalRuntime && typeof window.AksrtModalRuntime.invalidate === "function") { - window.AksrtModalRuntime.invalidate(["orders", "vip"]); - } + window.AksrtModalRuntime.invalidate(["orders", "vip"]); toast("订单已关闭"); refreshView(); }).catch(function (error) { diff --git a/assets/js/pay-vip.js b/assets/js/pay-vip.js index fbbf5283..ad0ef7b2 100644 --- a/assets/js/pay-vip.js +++ b/assets/js/pay-vip.js @@ -6,16 +6,7 @@ "use strict"; function api(path, options) { - var opts = Object.assign({}, options || {}); - var request = window.AksrtRequest && typeof window.AksrtRequest.request === "function" - ? window.AksrtRequest.request(path, opts) - : fetch(path, Object.assign({ credentials: "same-origin" }, opts)).then(function (response) { - return response.json().then(function (json) { - return { ok: response.ok, status: response.status, json: json }; - }); - }); - - return request.then(function (response) { + return window.AksrtRequest.request(path, Object.assign({}, options || {})).then(function (response) { return { ok: response.ok, status: response.status, @@ -34,11 +25,10 @@ var activeVipShell = null; function openModal() { - if (!window.AksrtModalRuntime || typeof window.AksrtModalRuntime.open !== "function") return null; activeVipShell = window.AksrtModalRuntime.open({ className: "aksrt-vip-modal-shell", mobileBottom: false, - skeleton: '
加载中…
', + skeleton: '
加载中…
', telemetry: false, kind: "vip", }); @@ -46,9 +36,7 @@ } function closeModal() { - if (window.AksrtModalRuntime && typeof window.AksrtModalRuntime.close === "function") { - window.AksrtModalRuntime.close("vip"); - } + window.AksrtModalRuntime.close("vip"); activeVipShell = null; } @@ -151,17 +139,13 @@ } var data = r.json && r.json.data ? r.json.data : r.json; if (data && data.redirect && (data.payment_url || data.qr_content)) { - if (window.AksrtModalRuntime && typeof window.AksrtModalRuntime.invalidate === "function") { - window.AksrtModalRuntime.invalidate(["vip", "orders"]); - } + window.AksrtModalRuntime.invalidate(["vip", "orders"]); if (data.payment_url) window.open(data.payment_url, "_blank"); setTip(pane, data.message || "请完成支付,支付成功后将自动刷新。"); if (data.order_number) pollOrder(data.order_number); return; } - if (window.AksrtModalRuntime && typeof window.AksrtModalRuntime.invalidate === "function") { - window.AksrtModalRuntime.invalidate(["vip", "orders"]); - } + window.AksrtModalRuntime.invalidate(["vip", "orders"]); setTip(pane, (data && data.message) || "开通成功,正在刷新…"); setTimeout(function () { window.location.reload(); @@ -189,11 +173,6 @@ if (telemetry && telemetryId && typeof telemetry.mark === "function") telemetry.mark(telemetryId, event); } var shell = openModal(); - if (!shell) { - loading = false; - telemetryMark("error"); - return; - } var body = shell.querySelector("[data-aksrt-vip-modal-body]"); telemetryMark("shell"); @@ -220,9 +199,7 @@ if (body) { body.innerHTML = r.json.html; telemetryMark("first-content"); - if (window.AksrtFrontend && typeof window.AksrtFrontend.rehydrate === "function") { - window.AksrtFrontend.rehydrate(body, "modal"); - } + window.AksrtFrontend.rehydrate(body, "modal"); bindModalUi(body); telemetryMark("ready"); } diff --git a/assets/js/user-badges.js b/assets/js/user-badges.js index 9e3fd19e..24df0b37 100644 --- a/assets/js/user-badges.js +++ b/assets/js/user-badges.js @@ -3,15 +3,12 @@ * * User-center modal: category-grouped medal card grid + in-modal detail * view (medal-background card, obtain time, wear action). Also provides the - * global wear handler used inside remote (AJAX HTML) badge modals. + * global wear handler used inside runtime-loaded badge modals. */ (function () { 'use strict'; function api(path, options) { - if (!window.AksrtRequest || typeof window.AksrtRequest.request !== 'function') { - return Promise.resolve({ ok: false, status: 0, json: { message: '请求适配器未加载' } }); - } return window.AksrtRequest.request(path, options || {}).then(function (response) { return { ok: response.ok, status: response.status, json: response.json || {} }; }).catch(function (error) { @@ -25,15 +22,7 @@ } function toast(msg, type) { - if (typeof window.AksrtToast === 'function') { - window.AksrtToast(msg, type || 'info'); - return; - } - if (typeof window.aksrtNotify === 'function') { - window.aksrtNotify(msg, type || 'info'); - return; - } - try { console.log('[AKSRT badges]', msg); } catch (_) {} + window.AksrtToast(msg, type || 'info'); } function escapeHtml(s) { @@ -260,9 +249,7 @@ return; } toast((r.json && r.json.message) || '已佩戴徽章', 'success'); - if (window.AksrtModalRuntime && typeof window.AksrtModalRuntime.invalidate === 'function') { - window.AksrtModalRuntime.invalidate(['badges', 'identity']); - } + window.AksrtModalRuntime.invalidate(['badges', 'identity']); state.data = (r.json && r.json.data) || state.data; renderHead(); renderGrid(); @@ -352,9 +339,7 @@ return; } toast((r.json && r.json.message) || '已佩戴徽章', 'success'); - if (window.AksrtModalRuntime && typeof window.AksrtModalRuntime.invalidate === 'function') { - window.AksrtModalRuntime.invalidate(['badges', 'identity']); - } + window.AksrtModalRuntime.invalidate(['badges', 'identity']); // Re-open the refreshed single-badge modal so state stays in sync. var refreshed = null; try { @@ -362,19 +347,21 @@ var data = r.json && r.json.data; var isSelf = data && data.is_self; if (isSelf && data.wear) { - refreshed = (window.aksrtData && aksrtData.ajaxUrl) - ? aksrtData.ajaxUrl + '?action=aksrt_single_badge_info_modal&name=' + encodeURIComponent(data.wear) + '&user_id=' + (uid || 0) + refreshed = (window.aksrtData && window.aksrtData.ajaxUrl) + ? window.aksrtData.ajaxUrl + '?action=aksrt_single_badge_info_modal&name=' + encodeURIComponent(data.wear) + '&user_id=' + (uid || 0) : null; } } catch (_) {} - try { - if (refreshed && window.AksrtRemoteModal && typeof window.AksrtRemoteModal.openRemote === 'function') { - window.AksrtRemoteModal.openRemote({ url: refreshed, className: 'modal-mini', mobileBottom: true }); - } else { - document.dispatchEvent(new CustomEvent('aksrt:remote-modal:close')); - } - } catch (_) { - try { document.dispatchEvent(new CustomEvent('aksrt:remote-modal:close')); } catch (__) {} + if (refreshed) { + window.AksrtModalRuntime.openRemote({ + url: refreshed, + className: 'modal-mini', + mobileBottom: true, + kind: 'badges', + cache: false, + }); + } else { + window.AksrtModalRuntime.close('badge-wear'); } }).catch(function () { toast('网络错误', 'error'); diff --git a/inc/helpers/aksrt-ajax-list.php b/inc/helpers/aksrt-ajax-list.php index 9013a057..1021888b 100644 --- a/inc/helpers/aksrt-ajax-list.php +++ b/inc/helpers/aksrt-ajax-list.php @@ -1,6 +1,6 @@ * } $args */ - function aksrt_get_remote_modal_link(array $args = []): string { + function aksrt_get_modal_link(array $args = []): string { $tag = sanitize_key((string) ($args['tag'] ?? 'a')); if (!in_array($tag, ['a', 'button', 'span'], true)) { $tag = 'a'; @@ -90,15 +90,15 @@ if (!function_exists('aksrt_get_remote_modal_link')) { $height = isset($args['height']) ? (int) $args['height'] : 0; $mobile = !isset($args['mobile_bottom']) || !empty($args['mobile_bottom']); - $attrs = ' class="' . esc_attr(trim($class . ' aksrt-remote-modal-link')) . '"'; - $attrs .= ' data-toggle="AksrtRemoteModal"'; - $attrs .= ' data-remote="' . esc_url($url) . '"'; - $attrs .= ' data-class="' . esc_attr($data_class) . '"'; + $attrs = ' class="' . esc_attr(trim($class . ' aksrt-modal-link')) . '"'; + $attrs .= ' data-aksrt-modal="remote"'; + $attrs .= ' data-aksrt-modal-url="' . esc_url($url) . '"'; + $attrs .= ' data-aksrt-modal-class="' . esc_attr($data_class) . '"'; if ($height > 0) { - $attrs .= ' data-height="' . esc_attr((string) $height) . '"'; + $attrs .= ' data-aksrt-modal-height="' . esc_attr((string) $height) . '"'; } if ($mobile) { - $attrs .= ' mobile-bottom="true"'; + $attrs .= ' data-aksrt-modal-mobile-bottom="true"'; } if ($tag === 'a') { $attrs .= ' href="' . esc_url($url !== '' ? $url : '#') . '"'; diff --git a/src/Content/VipServiceModal.php b/src/Content/VipServiceModal.php index c26b0d3e..b1433d68 100644 --- a/src/Content/VipServiceModal.php +++ b/src/Content/VipServiceModal.php @@ -152,7 +152,7 @@ trait VipServiceModal { } $html = '
'; - $html .= ''; + $html .= ''; $html .= '
  • ' . $avatar . '
  • '; $html .= '
  • ' . esc_html($name) . ''; if ($desc !== '') { @@ -232,7 +232,7 @@ trait VipServiceModal { } $html = '
    '; - $html .= ''; + $html .= ''; $html .= '
    • ' . $avatar . '
    • '; $html .= '
    • ' . esc_html($name) . ''; $html .= '
      ' diff --git a/src/Core/ScriptsPart1b.php b/src/Core/ScriptsPart1b.php index 1b9bd2ff..bbf76059 100644 --- a/src/Core/ScriptsPart1b.php +++ b/src/Core/ScriptsPart1b.php @@ -130,7 +130,7 @@ trait ScriptsPart1b { } // 4a-b. 全局补充样式(骨架屏、空状态、分页、加载指示器) - foreach (['skeleton', 'empty-state', 'pagination', 'ajax-spinner', 'remote-modal'] as $slug) { + foreach (['skeleton', 'empty-state', 'pagination', 'ajax-spinner', 'modal-runtime'] as $slug) { $css_path = get_template_directory() . '/assets/css/frontend/' . $slug . '.css'; if (file_exists($css_path)) { wp_enqueue_style( @@ -894,7 +894,7 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js'; wp_enqueue_script( 'aksrt-user-badges', $base_uri . '/assets/js/user-badges.js', - ['aksrt-script', 'aksrt-core-request', 'aksrt-core-modal-runtime', 'aksrt-core-remote-modal'], + ['aksrt-script', 'aksrt-core-request', 'aksrt-core-modal-runtime', 'aksrt-core-modal-triggers'], (string) filemtime($badges_js_path), true ); @@ -917,7 +917,7 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js'; wp_enqueue_script( 'aksrt-user-badges', $base_uri . '/assets/js/user-badges.js', - ['aksrt-script', 'aksrt-core-request', 'aksrt-core-modal-runtime', 'aksrt-core-remote-modal'], + ['aksrt-script', 'aksrt-core-request', 'aksrt-core-modal-runtime', 'aksrt-core-modal-triggers'], (string) filemtime($badges_js_path_global), true ); diff --git a/src/Core/ScriptsPart1c.php b/src/Core/ScriptsPart1c.php index a715b0b7..bd5dbff9 100644 --- a/src/Core/ScriptsPart1c.php +++ b/src/Core/ScriptsPart1c.php @@ -101,7 +101,7 @@ trait ScriptsPart1c { true ); - // Runtime infrastructure: tbquire / notyf / confirm / refreshModal / scrollTopTo / [data-*] declarative + // Runtime infrastructure: tbquire / notyf / confirm / scrollTopTo / [data-*] declarative $runtime_js = get_template_directory() . '/assets/js/aksrt-runtime.js'; if (file_exists($runtime_js)) { wp_enqueue_script( @@ -132,7 +132,7 @@ trait ScriptsPart1c { 'aksrt-core-modal-cache' => 'modal-cache.js', 'aksrt-core-modal-state' => 'modal-state.js', 'aksrt-core-modal-runtime' => 'modal-runtime.js', - 'aksrt-core-remote-modal' => 'remote-modal.js', + 'aksrt-core-modal-triggers' => 'modal-triggers.js', 'aksrt-core-pagination' => 'pagination.js', 'aksrt-core-archive' => 'archive.js', 'aksrt-core-footer-tabbar' => 'footer-tabbar.js', @@ -153,7 +153,7 @@ trait ScriptsPart1c { 'aksrt-core-modal-cache', 'aksrt-core-modal-state', ]); - } elseif ($handle === 'aksrt-core-remote-modal') { + } elseif ($handle === 'aksrt-core-modal-triggers') { $deps[] = 'aksrt-core-modal-runtime'; } wp_enqueue_script( diff --git a/src/Credits/Signin.php b/src/Credits/Signin.php index cfb6bff3..3b072b2d 100644 --- a/src/Credits/Signin.php +++ b/src/Credits/Signin.php @@ -276,7 +276,7 @@ class Signin { $html .= '
      '; $html .= ''; $html .= '' . esc_html__('签到领奖励', 'aksrt') . ''; - $html .= ''; + $html .= ''; $html .= '
      '; $html .= '
      '; diff --git a/src/User/Badges.php b/src/User/Badges.php index f3650169..308dd91a 100644 --- a/src/User/Badges.php +++ b/src/User/Badges.php @@ -30,7 +30,7 @@ class Badges { public function init(): void { add_action('rest_api_init', [$this, 'register_routes']); add_action('aksrt_badge_granted', [$this, 'on_badge_granted_notify'], 10, 3); - // HTML modal for remote-modal (zibll user_medal_info_modal). + // HTML modal for modal runtime (zibll user_medal_info_modal). add_action('wp_ajax_aksrt_user_badge_info_modal', [$this, 'ajax_info_modal']); add_action('wp_ajax_nopriv_aksrt_user_badge_info_modal', [$this, 'ajax_info_modal']); add_action('wp_ajax_aksrt_single_badge_info_modal', [$this, 'ajax_single_info_modal']); @@ -102,8 +102,8 @@ class Badges { * Badge list modal HTML (zibll zib_get_user_medal_info_modal parity). * * Head (avatar + owner + worn medal) + category-grouped medal card grid. - * Each card deep-links into the single badge modal via the remote-modal - * contract (data-toggle="AksrtRemoteModal" + data-remote). + * Each card deep-links into the single badge modal via the modal runtime + * contract (data-aksrt-modal="remote" + data-aksrt-modal-url). */ public static function render_info_modal_html(int $user_id): string { $user = get_userdata($user_id); @@ -134,7 +134,7 @@ class Badges { } $html = '
      '; - $html .= ''; + $html .= ''; $html .= '
      '; $html .= ''; $html .= '
      ' . esc_html(sprintf(__('%s的徽章', 'aksrt'), $display)) . '' . $wear_slot . '
      '; @@ -164,7 +164,7 @@ class Badges { if (!$is_self && $current > 0) { $my_url = add_query_arg(['action' => 'aksrt_user_badge_info_modal', 'id' => $current], admin_url('admin-ajax.php')); $html .= ''; } @@ -204,7 +204,7 @@ class Badges { admin_url('admin-ajax.php') ); - return '' . $card . ''; + return '' . $card . ''; } /** @@ -244,7 +244,7 @@ class Badges { } $html = '
      '; - $html .= ''; + $html .= ''; $html .= '
      '; $html .= '
      '; $html .= '
      ' . $icon_html; @@ -263,7 +263,7 @@ class Badges { // Back to the full badge list modal. if ($user_id > 0) { $back_url = add_query_arg(['action' => 'aksrt_user_badge_info_modal', 'id' => $user_id], admin_url('admin-ajax.php')); - $html .= ' ' . esc_html__('返回徽章列表', 'aksrt') . ''; + $html .= ' ' . esc_html__('返回徽章列表', 'aksrt') . ''; } $html .= '
      '; @@ -280,7 +280,7 @@ class Badges { if ($text === null) { $text = __('徽章明细', 'aksrt'); } - if (!function_exists('aksrt_get_remote_modal_link')) { + if (!function_exists('aksrt_get_modal_link')) { return ''; } $url = add_query_arg( @@ -290,7 +290,7 @@ class Badges { ], admin_url('admin-ajax.php') ); - return aksrt_get_remote_modal_link([ + return aksrt_get_modal_link([ 'tag' => 'a', 'class' => trim('user-medal-info ' . $class), 'text' => $text, diff --git a/templates/parts/user/author-hero.php b/templates/parts/user/author-hero.php index 9ac275a2..fcd8f509 100644 --- a/templates/parts/user/author-hero.php +++ b/templates/parts/user/author-hero.php @@ -109,7 +109,7 @@ $pm_url = ($pm_enabled && $messages_url !== '') Date: Wed, 5 Aug 2026 00:16:39 +0800 Subject: [PATCH 2/2] refactor(badges): use modal REST routes Co-Authored-By: Claude Fable 5 --- assets/js/user-badges.js | 4 +- src/User/Badges.php | 88 ++++++++++++++++++++++------------------ 2 files changed, 51 insertions(+), 41 deletions(-) diff --git a/assets/js/user-badges.js b/assets/js/user-badges.js index 24df0b37..04ed5e8d 100644 --- a/assets/js/user-badges.js +++ b/assets/js/user-badges.js @@ -347,8 +347,8 @@ var data = r.json && r.json.data; var isSelf = data && data.is_self; if (isSelf && data.wear) { - refreshed = (window.aksrtData && window.aksrtData.ajaxUrl) - ? window.aksrtData.ajaxUrl + '?action=aksrt_single_badge_info_modal&name=' + encodeURIComponent(data.wear) + '&user_id=' + (uid || 0) + refreshed = uid > 0 && data.wear + ? window.AksrtRequest.url('/users/' + uid + '/badges/' + encodeURIComponent(data.wear) + '/modal') : null; } } catch (_) {} diff --git a/src/User/Badges.php b/src/User/Badges.php index 308dd91a..ff69551e 100644 --- a/src/User/Badges.php +++ b/src/User/Badges.php @@ -30,11 +30,6 @@ class Badges { public function init(): void { add_action('rest_api_init', [$this, 'register_routes']); add_action('aksrt_badge_granted', [$this, 'on_badge_granted_notify'], 10, 3); - // HTML modal for modal runtime (zibll user_medal_info_modal). - add_action('wp_ajax_aksrt_user_badge_info_modal', [$this, 'ajax_info_modal']); - add_action('wp_ajax_nopriv_aksrt_user_badge_info_modal', [$this, 'ajax_info_modal']); - add_action('wp_ajax_aksrt_single_badge_info_modal', [$this, 'ajax_single_info_modal']); - add_action('wp_ajax_nopriv_aksrt_single_badge_info_modal', [$this, 'ajax_single_info_modal']); } /** @@ -71,39 +66,32 @@ class Badges { } /** - * AJAX: full badge list modal HTML. + * REST response for the badge list modal. */ - public function ajax_info_modal(): void { - $user_id = max(0, (int) ($_REQUEST['id'] ?? $_REQUEST['user_id'] ?? 0)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - if ($user_id <= 0) { - $user_id = get_current_user_id(); - } + public function rest_modal_list(\WP_REST_Request $request) { + $user_id = absint($request->get_param('id')); if ($user_id <= 0 || !get_userdata($user_id)) { - wp_send_json_error(['message' => __('用户不存在', 'aksrt')], 404); + return new \WP_Error('aksrt_badge_user_missing', __('用户不存在', 'aksrt'), ['status' => 404]); } - $html = self::render_info_modal_html($user_id); - wp_send_json_success(['html' => $html]); + return new \WP_REST_Response(['html' => self::render_info_modal_html($user_id)], 200); } /** - * AJAX: single badge detail modal. + * REST response for a single badge modal. */ - public function ajax_single_info_modal(): void { - $slug = sanitize_key((string) ($_REQUEST['name'] ?? $_REQUEST['slug'] ?? '')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $user_id = max(0, (int) ($_REQUEST['user_id'] ?? $_REQUEST['id'] ?? 0)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + public function rest_modal_single(\WP_REST_Request $request) { + $slug = sanitize_key((string) $request->get_param('slug')); + $user_id = absint($request->get_param('id')); if ($slug === '') { - wp_send_json_error(['message' => __('徽章不存在', 'aksrt')], 404); + return new \WP_Error('aksrt_badge_missing', __('徽章不存在', 'aksrt'), ['status' => 404]); } - $html = self::render_single_modal_html($slug, $user_id); - wp_send_json_success(['html' => $html]); + return new \WP_REST_Response(['html' => self::render_single_modal_html($slug, $user_id)], 200); } /** - * Badge list modal HTML (zibll zib_get_user_medal_info_modal parity). + * Badge list modal HTML. * - * Head (avatar + owner + worn medal) + category-grouped medal card grid. - * Each card deep-links into the single badge modal via the modal runtime - * contract (data-aksrt-modal="remote" + data-aksrt-modal-url). + * Head (avatar + owner + worn medal) and category-grouped medal card grid. */ public static function render_info_modal_html(int $user_id): string { $user = get_userdata($user_id); @@ -162,7 +150,7 @@ class Badges { // Footer: quick jump to my own badges while browsing someone else (zibll). if (!$is_self && $current > 0) { - $my_url = add_query_arg(['action' => 'aksrt_user_badge_info_modal', 'id' => $current], admin_url('admin-ajax.php')); + $my_url = rest_url('aksrt/v1/users/' . $current . '/badges/modal'); $html .= ''; @@ -199,10 +187,7 @@ class Badges { } $card .= '
      '; - $url = add_query_arg( - ['action' => 'aksrt_single_badge_info_modal', 'name' => $slug, 'user_id' => $user_id], - admin_url('admin-ajax.php') - ); + $url = rest_url('aksrt/v1/users/' . $user_id . '/badges/' . rawurlencode($slug) . '/modal'); return '' . $card . ''; } @@ -262,7 +247,7 @@ class Badges { // Back to the full badge list modal. if ($user_id > 0) { - $back_url = add_query_arg(['action' => 'aksrt_user_badge_info_modal', 'id' => $user_id], admin_url('admin-ajax.php')); + $back_url = rest_url('aksrt/v1/users/' . $user_id . '/badges/modal'); $html .= ' ' . esc_html__('返回徽章列表', 'aksrt') . ''; } @@ -271,7 +256,7 @@ class Badges { } /** - * Remote-modal trigger for a user's badge list (zibll zib_get_user_medal_info_link). + * Declarative modal runtime trigger for a user's badge list. */ public static function get_info_link(int $user_id, string $class = '', ?string $text = null): string { if ($user_id <= 0 || !self::enabled()) { @@ -283,13 +268,7 @@ class Badges { if (!function_exists('aksrt_get_modal_link')) { return ''; } - $url = add_query_arg( - [ - 'action' => 'aksrt_user_badge_info_modal', - 'id' => $user_id, - ], - admin_url('admin-ajax.php') - ); + $url = rest_url('aksrt/v1/users/' . $user_id . '/badges/modal'); return aksrt_get_modal_link([ 'tag' => 'a', 'class' => trim('user-medal-info ' . $class), @@ -315,6 +294,37 @@ class Badges { ], ]); + register_rest_route('aksrt/v1', '/users/(?P\d+)/badges/modal', [ + 'methods' => 'GET', + 'callback' => [$this, 'rest_modal_list'], + 'permission_callback' => '__return_true', + 'args' => [ + 'id' => [ + 'type' => 'integer', + 'required' => true, + 'sanitize_callback' => 'absint', + ], + ], + ]); + + register_rest_route('aksrt/v1', '/users/(?P\d+)/badges/(?P[a-z0-9_-]+)/modal', [ + 'methods' => 'GET', + 'callback' => [$this, 'rest_modal_single'], + 'permission_callback' => '__return_true', + 'args' => [ + 'id' => [ + 'type' => 'integer', + 'required' => true, + 'sanitize_callback' => 'absint', + ], + 'slug' => [ + 'type' => 'string', + 'required' => true, + 'sanitize_callback' => 'sanitize_key', + ], + ], + ]); + register_rest_route('aksrt/v1', '/users/me/badges/wear', [ 'methods' => 'POST', 'callback' => [$this, 'rest_wear'],