diff --git a/assets/css/frontend/remote-modal.css b/assets/css/frontend/remote-modal.css
index 044a33e5..92a2fdd2 100644
--- a/assets/css/frontend/remote-modal.css
+++ b/assets/css/frontend/remote-modal.css
@@ -71,6 +71,20 @@ body.aksrt-remote-modal-open {
transform: translateY(0);
}
+.aksrt-remote-modal__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 16px 18px 0;
+}
+
+.aksrt-remote-modal__title {
+ margin: 0;
+ font-size: 17px;
+ line-height: 1.35;
+}
+
.aksrt-remote-modal__content {
padding: 18px 18px 16px;
}
@@ -83,6 +97,49 @@ body.aksrt-remote-modal-open {
font-size: 14px;
}
+.aksrt-remote-modal__skeleton {
+ display: grid;
+ gap: 12px;
+ padding: 8px 2px;
+}
+
+.aksrt-remote-modal__skeleton-line,
+.aksrt-remote-modal__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));
+ background-size: 200% 100%;
+ animation: aksrt-modal-skeleton 1.2s ease-in-out infinite;
+}
+
+.aksrt-remote-modal__skeleton-line {
+ width: 72%;
+ height: 12px;
+}
+
+.aksrt-remote-modal__skeleton-line.is-wide {
+ width: 92%;
+ height: 18px;
+}
+
+.aksrt-remote-modal__skeleton-line.is-short {
+ width: 44%;
+}
+
+.aksrt-remote-modal__skeleton-block {
+ width: 100%;
+ min-height: 84px;
+}
+
+.aksrt-remote-modal__retry {
+ cursor: pointer;
+}
+
+@keyframes aksrt-modal-skeleton {
+ 0% { background-position: 100% 0; }
+ 100% { background-position: -100% 0; }
+}
+
.aksrt-remote-modal .touch.border-title,
.aksrt-remote-modal .border-title {
font-size: 16px;
diff --git a/assets/css/frontend/vip-modal.css b/assets/css/frontend/vip-modal.css
index 8349818e..bd68c1ce 100644
--- a/assets/css/frontend/vip-modal.css
+++ b/assets/css/frontend/vip-modal.css
@@ -30,6 +30,23 @@
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.22);
}
+/* VIP now uses the shared modal runtime host. */
+.aksrt-remote-modal.aksrt-vip-modal-shell {
+ z-index: 1200;
+}
+
+.aksrt-remote-modal.aksrt-vip-modal-shell .aksrt-remote-modal__dialog {
+ width: min(560px, 100%);
+ max-height: min(90vh, 760px);
+ background: var(--main-bg-color, #fff);
+ border-radius: 12px;
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.22);
+}
+
+.aksrt-remote-modal.aksrt-vip-modal-shell .aksrt-remote-modal__content {
+ padding: 0;
+}
+
.payvip-modal {
position: relative;
padding: 18px 16px 16px;
diff --git a/assets/js/aksrt-runtime.js b/assets/js/aksrt-runtime.js
index 0360c8b7..349575ba 100644
--- a/assets/js/aksrt-runtime.js
+++ b/assets/js/aksrt-runtime.js
@@ -317,120 +317,60 @@
// §10.10 — refreshModal: remote content modal
// ══════════════════════════════════════════════════════════════
- var _refreshModalEl = null;
-
- function getRefreshModalEl() {
- if (_refreshModalEl && _refreshModalEl.parentNode) return _refreshModalEl;
- _refreshModalEl = document.createElement('div');
- _refreshModalEl.className = 'aksrt-refresh-modal';
- _refreshModalEl.innerHTML =
- '
' +
- '';
- document.body.appendChild(_refreshModalEl);
-
- _refreshModalEl.querySelector('.aksrt-refresh-modal__close').addEventListener('click', closeRefreshModal);
- _refreshModalEl.querySelector('.aksrt-refresh-modal__overlay').addEventListener('click', function (e) {
- if (e.target === e.currentTarget) closeRefreshModal();
- });
-
- return _refreshModalEl;
- }
-
function closeRefreshModal() {
- if (!_refreshModalEl) return;
- _refreshModalEl.classList.remove('is-open', 'from-bottom');
- document.body.classList.remove('modal-open');
- setTimeout(function () {
- var body = _refreshModalEl.querySelector('.aksrt-refresh-modal__body');
- if (body) body.innerHTML = '';
- }, 300);
+ if (window.AksrtModalRuntime && typeof window.AksrtModalRuntime.close === 'function') {
+ window.AksrtModalRuntime.close('refresh-modal');
+ }
}
aksrt.refreshModal = function (opts) {
opts = opts || {};
- var modal = getRefreshModalEl();
- var body = modal.querySelector('.aksrt-refresh-modal__body');
- var title = modal.querySelector('.aksrt-refresh-modal__title');
+ var runtime = window.AksrtModalRuntime;
+ if (!runtime || typeof runtime.open !== 'function' || typeof runtime.openRemote !== 'function') {
+ return Promise.reject(new Error('modal runtime unavailable'));
+ }
- if (opts.title) title.textContent = opts.title;
- else title.textContent = '';
+ 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',
+ };
- var isMobile = window.innerWidth < 768;
- modal.classList.toggle('from-bottom', !!(opts.mobile_from_bottom && isMobile));
-
- if (opts.class) modal.querySelector('.aksrt-refresh-modal__dialog').className = 'aksrt-refresh-modal__dialog ' + opts.class;
-
- body.innerHTML = '';
- aksrt.loading(body, true);
- modal.classList.add('is-open');
- document.body.classList.add('modal-open');
-
- if (opts.content) {
- aksrt.loading(body, false);
- body.innerHTML = opts.content;
- if (window.AksrtFrontend) AksrtFrontend.rehydrate(body, 'modal');
- return Promise.resolve(modal);
+ if (opts.content !== undefined) {
+ return Promise.resolve(runtime.open(Object.assign({}, base, { html: opts.content })))
+ .catch(function () { return null; });
}
if (opts.remote) {
- return fetch(opts.remote, { credentials: 'same-origin' })
- .then(function (r) { return r.text(); })
- .then(function (html) {
- aksrt.loading(body, false);
- body.innerHTML = html;
- if (window.AksrtFrontend) AksrtFrontend.rehydrate(body, 'modal');
- return modal;
- })
- .catch(function (err) {
- aksrt.loading(body, false);
- body.innerHTML = '' + escapeHtml(err.message) + '
';
- return modal;
- });
+ return runtime.openRemote(Object.assign({}, base, {
+ url: opts.remote,
+ method: opts.method || 'GET',
+ }));
}
if (opts.action) {
- var ajaxUrl = (window.aksrtData && aksrtData.ajaxUrl) || '/wp-admin/admin-ajax.php';
+ var ajaxUrl = (window.aksrtData && window.aksrtData.ajaxUrl) || '/wp-admin/admin-ajax.php';
var params = new URLSearchParams();
params.set('action', opts.action);
- if (window.aksrtData && aksrtData.nonce) params.set('nonce', aksrtData.nonce);
+ if (window.aksrtData && window.aksrtData.nonce) params.set('nonce', window.aksrtData.nonce);
if (opts.data) {
- Object.keys(opts.data).forEach(function (k) { params.set(k, opts.data[k]); });
+ Object.keys(opts.data).forEach(function (key) { params.set(key, opts.data[key]); });
}
-
- return fetch(ajaxUrl, {
+ return runtime.openRemote(Object.assign({}, base, {
+ url: ajaxUrl,
method: 'POST',
+ cache: false,
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
- credentials: 'same-origin',
- body: params.toString()
- })
- .then(function (r) { return r.json(); })
- .then(function (json) {
- aksrt.loading(body, false);
- if (json && json.data && json.data.html) {
- body.innerHTML = json.data.html;
- } else if (json && json.data && json.data.show_modal) {
- body.innerHTML = json.data.show_modal;
- } else {
- body.innerHTML = '' + escapeHtml(json.data && json.data.message || '') + '
';
- }
- if (window.AksrtFrontend) AksrtFrontend.rehydrate(body, 'modal');
- return modal;
- })
- .catch(function (err) {
- aksrt.loading(body, false);
- body.innerHTML = '' + escapeHtml(err.message) + '
';
- return modal;
- });
+ body: params.toString(),
+ }));
}
- aksrt.loading(body, false);
- return Promise.resolve(modal);
+ return Promise.resolve(runtime.open(base));
};
aksrt.refreshModal.close = closeRefreshModal;
diff --git a/assets/js/core/modal-cache.js b/assets/js/core/modal-cache.js
new file mode 100644
index 00000000..c7fe8eaf
--- /dev/null
+++ b/assets/js/core/modal-cache.js
@@ -0,0 +1,150 @@
+/**
+ * User-scoped in-memory modal cache with short TTLs and request deduplication.
+ * Private modal payloads are never persisted to browser storage.
+ *
+ * @package AKSRT
+ */
+(function (window, document) {
+ "use strict";
+
+ var DEFAULT_TTL = 15000;
+ var entries = Object.create(null);
+ var flights = Object.create(null);
+
+ function scope() {
+ var config = window.aksrtData || window.aksrt || {};
+ var userId = parseInt(config.userId || 0, 10) || 0;
+ return (config.isLoggedIn && userId > 0 ? "user:" + userId : "guest") + ":v1";
+ }
+
+ function token(key) {
+ return scope() + "::" + String(key || "");
+ }
+
+ function read(key) {
+ var id = token(key);
+ var entry = entries[id];
+ if (!entry) return { hit: false, value: undefined };
+ if (entry.expiresAt <= Date.now()) {
+ delete entries[id];
+ return { hit: false, value: undefined };
+ }
+ return { hit: true, value: entry.value };
+ }
+
+ function tagsOf(value) {
+ return Array.isArray(value) ? value.slice() : (value ? [value] : []);
+ }
+
+ function set(key, value, options) {
+ options = options || {};
+ var ttl = Number(options.ttl || DEFAULT_TTL);
+ entries[token(key)] = {
+ value: value,
+ expiresAt: Date.now() + Math.max(0, ttl),
+ tags: tagsOf(options.tags),
+ };
+ return value;
+ }
+
+ function matchesTags(entry, wanted) {
+ if (!wanted.length) return true;
+ return wanted.some(function (tag) { return entry.tags.indexOf(tag) >= 0; });
+ }
+
+ function invalidate(tags) {
+ var wanted = Array.isArray(tags) ? tags : (tags ? [tags] : []);
+ var removed = 0;
+ Object.keys(entries).forEach(function (id) {
+ if (!wanted.length || matchesTags(entries[id], wanted)) {
+ delete entries[id];
+ removed += 1;
+ }
+ });
+ Object.keys(flights).forEach(function (id) {
+ var flight = flights[id];
+ if (!wanted.length || matchesTags(flight, wanted)) {
+ flight.cancelled = true;
+ delete flights[id];
+ if (typeof flight.cancel === "function") flight.cancel();
+ }
+ });
+ return removed;
+ }
+
+ function clear() {
+ entries = Object.create(null);
+ Object.keys(flights).forEach(function (id) {
+ var flight = flights[id];
+ flight.cancelled = true;
+ if (typeof flight.cancel === "function") flight.cancel();
+ });
+ flights = Object.create(null);
+ }
+
+ function acquire(key, loader, options) {
+ options = options || {};
+ var id = token(key);
+ var cached = options.skipCache ? { hit: false } : read(key);
+
+ if (cached.hit) {
+ return {
+ cached: true,
+ promise: Promise.resolve(cached.value),
+ release: function () {},
+ };
+ }
+
+ var flight = flights[id];
+ if (!flight) {
+ flight = {
+ refs: 0,
+ settled: false,
+ cancelled: false,
+ tags: tagsOf(options.tags),
+ cancel: options.cancel,
+ };
+ flight.promise = Promise.resolve().then(loader).then(function (value) {
+ flight.settled = true;
+ if (!flight.cancelled && options.store !== false) set(key, value, options);
+ if (flights[id] === flight) delete flights[id];
+ return value;
+ }, function (error) {
+ flight.settled = true;
+ if (flights[id] === flight) delete flights[id];
+ throw error;
+ });
+ flights[id] = flight;
+ }
+
+ flight.refs += 1;
+ var released = false;
+ return {
+ cached: false,
+ promise: flight.promise,
+ release: function () {
+ if (released) return;
+ released = true;
+ flight.refs = Math.max(0, flight.refs - 1);
+ if (flight.refs === 0 && !flight.settled) {
+ flight.cancelled = true;
+ if (flights[id] === flight) delete flights[id];
+ if (typeof flight.cancel === "function") flight.cancel();
+ }
+ },
+ };
+ }
+
+ ["aksrt:auth:logout", "aksrt:account-switch", "aksrt:logout", "aksrt:user:logout", "aksrt:auth:changed", "wp_logout"].forEach(function (eventName) {
+ document.addEventListener(eventName, clear);
+ });
+
+ window.AksrtModalCache = {
+ get: function (key) { return read(key); },
+ set: set,
+ acquire: acquire,
+ invalidate: invalidate,
+ clear: clear,
+ scope: scope,
+ };
+})(window, document);
diff --git a/assets/js/core/modal-runtime.js b/assets/js/core/modal-runtime.js
new file mode 100644
index 00000000..c5150ccd
--- /dev/null
+++ b/assets/js/core/modal-runtime.js
@@ -0,0 +1,422 @@
+/**
+ * Canonical frontend modal runtime.
+ *
+ * Keeps the existing remote-modal shell while adding cancellable loaders,
+ * short in-memory caching, request deduplication, retryable errors, focus
+ * restoration, and modal-scoped rehydration.
+ *
+ * @package AKSRT
+ */
+(function (window, document) {
+ "use strict";
+
+ var ROOT_ID = "aksrt-remote-modal-root";
+ var active = null;
+ var sequence = 0;
+ var loaders = Object.create(null);
+
+ function isMobile() {
+ return window.matchMedia && window.matchMedia("(max-width: 768px)").matches;
+ }
+
+ function ensureRoot() {
+ var root = document.getElementById(ROOT_ID);
+ if (root) return root;
+ root = document.createElement("div");
+ root.id = ROOT_ID;
+ root.className = "aksrt-remote-modal-host";
+ document.body.appendChild(root);
+ return root;
+ }
+
+ function escapeHtml(value) {
+ return String(value == null ? "" : value)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/\"/g, """)
+ .replace(/'/g, "'");
+ }
+
+ function lockScroll(lock) {
+ document.documentElement.classList.toggle("aksrt-remote-modal-open", !!lock);
+ document.body.classList.toggle("aksrt-remote-modal-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");
+ }
+
+ function telemetryMark(session, event) {
+ var telemetry = window.AksrtModalTelemetry;
+ if (!session || !session.telemetryId || !telemetry || typeof telemetry.mark !== "function") return;
+ telemetry.mark(session.telemetryId, event);
+ }
+
+ function setState(session, status, error) {
+ session.state.status = status;
+ session.state.error = error || null;
+ session.state.updatedAt = Date.now();
+ if (session.shell) session.shell.setAttribute("data-aksrt-modal-state", status);
+ if (typeof session.options.onState === "function") {
+ session.options.onState(status, session);
+ }
+ }
+
+ function focusable(shell) {
+ return shell.querySelectorAll("button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])");
+ }
+
+ 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";
+ }
+
+ function bindChrome(session) {
+ var shell = session.shell;
+ shell.addEventListener("click", function (event) {
+ var closeButton = event.target.closest(closeSelector());
+ if (closeButton) {
+ event.preventDefault();
+ close("control");
+ return;
+ }
+ if (event.target === shell || event.target.classList.contains("aksrt-remote-modal__overlay")) {
+ close("overlay");
+ return;
+ }
+ var retry = event.target.closest("[data-aksrt-remote-modal-retry], [data-aksrt-modal-retry]");
+ if (retry) {
+ event.preventDefault();
+ refresh();
+ }
+ });
+
+ shell.addEventListener("keydown", function (event) {
+ if (event.key === "Escape") {
+ event.preventDefault();
+ close("escape");
+ return;
+ }
+ if (event.key !== "Tab") return;
+ var nodes = focusable(shell);
+ if (!nodes.length) {
+ event.preventDefault();
+ shell.focus();
+ return;
+ }
+ var first = nodes[0];
+ var last = nodes[nodes.length - 1];
+ if (event.shiftKey && document.activeElement === first) {
+ event.preventDefault();
+ last.focus();
+ } else if (!event.shiftKey && document.activeElement === last) {
+ event.preventDefault();
+ first.focus();
+ }
+ });
+ }
+
+ function createSession(options) {
+ options = options || {};
+ var root = ensureRoot();
+ var shell = document.createElement("div");
+ var bottom = options.bottom === true || (options.mobileBottom !== false && isMobile());
+ var heightStyle = options.height ? "min-height:" + parseInt(options.height, 10) + "px;" : "";
+ var initialHtml = options.html !== undefined ? options.html : skeleton(options);
+ var titleId = "aksrt-modal-title-" + String(sequence + 1);
+ var titleHtml = options.title
+ ? ''
+ : "";
+
+ shell.className =
+ "aksrt-remote-modal" +
+ (bottom ? " aksrt-remote-modal--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 + "
";
+
+ var session = {
+ id: String(++sequence),
+ shell: shell,
+ content: shell.querySelector(".aksrt-remote-modal__content"),
+ options: options,
+ state: window.AksrtModalState && typeof window.AksrtModalState.create === "function"
+ ? window.AksrtModalState.create(options.kind || "generic")
+ : { status: "idle", error: null },
+ focusReturn: document.activeElement,
+ controller: null,
+ flight: null,
+ closed: false,
+ telemetryId: telemetryStart(options.telemetry === false ? false : options.kind),
+ };
+
+ root.appendChild(shell);
+ bindChrome(session);
+ active = session;
+ lockScroll(true);
+ void shell.offsetWidth;
+ shell.classList.add("is-open");
+ setState(session, options.html !== undefined ? "ready" : "loading");
+ telemetryMark(session, "shell");
+
+ var nodes = focusable(shell);
+ try {
+ (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 },
+ }));
+
+ if (options.html !== undefined) {
+ rehydrate(session);
+ telemetryMark(session, "first-content");
+ telemetryMark(session, "ready");
+ if (typeof options.onContent === "function") options.onContent(options.html, session);
+ }
+
+ return session;
+ }
+
+ function rehydrate(session) {
+ if (!session || !session.content) return;
+ if (window.AksrtFrontend && typeof window.AksrtFrontend.rehydrate === "function") {
+ window.AksrtFrontend.rehydrate(session.content, "modal");
+ }
+ }
+
+ function abortSessionRequest(session) {
+ if (!session) return;
+ if (session.flight && typeof session.flight.release === "function") session.flight.release();
+ session.flight = null;
+ if (session.controller) {
+ try { session.controller.abort(); } catch (error) {}
+ }
+ session.controller = null;
+ }
+
+ function close(reason, preserveRequest) {
+ if (!active) return;
+ var session = active;
+ active = null;
+ session.closed = true;
+ 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 },
+ }));
+
+ var node = session.shell;
+ window.setTimeout(function () {
+ if (node && node.parentNode) node.parentNode.removeChild(node);
+ var target = session.focusReturn;
+ if (!preserveRequest && !active && target && target !== node && document.contains(target) && typeof target.focus === "function") {
+ try { target.focus(); } catch (error) {}
+ }
+ }, 220);
+ }
+
+ function responseHtml(result) {
+ if (!result) return "";
+ var json = result.json;
+ 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 : "";
+ }
+
+ function errorMessage(error) {
+ if (error && error.code === "unauthorized") return "请先登录";
+ return error && error.message ? String(error.message) : "加载失败,请重试";
+ }
+
+ function renderError(session, error) {
+ if (!session || session.closed || active !== session) return;
+ var unauthorized = error && error.code === "unauthorized";
+ setState(session, unauthorized ? "unauthorized" : "error", error);
+ telemetryMark(session, unauthorized ? "unauthorized" : "error");
+ session.content.innerHTML =
+ '提示
' +
+ '' + escapeHtml(errorMessage(error)) +
+ '
' +
+ '' +
+ '
';
+ rehydrate(session);
+ if (typeof session.options.onError === "function") session.options.onError(error, session);
+ }
+
+ function renderResult(session, result) {
+ if (!session || session.closed || active !== session) return;
+ var html = responseHtml(result);
+ if (!html) {
+ setState(session, "empty");
+ telemetryMark(session, "first-content");
+ telemetryMark(session, "ready");
+ session.content.innerHTML = '暂无内容
';
+ return;
+ }
+ session.content.innerHTML = html;
+ setState(session, "ready");
+ telemetryMark(session, "first-content");
+ rehydrate(session);
+ telemetryMark(session, "ready");
+ if (typeof session.options.onContent === "function") session.options.onContent(html, session);
+ }
+
+ function load(session, loadOptions) {
+ loadOptions = loadOptions || {};
+ if (!session || session.closed || !session.options.url) return Promise.resolve(session && session.shell);
+ abortSessionRequest(session);
+ setState(session, "loading");
+ session.content.innerHTML = skeleton(session.options);
+ session.controller = window.AbortController ? new window.AbortController() : null;
+
+ var requestOptions = {
+ method: session.options.method || "GET",
+ headers: session.options.headers || {},
+ body: session.options.body,
+ query: session.options.query,
+ signal: session.controller ? session.controller.signal : undefined,
+ };
+ 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 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);
+ var cache = window.AksrtModalCache;
+ var acquireOptions = {
+ ttl: Number(session.options.cacheTtl || 15000),
+ tags: session.options.cacheTags || session.options.tags || [],
+ skipCache: !!loadOptions.force || !cacheable,
+ store: cacheable,
+ cancel: function () {
+ if (session.controller) {
+ try { session.controller.abort(); } catch (error) {}
+ }
+ },
+ };
+
+ session.flight = cacheable && cache && typeof cache.acquire === "function"
+ ? cache.acquire(cacheKey, request, acquireOptions)
+ : { promise: request(), release: function () {} };
+
+ return session.flight.promise.then(function (result) {
+ if (!session.closed && active === session) renderResult(session, result);
+ return session.shell;
+ }).catch(function (error) {
+ var aborted = error && (error.name === "AbortError" || error.code === "ABORT_ERR");
+ if (!aborted && !session.closed && active === session) renderError(session, error);
+ return session.shell;
+ }).then(function (shell) {
+ if (session.flight && typeof session.flight.release === "function") session.flight.release();
+ session.flight = null;
+ return shell;
+ });
+ }
+
+ function open(options) {
+ options = options || {};
+ close("replace");
+ var session = createSession(options);
+ return session.shell;
+ }
+
+ 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);
+ return method + ":" + requestUrl;
+ }
+
+ function openRemote(options) {
+ options = Object.assign({}, options || {});
+ if (!options.url) return Promise.reject(new Error("missing url"));
+ var sameRequest = active && active.flight && sessionKey(active.options) === sessionKey(options);
+ close("replace", !!sameRequest);
+ var session = createSession(options);
+ return load(session).then(function () { return session.shell; });
+ }
+
+ function refresh(options) {
+ if (!active || !active.options.url) return Promise.resolve(null);
+ if (options && typeof options === "object") active.options = Object.assign({}, active.options, options);
+ return load(active, { force: true });
+ }
+
+ function registerLoader(kind, loader) {
+ if (!kind || typeof loader !== "function") return;
+ loaders[String(kind)] = loader;
+ }
+
+ function openLoader(kind, context) {
+ var loader = loaders[String(kind)];
+ if (!loader) return Promise.reject(new Error("missing modal loader: " + kind));
+ return openRemote(loader(context || {}));
+ }
+
+ function invalidate(tags) {
+ if (window.AksrtModalCache && typeof window.AksrtModalCache.invalidate === "function") {
+ return window.AksrtModalCache.invalidate(tags);
+ }
+ return 0;
+ }
+
+ window.AksrtModalRuntime = {
+ open: open,
+ openRemote: openRemote,
+ close: close,
+ refresh: refresh,
+ registerLoader: registerLoader,
+ openLoader: openLoader,
+ invalidate: invalidate,
+ getActive: function () { return active ? active.shell : null; },
+ getState: function () { return active ? active.state : null; },
+ };
+
+ document.dispatchEvent(new CustomEvent("aksrt:modal-runtime:ready"));
+})(window, document);
diff --git a/assets/js/core/modal-state.js b/assets/js/core/modal-state.js
new file mode 100644
index 00000000..b324919a
--- /dev/null
+++ b/assets/js/core/modal-state.js
@@ -0,0 +1,34 @@
+/**
+ * Shared modal state vocabulary.
+ *
+ * @package AKSRT
+ */
+(function (window) {
+ "use strict";
+
+ var statuses = {
+ idle: "idle",
+ loading: "loading",
+ ready: "ready",
+ empty: "empty",
+ submitting: "submitting",
+ success: "success",
+ error: "error",
+ unauthorized: "unauthorized",
+ stale: "stale",
+ };
+
+ function create(kind) {
+ return {
+ kind: String(kind || "generic"),
+ status: statuses.idle,
+ updatedAt: 0,
+ error: null,
+ };
+ }
+
+ window.AksrtModalState = {
+ statuses: statuses,
+ create: create,
+ };
+})(window);
diff --git a/assets/js/core/remote-modal.js b/assets/js/core/remote-modal.js
index e07802ce..e115c9b5 100644
--- a/assets/js/core/remote-modal.js
+++ b/assets/js/core/remote-modal.js
@@ -1,232 +1,77 @@
/**
- * AKSRT Remote Modal — zibll RefreshModal parity.
+ * AKSRT Remote Modal compatibility bridge.
*
- * Open a center/bottom modal with static HTML or remote fetch.
- * Contract:
- * - data-toggle="AksrtRemoteModal" | data-aksrt-remote-modal
- * - data-remote / href → fetch HTML
- * - data-class → dialog class (modal-mini full-sm …)
- * - data-height → min-height hint
- * - mobile-bottom="true" → bottom sheet on narrow viewports
+ * 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";
- var ROOT_ID = "aksrt-remote-modal-root";
- var active = null;
-
- function isMobile() {
- return window.matchMedia && window.matchMedia("(max-width: 768px)").matches;
+ function runtime() {
+ return window.AksrtModalRuntime || null;
}
- function ensureRoot() {
- var root = document.getElementById(ROOT_ID);
- if (root) return root;
- root = document.createElement("div");
- root.id = ROOT_ID;
- root.className = "aksrt-remote-modal-host";
- document.body.appendChild(root);
- return root;
- }
+ function fromTrigger(element) {
+ var modalRuntime = runtime();
+ if (!modalRuntime || !element) return Promise.reject(new Error("modal runtime unavailable"));
- function restHeaders() {
- var h = { Accept: "text/html, application/json" };
- if (window.aksrtData && aksrtData.restNonce) {
- h["X-WP-Nonce"] = aksrtData.restNonce;
- }
- return h;
- }
+ var url = element.getAttribute("data-remote") ||
+ element.getAttribute("data-aksrt-remote") ||
+ element.getAttribute("href") || "";
+ if (!url || url === "#") return Promise.resolve(null);
- function lockScroll(lock) {
- document.documentElement.classList.toggle("aksrt-remote-modal-open", !!lock);
- document.body.classList.toggle("aksrt-remote-modal-open", !!lock);
- }
+ 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);
- function close() {
- if (!active) return;
- active.classList.remove("is-open");
- lockScroll(false);
- var node = active;
- active = null;
- window.setTimeout(function () {
- if (node && node.parentNode) node.parentNode.removeChild(node);
- }, 220);
- document.dispatchEvent(new CustomEvent("aksrt:remote-modal:close"));
- }
-
- function bindChrome(shell) {
- shell.addEventListener("click", function (e) {
- if (e.target.closest("[data-aksrt-remote-modal-close], [data-dismiss='modal'], .aksrt-remote-modal__close")) {
- e.preventDefault();
- close();
- return;
- }
- if (e.target === shell || e.target.classList.contains("aksrt-remote-modal__overlay")) {
- close();
- }
- });
- shell.addEventListener("keydown", function (e) {
- if (e.key === "Escape") close();
- });
- }
-
- function open(options) {
- options = options || {};
- close();
-
- var root = ensureRoot();
- var shell = document.createElement("div");
- var bottom = options.bottom === true || (options.mobileBottom !== false && isMobile());
- shell.className =
- "aksrt-remote-modal" +
- (bottom ? " aksrt-remote-modal--bottom" : "") +
- (options.className ? " " + options.className : "");
- shell.setAttribute("role", "dialog");
- shell.setAttribute("aria-modal", "true");
-
- var heightStyle = options.height ? "min-height:" + parseInt(options.height, 10) + "px;" : "";
- shell.innerHTML =
- '' +
- '' +
- '
' +
- (options.html || "") +
- "
";
-
- root.appendChild(shell);
- bindChrome(shell);
- active = shell;
- lockScroll(true);
-
- // Force reflow then open for CSS transition.
- void shell.offsetWidth;
- shell.classList.add("is-open");
-
- var focusable = shell.querySelector(
- "button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])"
- );
- if (focusable) {
- try {
- focusable.focus();
- } catch (err) {}
- }
-
- document.dispatchEvent(
- new CustomEvent("aksrt:remote-modal:open", { detail: { shell: shell, options: options } })
- );
- return shell;
- }
-
- function openRemote(options) {
- options = options || {};
- var url = options.url || "";
- if (!url) return Promise.reject(new Error("missing url"));
-
- var loading = open({
- html:
- ' ' +
- ((window.aksrtData && aksrtData.ajaxLoading) || "加载中…") +
- "
",
- className: options.className || "modal-mini full-sm",
- bottom: options.bottom,
- mobileBottom: options.mobileBottom,
- height: options.height,
- });
-
- return fetch(url, {
- method: options.method || "GET",
- credentials: "same-origin",
- headers: Object.assign(restHeaders(), options.headers || {}),
- body: options.body,
- })
- .then(function (res) {
- var type = (res.headers.get("content-type") || "").toLowerCase();
- if (type.indexOf("application/json") !== -1) {
- return res.json().then(function (json) {
- if (!res.ok || (json && json.success === false)) {
- var msg =
- (json && (json.message || (json.data && json.data.message))) ||
- "加载失败";
- throw new Error(msg);
- }
- var html =
- (json && (json.html || (json.data && (json.data.html || json.data)))) || "";
- if (typeof html !== "string") html = "";
- return html;
- });
- }
- return res.text().then(function (text) {
- if (!res.ok) throw new Error("加载失败");
- return text;
- });
- })
- .then(function (html) {
- if (loading && loading.parentNode) loading.parentNode.removeChild(loading);
- if (active === loading) active = null;
- return open({
- html: html,
- className: options.className || "modal-mini full-sm",
- bottom: options.bottom,
- mobileBottom: options.mobileBottom,
- height: options.height,
- });
- })
- .catch(function (err) {
- if (loading && loading.parentNode) loading.parentNode.removeChild(loading);
- if (active === loading) active = null;
- open({
- html:
- '提示
' +
- '' +
- (err && err.message ? String(err.message) : "加载失败") +
- '
',
- className: "modal-mini",
- mobileBottom: true,
- });
- throw err;
- });
- }
-
- function fromTrigger(el) {
- if (!el) return null;
- var url =
- el.getAttribute("data-remote") ||
- el.getAttribute("data-aksrt-remote") ||
- el.getAttribute("href") ||
- "";
- if (!url || url === "#") return null;
- var height = el.getAttribute("data-height") || el.getAttribute("data-aksrt-height");
- return openRemote({
+ return modalRuntime.openRemote({
url: url,
- className: el.getAttribute("data-class") || el.getAttribute("data-aksrt-class") || "modal-mini full-sm",
- height: height ? parseInt(height, 10) : 0,
- mobileBottom: el.getAttribute("mobile-bottom") !== "false",
- bottom: el.getAttribute("data-bottom") === "true",
+ 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,
});
}
- document.addEventListener("click", function (e) {
- var trigger = e.target.closest(
- '[data-toggle="AksrtRemoteModal"], [data-toggle="RefreshModal"], [data-aksrt-remote-modal], a.aksrt-remote-modal-link'
- );
- if (!trigger) return;
- if (trigger.getAttribute("data-aksrt-remote-modal") === "off") return;
- e.preventDefault();
- fromTrigger(trigger);
- });
+ function bind() {
+ if (window.__aksrtRemoteModalBound) return;
+ if (!runtime()) return;
+ window.__aksrtRemoteModalBound = true;
- document.addEventListener("keydown", function (e) {
- if (e.key === "Escape" && active) close();
- });
+ 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);
+ });
+ }
- window.AksrtRemoteModal = {
- open: open,
- openRemote: openRemote,
- close: close,
- fromTrigger: fromTrigger,
- getActive: function () {
- return active;
- },
- };
+ 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/core/request.js b/assets/js/core/request.js
new file mode 100644
index 00000000..b82c768f
--- /dev/null
+++ b/assets/js/core/request.js
@@ -0,0 +1,157 @@
+/**
+ * AKSRT frontend request adapter.
+ *
+ * Owns the injected REST root, nonce headers, response envelopes, and
+ * abort-aware error normalization used by modal loaders.
+ *
+ * @package AKSRT
+ */
+(function (window) {
+ "use strict";
+
+ function data() {
+ return window.aksrtData || window.aksrt || {};
+ }
+
+ function restBase() {
+ var config = data();
+ var raw = config.restUrl || config.apiUrl || config.rest_url || "";
+ var siteUrl = config.siteUrl || config.homeUrl || (window.location.origin + "/");
+
+ if (window.location.protocol === "https:" && siteUrl.substring(0, 5) === "http:") {
+ siteUrl = "https:" + siteUrl.substring(5);
+ }
+
+ if (raw) return String(raw).replace(/\/$/, "");
+ return String(siteUrl).replace(/\/$/, "") + "/index.php?rest_route=/aksrt/v1";
+ }
+
+ function appendQuery(url, query) {
+ if (!query) return url;
+ var parts = [];
+
+ if (typeof query === "string") {
+ parts = query.replace(/^\?/, "").split("&").filter(Boolean);
+ } else if (typeof query === "object") {
+ Object.keys(query).forEach(function (key) {
+ if (query[key] === undefined || query[key] === null) return;
+ parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(String(query[key])));
+ });
+ }
+
+ if (!parts.length) return url;
+ return url + (url.indexOf("?") >= 0 ? "&" : "?") + parts.join("&");
+ }
+
+ function url(path, query) {
+ path = String(path || "");
+ var inlineQuery = "";
+ var queryIndex = path.indexOf("?");
+
+ if (queryIndex >= 0) {
+ inlineQuery = path.slice(queryIndex + 1);
+ path = path.slice(0, queryIndex);
+ }
+
+ if (/^https?:\/\//i.test(path)) {
+ return appendQuery(path, query || inlineQuery);
+ }
+
+ if (path.charAt(0) !== "/") path = "/" + path;
+ var base = restBase();
+ var result = base.indexOf("rest_route=") >= 0 ? base + path : base + path;
+ return appendQuery(result, query || inlineQuery);
+ }
+
+ function headers(options) {
+ var config = data();
+ var source = (options && options.headers) || {};
+ var result = {};
+ var nonce = config.restNonce || config.nonce || "";
+
+ Object.keys(source).forEach(function (key) {
+ result[key] = source[key];
+ });
+
+ if (!result.Accept) result.Accept = "application/json, text/html;q=0.9, */*;q=0.8";
+ if (nonce && !result["X-WP-Nonce"]) result["X-WP-Nonce"] = nonce;
+
+ if (options && options.body !== undefined && options.body !== null &&
+ !(window.FormData && options.body instanceof window.FormData) &&
+ !result["Content-Type"]) {
+ result["Content-Type"] = "application/json";
+ }
+
+ return result;
+ }
+
+ function responseError(result) {
+ var json = result.json;
+ var message = json && (json.message || (json.data && json.data.message));
+ var error = new Error(message || (result.status ? "请求失败(HTTP " + result.status + ")" : "请求失败"));
+ error.status = result.status;
+ error.code = result.status === 401 ? "unauthorized" : "http_error";
+ error.response = result;
+ error.json = json;
+ return error;
+ }
+
+ function parseResponse(response) {
+ return response.text().then(function (text) {
+ var type = (response.headers.get("content-type") || "").toLowerCase();
+ var json = null;
+ var trimmed = String(text || "").trim();
+
+ if (type.indexOf("application/json") >= 0 || trimmed.charAt(0) === "{" || trimmed.charAt(0) === "[") {
+ try {
+ json = trimmed ? JSON.parse(trimmed) : {};
+ } catch (error) {
+ json = null;
+ }
+ }
+
+ return {
+ ok: response.ok,
+ status: response.status,
+ headers: response.headers,
+ text: text,
+ json: json,
+ data: json && json.data !== undefined ? json.data : json,
+ };
+ });
+ }
+
+ function request(path, options) {
+ options = Object.assign({}, options || {});
+ var requestUrl = url(path, options.query);
+ delete options.query;
+
+ var fetchOptions = Object.assign({}, options, {
+ credentials: options.credentials || "same-origin",
+ headers: headers(options),
+ });
+
+ return window.fetch(requestUrl, fetchOptions).then(function (response) {
+ return parseResponse(response);
+ }).then(function (result) {
+ if (!result.ok || (result.json && result.json.success === false)) {
+ throw responseError(result);
+ }
+ return result;
+ });
+ }
+
+ window.AksrtRequest = {
+ url: url,
+ request: request,
+ get: function (path, options) {
+ return request(path, Object.assign({}, options || {}, { method: "GET" }));
+ },
+ post: function (path, body, options) {
+ return request(path, Object.assign({}, options || {}, {
+ method: "POST",
+ body: body,
+ }));
+ },
+ };
+})(window);
diff --git a/assets/js/order-center.js b/assets/js/order-center.js
index 2549413f..941b14c9 100644
--- a/assets/js/order-center.js
+++ b/assets/js/order-center.js
@@ -15,31 +15,31 @@
var detailFooter = page.querySelector("[data-aksrt-order-detail-footer]");
var detailOrderNumber = "";
- function toast(message) {
- if (window.AksrtToast && typeof window.AksrtToast.show === "function") {
- window.AksrtToast.show(message);
+ 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);
}
- function restRoot() {
- return String(aksrtData.siteUrl || "").replace(/\/$/, "") + "/wp-json/aksrt/v1";
- }
-
function request(path, body, method) {
- var options = {
- method: method || "POST",
- credentials: "same-origin",
- headers: { "X-WP-Nonce": aksrtData.restNonce || "" }
- };
- if (body !== undefined) {
- options.headers["Content-Type"] = "application/json";
- options.body = JSON.stringify(body || {});
+ if (!window.AksrtRequest || typeof window.AksrtRequest.request !== "function") {
+ return Promise.resolve({ success: false, message: "请求适配器未加载" });
}
- return fetch(restRoot() + path, options).then(function (response) {
- return response.json().catch(function () {
- return { success: false, message: "请求失败(HTTP " + response.status + ")" };
+ var options = { method: method || "POST" };
+ if (body !== undefined) options.body = JSON.stringify(body || {});
+ return window.AksrtRequest.request(path, options).then(function (response) {
+ return response.json || {};
+ }).catch(function (error) {
+ var response = error && error.response ? error.response : {};
+ return error && error.json ? error.json : (response.json || {
+ success: false,
+ message: error && error.message ? error.message : "请求失败",
});
});
}
@@ -219,6 +219,9 @@
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"]);
+ }
var data = result.data || {};
if (data.redirect && data.payment_url) {
window.location.assign(data.payment_url);
@@ -243,6 +246,9 @@
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"]);
+ }
toast("订单已关闭");
refreshView();
}).catch(function (error) {
diff --git a/assets/js/pay-vip.js b/assets/js/pay-vip.js
index 9a32b42c..fbbf5283 100644
--- a/assets/js/pay-vip.js
+++ b/assets/js/pay-vip.js
@@ -5,126 +5,51 @@
(function () {
"use strict";
- function restBase() {
- var aksrt = window.aksrtData || window.aksrt || {};
- var siteUrl = aksrt.siteUrl || aksrt.homeUrl || (window.location.origin + "/");
- if (window.location.protocol === "https:" && siteUrl.substring(0, 5) === "http:") {
- siteUrl = "https:" + siteUrl.substring(5);
- }
- var raw = aksrt.apiUrl || aksrt.restUrl || aksrt.rest_url || "";
- if (raw) {
- return String(raw).replace(/\/$/, "");
- }
- return siteUrl.replace(/\/$/, "") + "/index.php?rest_route=/aksrt/v1";
- }
-
- /**
- * Join REST root + path [+ query].
- * Pretty: /wp-json/aksrt/v1/pay/vip/modal?vip_level=1
- * Plain: index.php?rest_route=/aksrt/v1/pay/vip/modal&vip_level=1
- * Never put a second "?" after rest_route= (that 404s).
- */
- function restUrl(path, query) {
- path = String(path || "");
- if (path.charAt(0) !== "/") path = "/" + path;
- var base = restBase();
- var qstr = "";
- if (query && typeof query === "object") {
- var parts = [];
- Object.keys(query).forEach(function (k) {
- if (query[k] === undefined || query[k] === null) return;
- parts.push(encodeURIComponent(k) + "=" + encodeURIComponent(String(query[k])));
- });
- qstr = parts.join("&");
- } else if (typeof query === "string" && query) {
- qstr = query.replace(/^\?/, "");
- }
-
- if (base.indexOf("rest_route=") !== -1) {
- var url = base + path;
- if (qstr) url += "&" + qstr;
- return url;
- }
-
- var pretty = base + path;
- if (qstr) pretty += (pretty.indexOf("?") >= 0 ? "&" : "?") + qstr;
- return pretty;
- }
-
- function nonceHeaders() {
- var aksrt = window.aksrtData || window.aksrt || {};
- var n = aksrt.restNonce || aksrt.nonce || "";
- return n ? { "X-WP-Nonce": n } : {};
- }
-
function api(path, options) {
var opts = Object.assign({}, options || {});
- var query = opts.query;
- if (Object.prototype.hasOwnProperty.call(opts, "query")) {
- delete opts.query;
- }
- var onlyPath = String(path || "");
- var inlineQ = null;
- var qi = onlyPath.indexOf("?");
- if (qi >= 0) {
- inlineQ = onlyPath.slice(qi + 1);
- onlyPath = onlyPath.slice(0, qi);
- }
- var url = restUrl(onlyPath, query || inlineQ);
-
- var merged = Object.assign(
- {
- credentials: "same-origin",
- headers: Object.assign({ "Content-Type": "application/json" }, nonceHeaders()),
- },
- opts
- );
- return fetch(url, merged).then(function (r) {
- return r
- .json()
- .catch(function () {
- return { message: "Invalid JSON (HTTP " + r.status + ")" };
- })
- .then(function (j) {
- return { ok: r.ok, status: r.status, json: j };
+ 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 {
+ ok: response.ok,
+ status: response.status,
+ json: response.json || {},
+ };
+ }).catch(function (error) {
+ var response = error && error.response ? error.response : {};
+ return {
+ ok: false,
+ status: error && error.status ? error.status : (response.status || 0),
+ json: error && error.json ? error.json : (response.json || { message: error && error.message }),
+ };
});
}
- function ensureModalShell() {
- var el = document.getElementById("modal_pay_uservip");
- if (el) return el;
- el = document.createElement("div");
- el.id = "modal_pay_uservip";
- el.className = "aksrt-vip-modal-overlay";
- el.setAttribute("aria-hidden", "true");
- el.innerHTML =
- '';
- document.body.appendChild(el);
- el.addEventListener("click", function (e) {
- if (e.target === el || e.target.closest(".aksrt-vip-modal-close")) {
- closeModal();
- }
- });
- document.addEventListener("keydown", function (e) {
- if (e.key === "Escape") closeModal();
- });
- return el;
- }
+
+ var activeVipShell = null;
function openModal() {
- var el = ensureModalShell();
- el.classList.add("is-open");
- el.setAttribute("aria-hidden", "false");
- document.body.classList.add("aksrt-vip-modal-open");
+ if (!window.AksrtModalRuntime || typeof window.AksrtModalRuntime.open !== "function") return null;
+ activeVipShell = window.AksrtModalRuntime.open({
+ className: "aksrt-vip-modal-shell",
+ mobileBottom: false,
+ skeleton: '',
+ telemetry: false,
+ kind: "vip",
+ });
+ return activeVipShell;
}
function closeModal() {
- var el = document.getElementById("modal_pay_uservip");
- if (!el) return;
- el.classList.remove("is-open");
- el.setAttribute("aria-hidden", "true");
- document.body.classList.remove("aksrt-vip-modal-open");
+ if (window.AksrtModalRuntime && typeof window.AksrtModalRuntime.close === "function") {
+ window.AksrtModalRuntime.close("vip");
+ }
+ activeVipShell = null;
}
function bindModalUi(root) {
@@ -226,11 +151,17 @@
}
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"]);
+ }
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"]);
+ }
setTip(pane, (data && data.message) || "开通成功,正在刷新…");
setTimeout(function () {
window.location.reload();
@@ -257,10 +188,13 @@
function telemetryMark(event) {
if (telemetry && telemetryId && typeof telemetry.mark === "function") telemetry.mark(telemetryId, event);
}
- var shell = ensureModalShell();
+ var shell = openModal();
+ if (!shell) {
+ loading = false;
+ telemetryMark("error");
+ return;
+ }
var body = shell.querySelector("[data-aksrt-vip-modal-body]");
- if (body) body.innerHTML = '加载中…
';
- openModal();
telemetryMark("shell");
api("/pay/vip/modal", { query: { vip_level: level } })
@@ -286,6 +220,9 @@
if (body) {
body.innerHTML = r.json.html;
telemetryMark("first-content");
+ if (window.AksrtFrontend && typeof window.AksrtFrontend.rehydrate === "function") {
+ window.AksrtFrontend.rehydrate(body, "modal");
+ }
bindModalUi(body);
telemetryMark("ready");
}
@@ -318,31 +255,19 @@
document.addEventListener("click", onDocClick, true);
}
- // Prefer module registry, but always bind once as hard fallback
- // (float rail must work even if AksrtFrontend.rehydrate is a no-op).
- if (window.__aksrtPayVipBound) {
- // already initialized
- } else if (window.AksrtFrontend && typeof window.AksrtFrontend.register === "function") {
- window.AksrtFrontend.register("aksrt-pay-vip", function () {
- if (window.__aksrtPayVipBound) return;
- window.__aksrtPayVipBound = true;
- init();
- });
- // Also bind immediately — float button is in footer, available now.
- if (!window.__aksrtPayVipBound) {
- window.__aksrtPayVipBound = true;
- init();
- }
- } else if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", function () {
- if (window.__aksrtPayVipBound) return;
- window.__aksrtPayVipBound = true;
- init();
- });
- } else {
+ // This handler is delegated from document, so registry rehydration is not
+ // needed. Bind exactly once and keep dynamically injected triggers working.
+ function bindOnce() {
+ if (window.__aksrtPayVipBound) return;
window.__aksrtPayVipBound = true;
init();
}
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", bindOnce, { once: true });
+ } else {
+ bindOnce();
+ }
+
window.aksrtVipPay = vipPay;
})();
diff --git a/assets/js/user-badges.js b/assets/js/user-badges.js
index 7feac6fd..9e3fd19e 100644
--- a/assets/js/user-badges.js
+++ b/assets/js/user-badges.js
@@ -8,36 +8,31 @@
(function () {
'use strict';
- var aksrt = window.aksrtData || {};
- var siteUrl = aksrt.siteUrl || '';
- if (window.location.protocol === 'https:' && siteUrl.substring(0, 5) === 'http:') {
- siteUrl = 'https:' + siteUrl.substring(5);
- }
- var REST = (siteUrl || '').replace(/\/$/, '') + '/wp-json/aksrt/v1';
- var NONCE = aksrt.restNonce || aksrt.nonce || '';
-
function api(path, options) {
- var opts = Object.assign({
- credentials: 'same-origin',
- headers: Object.assign({ 'Content-Type': 'application/json', 'X-WP-Nonce': NONCE }, (options && options.headers) || {}),
- }, options || {});
- return fetch(REST + path, opts).then(function (r) {
- return r.json().catch(function () { return {}; }).then(function (j) {
- return { ok: r.ok, status: r.status, json: j };
- });
+ 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) {
+ var response = error && error.response ? error.response : {};
+ return {
+ ok: false,
+ status: error && error.status ? error.status : (response.status || 0),
+ json: error && error.json ? error.json : (response.json || { message: error && error.message }),
+ };
});
}
function toast(msg, type) {
- if (window.AksrtToast && typeof window.AksrtToast.show === 'function') {
- window.AksrtToast.show(msg, type || 'info');
+ if (typeof window.AksrtToast === 'function') {
+ window.AksrtToast(msg, type || 'info');
return;
}
- if (window.aksrtNotify) {
+ if (typeof window.aksrtNotify === 'function') {
window.aksrtNotify(msg, type || 'info');
return;
}
- // fallback
try { console.log('[AKSRT badges]', msg); } catch (_) {}
}
@@ -265,6 +260,9 @@
return;
}
toast((r.json && r.json.message) || '已佩戴徽章', 'success');
+ if (window.AksrtModalRuntime && typeof window.AksrtModalRuntime.invalidate === 'function') {
+ window.AksrtModalRuntime.invalidate(['badges', 'identity']);
+ }
state.data = (r.json && r.json.data) || state.data;
renderHead();
renderGrid();
@@ -354,6 +352,9 @@
return;
}
toast((r.json && r.json.message) || '已佩戴徽章', 'success');
+ if (window.AksrtModalRuntime && typeof window.AksrtModalRuntime.invalidate === 'function') {
+ window.AksrtModalRuntime.invalidate(['badges', 'identity']);
+ }
// Re-open the refreshed single-badge modal so state stays in sync.
var refreshed = null;
try {
diff --git a/src/Core/ScriptsPart1b.php b/src/Core/ScriptsPart1b.php
index b8518de9..1b9bd2ff 100644
--- a/src/Core/ScriptsPart1b.php
+++ b/src/Core/ScriptsPart1b.php
@@ -227,7 +227,7 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js';
wp_enqueue_script(
'aksrt-pay-vip',
$base_uri . '/assets/js/pay-vip.js',
- ['aksrt-script'],
+ ['aksrt-script', 'aksrt-core-request', 'aksrt-core-modal-runtime'],
(string) filemtime($vip_js),
true
);
@@ -671,7 +671,7 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js';
wp_enqueue_script(
'aksrt-order-center',
$base_uri . '/assets/js/order-center.js',
- ['aksrt-script'],
+ ['aksrt-script', 'aksrt-core-request', 'aksrt-core-modal-runtime'],
file_exists(get_template_directory() . '/assets/js/order-center.js')
? (string) filemtime(get_template_directory() . '/assets/js/order-center.js')
: $version,
@@ -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-script', 'aksrt-core-request', 'aksrt-core-modal-runtime', 'aksrt-core-remote-modal'],
(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-remote-modal'],
+ ['aksrt-script', 'aksrt-core-request', 'aksrt-core-modal-runtime', 'aksrt-core-remote-modal'],
(string) filemtime($badges_js_path_global),
true
);
diff --git a/src/Core/ScriptsPart1c.php b/src/Core/ScriptsPart1c.php
index 0a5d3796..a715b0b7 100644
--- a/src/Core/ScriptsPart1c.php
+++ b/src/Core/ScriptsPart1c.php
@@ -126,10 +126,14 @@ trait ScriptsPart1c {
'aksrt-core-comment-fixed' => 'comment-fixed-input.js',
'aksrt-core-comment-move' => 'comment-move-form.js',
// Shared zibll-parity list/modal engines (before pagination consumers).
- 'aksrt-core-ajax-list' => 'ajax-list.js',
+ 'aksrt-core-ajax-list' => 'ajax-list.js',
'aksrt-core-modal-telemetry' => 'modal-telemetry.js',
- 'aksrt-core-remote-modal' => 'remote-modal.js',
- 'aksrt-core-pagination' => 'pagination.js',
+ 'aksrt-core-request' => 'request.js',
+ '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-pagination' => 'pagination.js',
'aksrt-core-archive' => 'archive.js',
'aksrt-core-footer-tabbar' => 'footer-tabbar.js',
'aksrt-core-misc' => 'misc.js',
@@ -142,6 +146,15 @@ trait ScriptsPart1c {
$deps = ['aksrt-rehydrate', 'aksrt-runtime'];
if ($handle === 'aksrt-core-slider') {
$deps = array_merge($deps, $needs_swiper ? ['aksrt-swiper'] : []);
+ } elseif ($handle === 'aksrt-core-modal-runtime') {
+ $deps = array_merge($deps, [
+ 'aksrt-core-modal-telemetry',
+ 'aksrt-core-request',
+ 'aksrt-core-modal-cache',
+ 'aksrt-core-modal-state',
+ ]);
+ } elseif ($handle === 'aksrt-core-remote-modal') {
+ $deps[] = 'aksrt-core-modal-runtime';
}
wp_enqueue_script(
$handle,