feat(modal): add cancellable shared runtime
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
@@ -0,0 +1,416 @@
|
||||
/**
|
||||
* 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, """)
|
||||
.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) ||
|
||||
'<div class="aksrt-remote-modal__skeleton" aria-live="polite" aria-busy="true">' +
|
||||
'<span class="aksrt-remote-modal__skeleton-line is-wide"></span>' +
|
||||
'<span class="aksrt-remote-modal__skeleton-line"></span>' +
|
||||
'<span class="aksrt-remote-modal__skeleton-line is-short"></span>' +
|
||||
'<span class="aksrt-remote-modal__skeleton-block"></span>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
shell.className =
|
||||
"aksrt-remote-modal" +
|
||||
(bottom ? " aksrt-remote-modal--bottom" : "") +
|
||||
(options.className ? " " + options.className : "");
|
||||
shell.setAttribute("role", "dialog");
|
||||
shell.setAttribute("aria-modal", "true");
|
||||
shell.setAttribute("tabindex", "-1");
|
||||
shell.innerHTML =
|
||||
'<div class="aksrt-remote-modal__overlay" data-aksrt-remote-modal-close></div>' +
|
||||
'<div class="aksrt-remote-modal__dialog" style="' + heightStyle + '">' +
|
||||
'<div class="aksrt-remote-modal__content">' + initialHtml + "</div></div>";
|
||||
|
||||
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 =
|
||||
'<div class="aksrt-remote-modal__error touch border-title"><b>提示</b></div>' +
|
||||
'<div class="aksrt-remote-modal__error-body">' + escapeHtml(errorMessage(error)) +
|
||||
'</div><div class="but-average modal-buts">' +
|
||||
'<button type="button" class="but" data-aksrt-remote-modal-retry>重试</button>' +
|
||||
'<button type="button" class="but" data-aksrt-remote-modal-close>关闭</button></div>';
|
||||
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 = '<div class="aksrt-remote-modal__error-body">暂无内容</div>';
|
||||
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);
|
||||
@@ -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);
|
||||
+57
-212
@@ -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 =
|
||||
'<div class="aksrt-remote-modal__overlay" data-aksrt-remote-modal-close></div>' +
|
||||
'<div class="aksrt-remote-modal__dialog" style="' + heightStyle + '">' +
|
||||
'<div class="aksrt-remote-modal__content">' +
|
||||
(options.html || "") +
|
||||
"</div></div>";
|
||||
|
||||
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:
|
||||
'<div class="aksrt-remote-modal__loading"><i class="fas fa-spinner fa-spin"></i> ' +
|
||||
((window.aksrtData && aksrtData.ajaxLoading) || "加载中…") +
|
||||
"</div>",
|
||||
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:
|
||||
'<div class="aksrt-remote-modal__error touch border-title"><b>提示</b></div>' +
|
||||
'<div class="aksrt-remote-modal__error-body">' +
|
||||
(err && err.message ? String(err.message) : "加载失败") +
|
||||
'</div><div class="but-average modal-buts"><button type="button" class="but" data-aksrt-remote-modal-close>关闭</button></div>',
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
+24
-18
@@ -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) {
|
||||
|
||||
+60
-135
@@ -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 =
|
||||
'<div class="aksrt-vip-modal-dialog" role="dialog" aria-modal="true">' +
|
||||
'<div class="aksrt-vip-modal-content" data-aksrt-vip-modal-body></div></div>';
|
||||
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: '<div class="aksrt-vip-modal-dialog"><div class="aksrt-vip-modal-content" data-aksrt-vip-modal-body><div class="text-center padding-20 muted-2-color">加载中…</div></div></div>',
|
||||
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 = '<div class="text-center padding-20 muted-2-color">加载中…</div>';
|
||||
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;
|
||||
})();
|
||||
|
||||
+21
-20
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user