feat(ux): shared ajax-list/remote-modal + order review modal 1:1
Add zibll-parity AjaxList/RemoteModal engines and replace order-center prompt() review flow with multi-dim star modal, multi-image upload, auto-praise countdown, and REST review-modal endpoint.
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
/**
|
||||
* AKSRT Ajax List Engine — zibll .ajax-next / ajax-replace / route parity.
|
||||
*
|
||||
* DOM contract:
|
||||
* .ajaxpager list container
|
||||
* .ajax-item replace/append unit
|
||||
* .ajax-pag .next-page.ajax-next paginate control
|
||||
* ajax-replace="true" replace mode
|
||||
* route / route-title history + document.title
|
||||
* [win-ajax-replace] partial header sync from response HTML
|
||||
*
|
||||
* Transport:
|
||||
* - Prefer data-aksrt-ajax-url JSON { html, header_html, paginate_html, title }
|
||||
* - Else fetch href as HTML fragment and parse .ajaxpager / .ajax-item
|
||||
*
|
||||
* Events: post_ajax.before / post_ajax.ed on container (bubbles).
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
(function (window, document) {
|
||||
"use strict";
|
||||
|
||||
var loadingMap = new WeakMap();
|
||||
|
||||
function restHeaders() {
|
||||
var h = { Accept: "application/json, text/html", "X-AKSRT-Ajax": "list" };
|
||||
if (window.aksrtData && aksrtData.restNonce) {
|
||||
h["X-WP-Nonce"] = aksrtData.restNonce;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
function closestPager(el) {
|
||||
if (!el) return null;
|
||||
return (
|
||||
el.closest(".ajaxpager") ||
|
||||
el.closest("[data-aksrt-ajaxpager]") ||
|
||||
el.closest(".aksrt-ajaxpager")
|
||||
);
|
||||
}
|
||||
|
||||
function dispatch(container, name, detail) {
|
||||
if (!container) return;
|
||||
try {
|
||||
container.dispatchEvent(
|
||||
new CustomEvent(name, { bubbles: true, detail: detail || {} })
|
||||
);
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
function showLoader(container) {
|
||||
if (!container) return;
|
||||
var loader = container.querySelector(".post_ajax_loader, .post-ajax-loader, [data-aksrt-ajax-loader]");
|
||||
if (loader) {
|
||||
loader.classList.add("is-loading");
|
||||
loader.hidden = false;
|
||||
loader.style.display = "";
|
||||
return;
|
||||
}
|
||||
container.classList.add("is-ajax-loading");
|
||||
}
|
||||
|
||||
function hideLoader(container) {
|
||||
if (!container) return;
|
||||
var loader = container.querySelector(".post_ajax_loader, .post-ajax-loader, [data-aksrt-ajax-loader]");
|
||||
if (loader) {
|
||||
loader.classList.remove("is-loading");
|
||||
if (loader.hasAttribute("data-aksrt-ajax-loader-hide")) {
|
||||
loader.hidden = true;
|
||||
loader.style.display = "none";
|
||||
}
|
||||
}
|
||||
container.classList.remove("is-ajax-loading");
|
||||
}
|
||||
|
||||
function parseHtml(html) {
|
||||
var wrap = document.createElement("div");
|
||||
wrap.innerHTML = html || "";
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function extractFromHtml(html, container) {
|
||||
var doc = parseHtml(html);
|
||||
var pager =
|
||||
doc.querySelector(".ajaxpager") ||
|
||||
doc.querySelector("[data-aksrt-ajaxpager]") ||
|
||||
doc.querySelector(".aksrt-ajaxpager");
|
||||
var items = [];
|
||||
var scope = pager || doc;
|
||||
scope.querySelectorAll(".ajax-item").forEach(function (n) {
|
||||
items.push(n.outerHTML);
|
||||
});
|
||||
if (!items.length && pager) {
|
||||
items.push(pager.innerHTML);
|
||||
}
|
||||
var pag = scope.querySelector(".ajax-pag");
|
||||
var headerBits = {};
|
||||
doc.querySelectorAll("[win-ajax-replace]").forEach(function (n) {
|
||||
var key = n.getAttribute("win-ajax-replace");
|
||||
if (key) headerBits[key] = n.outerHTML;
|
||||
});
|
||||
var titleEl = doc.querySelector("title");
|
||||
return {
|
||||
itemsHtml: items.join(""),
|
||||
paginateHtml: pag ? pag.outerHTML : "",
|
||||
headerBits: headerBits,
|
||||
title: titleEl ? titleEl.textContent : "",
|
||||
fullHtml: pager ? pager.innerHTML : html,
|
||||
};
|
||||
}
|
||||
|
||||
function syncWinReplace(bits) {
|
||||
if (!bits) return;
|
||||
Object.keys(bits).forEach(function (key) {
|
||||
var live = document.querySelector('[win-ajax-replace="' + key + '"]');
|
||||
if (!live) return;
|
||||
var tmp = document.createElement("div");
|
||||
tmp.innerHTML = bits[key];
|
||||
var next = tmp.firstElementChild;
|
||||
if (next) live.replaceWith(next);
|
||||
});
|
||||
}
|
||||
|
||||
function applyRoute(link, title) {
|
||||
if (!link) return;
|
||||
var route = link.getAttribute("route") || link.getAttribute("data-route");
|
||||
if (!(route === "1" || route === "true" || link.hasAttribute("route"))) return;
|
||||
var href = link.getAttribute("href") || link.getAttribute("ajax-href") || "";
|
||||
if (!href) return;
|
||||
try {
|
||||
window.history.replaceState({}, "", href);
|
||||
} catch (err) {}
|
||||
var rt =
|
||||
title ||
|
||||
link.getAttribute("route-title") ||
|
||||
link.getAttribute("data-route-title") ||
|
||||
"";
|
||||
if (rt) {
|
||||
try {
|
||||
document.title = rt;
|
||||
} catch (e2) {}
|
||||
}
|
||||
}
|
||||
|
||||
function replacePaginate(container, paginateHtml) {
|
||||
if (!container || !paginateHtml) return;
|
||||
var old = container.querySelector(".ajax-pag");
|
||||
if (!old) {
|
||||
container.insertAdjacentHTML("beforeend", paginateHtml);
|
||||
return;
|
||||
}
|
||||
var tmp = document.createElement("div");
|
||||
tmp.innerHTML = paginateHtml;
|
||||
var next = tmp.firstElementChild;
|
||||
if (next) old.replaceWith(next);
|
||||
}
|
||||
|
||||
function inject(container, payload, options) {
|
||||
options = options || {};
|
||||
var replace = options.replace !== false;
|
||||
if (!container) return;
|
||||
|
||||
var itemsHtml = payload.itemsHtml || payload.html || "";
|
||||
if (replace) {
|
||||
container.querySelectorAll(".ajax-item").forEach(function (n) {
|
||||
n.remove();
|
||||
});
|
||||
// If container itself holds only items, clear non-loader children.
|
||||
if (container.getAttribute("data-aksrt-ajax-clear") === "1") {
|
||||
Array.prototype.slice.call(container.children).forEach(function (child) {
|
||||
if (
|
||||
child.classList.contains("post_ajax_loader") ||
|
||||
child.classList.contains("post-ajax-loader") ||
|
||||
child.hasAttribute("data-aksrt-ajax-loader") ||
|
||||
child.classList.contains("ajax-pag")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
child.remove();
|
||||
});
|
||||
}
|
||||
if (itemsHtml) {
|
||||
var pag = container.querySelector(".ajax-pag");
|
||||
if (pag) {
|
||||
pag.insertAdjacentHTML("beforebegin", itemsHtml);
|
||||
} else {
|
||||
container.insertAdjacentHTML("beforeend", itemsHtml);
|
||||
}
|
||||
}
|
||||
} else if (itemsHtml) {
|
||||
var pag2 = container.querySelector(".ajax-pag");
|
||||
if (pag2) {
|
||||
pag2.insertAdjacentHTML("beforebegin", itemsHtml);
|
||||
} else {
|
||||
container.insertAdjacentHTML("beforeend", itemsHtml);
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.paginateHtml) {
|
||||
replacePaginate(container, payload.paginateHtml);
|
||||
}
|
||||
if (payload.headerBits) {
|
||||
syncWinReplace(payload.headerBits);
|
||||
}
|
||||
if (payload.headerHtml) {
|
||||
var headerHost = document.querySelector(
|
||||
options.headerSelector || "[data-aksrt-ajax-header]"
|
||||
);
|
||||
if (headerHost) headerHost.innerHTML = payload.headerHtml;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeJson(json) {
|
||||
var data = json && json.data ? json.data : json;
|
||||
if (!data) return null;
|
||||
var html = data.html || data.list_html || "";
|
||||
var itemsHtml = data.items_html || "";
|
||||
if (!itemsHtml && html) {
|
||||
// If html is only item nodes, use as-is; else extract.
|
||||
if (html.indexOf("ajax-item") !== -1) {
|
||||
var parsed = extractFromHtml("<div class='ajaxpager'>" + html + "</div>");
|
||||
itemsHtml = parsed.itemsHtml || html;
|
||||
} else {
|
||||
itemsHtml = html;
|
||||
}
|
||||
}
|
||||
return {
|
||||
itemsHtml: itemsHtml,
|
||||
html: html,
|
||||
paginateHtml: data.paginate_html || data.paginateHtml || "",
|
||||
headerHtml: data.header_html || data.headerHtml || "",
|
||||
headerBits: data.header_bits || null,
|
||||
title: data.title || "",
|
||||
emptyHtml: data.empty_html || "",
|
||||
hasMore: typeof data.has_more !== "undefined" ? !!data.has_more : null,
|
||||
};
|
||||
}
|
||||
|
||||
function loadFromLink(link, override) {
|
||||
override = override || {};
|
||||
if (!link) return Promise.resolve();
|
||||
|
||||
var container =
|
||||
override.container ||
|
||||
(link.getAttribute("ajaxpager-target")
|
||||
? document.querySelector(link.getAttribute("ajaxpager-target"))
|
||||
: null) ||
|
||||
closestPager(link);
|
||||
if (!container) {
|
||||
// Fall back: parent stream list.
|
||||
container = link.closest(".aksrt-product-grid, .aksrt-forum-topics__list, [data-aksrt-ajax-list]");
|
||||
}
|
||||
if (!container) return Promise.resolve();
|
||||
|
||||
if (loadingMap.get(container)) return Promise.resolve();
|
||||
loadingMap.set(container, true);
|
||||
|
||||
var replaceAttr = link.getAttribute("ajax-replace");
|
||||
var replace =
|
||||
typeof override.replace === "boolean"
|
||||
? override.replace
|
||||
: replaceAttr === "true" || replaceAttr === "1" || replaceAttr === "replace";
|
||||
|
||||
var href =
|
||||
override.url ||
|
||||
link.getAttribute("data-aksrt-ajax-url") ||
|
||||
link.getAttribute("ajax-href") ||
|
||||
link.getAttribute("href") ||
|
||||
"";
|
||||
|
||||
dispatch(container, "post_ajax.before", { link: link, replace: replace, url: href });
|
||||
showLoader(container);
|
||||
if (replace) {
|
||||
// Soft clear items while loading (keep skeleton if present).
|
||||
container.querySelectorAll(".ajax-item").forEach(function (n) {
|
||||
n.classList.add("is-fading");
|
||||
});
|
||||
}
|
||||
|
||||
return fetch(href, {
|
||||
method: override.method || "GET",
|
||||
credentials: "same-origin",
|
||||
headers: restHeaders(),
|
||||
})
|
||||
.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)) {
|
||||
throw new Error(
|
||||
(json && (json.message || (json.data && json.data.message))) || "加载失败"
|
||||
);
|
||||
}
|
||||
return normalizeJson(json);
|
||||
});
|
||||
}
|
||||
return res.text().then(function (text) {
|
||||
if (!res.ok) throw new Error("加载失败");
|
||||
return extractFromHtml(text, container);
|
||||
});
|
||||
})
|
||||
.then(function (payload) {
|
||||
if (!payload) throw new Error("空响应");
|
||||
if (replace && !payload.itemsHtml && payload.emptyHtml) {
|
||||
inject(container, { itemsHtml: payload.emptyHtml, paginateHtml: payload.paginateHtml }, { replace: true });
|
||||
} else {
|
||||
inject(container, payload, { replace: replace });
|
||||
}
|
||||
applyRoute(link, payload.title);
|
||||
hideLoader(container);
|
||||
loadingMap.set(container, false);
|
||||
dispatch(container, "post_ajax.ed", {
|
||||
link: link,
|
||||
replace: replace,
|
||||
url: href,
|
||||
payload: payload,
|
||||
});
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("aksrt:posts:rendered", {
|
||||
bubbles: true,
|
||||
detail: { container: container, replace: replace },
|
||||
})
|
||||
);
|
||||
return payload;
|
||||
})
|
||||
.catch(function (err) {
|
||||
hideLoader(container);
|
||||
loadingMap.set(container, false);
|
||||
container.querySelectorAll(".ajax-item.is-fading").forEach(function (n) {
|
||||
n.classList.remove("is-fading");
|
||||
});
|
||||
if (typeof window.aksrtToast === "function") {
|
||||
window.aksrtToast((err && err.message) || "加载失败");
|
||||
}
|
||||
dispatch(container, "post_ajax.error", { link: link, error: err });
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("click", function (e) {
|
||||
var link = e.target.closest("a.ajax-next, .ajax-next a, button.ajax-next, [data-aksrt-ajax-next]");
|
||||
if (!link) return;
|
||||
// Ignore if inside a component that opts out.
|
||||
if (link.closest("[data-aksrt-ajax-list='off']")) return;
|
||||
// Native pagination core may own .aksrt-pagination-ajax buttons.
|
||||
if (link.closest(".aksrt-pagination-ajax") && !link.classList.contains("ajax-next")) return;
|
||||
|
||||
var href = link.getAttribute("href") || link.getAttribute("ajax-href") || link.getAttribute("data-aksrt-ajax-url");
|
||||
if (!href && !link.getAttribute("data-aksrt-ajax-url")) return;
|
||||
|
||||
e.preventDefault();
|
||||
loadFromLink(link.tagName === "A" || link.classList.contains("ajax-next") ? link : link.closest(".ajax-next") || link);
|
||||
});
|
||||
|
||||
window.AksrtAjaxList = {
|
||||
load: loadFromLink,
|
||||
inject: inject,
|
||||
extractFromHtml: extractFromHtml,
|
||||
closestPager: closestPager,
|
||||
};
|
||||
})(window, document);
|
||||
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* AKSRT Remote Modal — zibll RefreshModal parity.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* @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 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 restHeaders() {
|
||||
var h = { Accept: "text/html, application/json" };
|
||||
if (window.aksrtData && aksrtData.restNonce) {
|
||||
h["X-WP-Nonce"] = aksrtData.restNonce;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
function lockScroll(lock) {
|
||||
document.documentElement.classList.toggle("aksrt-remote-modal-open", !!lock);
|
||||
document.body.classList.toggle("aksrt-remote-modal-open", !!lock);
|
||||
}
|
||||
|
||||
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({
|
||||
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",
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape" && active) close();
|
||||
});
|
||||
|
||||
window.AksrtRemoteModal = {
|
||||
open: open,
|
||||
openRemote: openRemote,
|
||||
close: close,
|
||||
fromTrigger: fromTrigger,
|
||||
getActive: function () {
|
||||
return active;
|
||||
},
|
||||
};
|
||||
})(window, document);
|
||||
+248
-37
@@ -67,6 +67,252 @@
|
||||
return aksrtData.cartUrl || (aksrtData.routes && aksrtData.routes.cart) || "/";
|
||||
}
|
||||
|
||||
/* ── Order review modal (zibll 1:1) ── */
|
||||
var STAR_SVG =
|
||||
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>';
|
||||
var SCORE_LABELS = ["", "非常差", "较差", "一般", "较好", "非常好"];
|
||||
|
||||
function formatRemain(sec) {
|
||||
sec = Math.max(0, sec | 0);
|
||||
var d = Math.floor(sec / 86400);
|
||||
var h = Math.floor((sec % 86400) / 3600);
|
||||
var m = Math.floor((sec % 3600) / 60);
|
||||
var s = sec % 60;
|
||||
function z(n) { return n < 10 ? "0" + n : String(n); }
|
||||
if (d > 0) return d + "天" + z(h) + ":" + z(m) + ":" + z(s);
|
||||
return z(h) + ":" + z(m) + ":" + z(s);
|
||||
}
|
||||
|
||||
function bindShopScore(root) {
|
||||
if (!root) return;
|
||||
root.querySelectorAll("[data-aksrt-score-box], .shop-score-box").forEach(function (box) {
|
||||
if (box.dataset.scoreBound === "1") return;
|
||||
box.dataset.scoreBound = "1";
|
||||
var iconBox = box.querySelector(".score-icon");
|
||||
var textBox = box.querySelector(".score-text");
|
||||
var input = box.querySelector(".score-input");
|
||||
if (!iconBox) return;
|
||||
var value = parseInt(box.getAttribute("score-value") || (input && input.value) || "0", 10) || 0;
|
||||
var html = "";
|
||||
for (var i = 1; i <= 5; i++) {
|
||||
html += '<i class="icon-star' + (i <= value ? " active" : "") + '" data-score="' + i + '">' + STAR_SVG + "</i>";
|
||||
}
|
||||
iconBox.innerHTML = html;
|
||||
function paint(score, hover) {
|
||||
iconBox.querySelectorAll(".icon-star").forEach(function (star) {
|
||||
var s = parseInt(star.getAttribute("data-score"), 10) || 0;
|
||||
star.classList.toggle("active", !hover && s <= score);
|
||||
star.classList.toggle("hover", !!hover && s <= score);
|
||||
});
|
||||
if (textBox) textBox.textContent = SCORE_LABELS[score] || "";
|
||||
}
|
||||
paint(value, false);
|
||||
iconBox.addEventListener("mouseover", function (e) {
|
||||
var star = e.target.closest(".icon-star");
|
||||
if (!star || !iconBox.contains(star)) return;
|
||||
paint(parseInt(star.getAttribute("data-score"), 10) || 0, true);
|
||||
});
|
||||
iconBox.addEventListener("mouseleave", function () {
|
||||
var cur = parseInt(box.getAttribute("score-value") || "0", 10) || 0;
|
||||
paint(cur, false);
|
||||
});
|
||||
iconBox.addEventListener("click", function (e) {
|
||||
var star = e.target.closest(".icon-star");
|
||||
if (!star || !iconBox.contains(star)) return;
|
||||
var score = parseInt(star.getAttribute("data-score"), 10) || 0;
|
||||
box.setAttribute("score-value", String(score));
|
||||
if (input) input.value = String(score);
|
||||
paint(score, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindReviewImages(form) {
|
||||
var preview = form.querySelector("[data-aksrt-review-preview]");
|
||||
var fileInput = form.querySelector("[data-aksrt-review-file]");
|
||||
var addBtn = form.querySelector("[data-aksrt-review-add]");
|
||||
if (!preview || !fileInput) return;
|
||||
var max = parseInt(preview.getAttribute("data-max") || "6", 10) || 6;
|
||||
var files = [];
|
||||
|
||||
function render() {
|
||||
preview.querySelectorAll(".preview-item").forEach(function (n) { n.remove(); });
|
||||
files.forEach(function (item, idx) {
|
||||
var node = document.createElement("div");
|
||||
node.className = "preview-item";
|
||||
node.innerHTML = '<img alt=""><button type="button" class="remove" aria-label="删除">×</button>';
|
||||
node.querySelector("img").src = item.url;
|
||||
node.querySelector(".remove").addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (item.url && item.url.indexOf("blob:") === 0) {
|
||||
try { URL.revokeObjectURL(item.url); } catch (err) {}
|
||||
}
|
||||
files.splice(idx, 1);
|
||||
render();
|
||||
});
|
||||
preview.insertBefore(node, addBtn);
|
||||
});
|
||||
if (addBtn) addBtn.style.display = files.length >= max ? "none" : "";
|
||||
form._aksrtReviewFiles = files.slice();
|
||||
}
|
||||
|
||||
function onPick() {
|
||||
if (files.length >= max) return;
|
||||
fileInput.click();
|
||||
}
|
||||
if (addBtn) {
|
||||
addBtn.addEventListener("click", onPick);
|
||||
addBtn.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
onPick();
|
||||
}
|
||||
});
|
||||
}
|
||||
fileInput.addEventListener("change", function () {
|
||||
var list = Array.prototype.slice.call(fileInput.files || []);
|
||||
list.forEach(function (file) {
|
||||
if (files.length >= max) return;
|
||||
if (!file.type || file.type.indexOf("image/") !== 0) return;
|
||||
files.push({ file: file, url: URL.createObjectURL(file) });
|
||||
});
|
||||
fileInput.value = "";
|
||||
render();
|
||||
});
|
||||
form._aksrtReviewFiles = [];
|
||||
render();
|
||||
}
|
||||
|
||||
function bindReviewCountdown(root) {
|
||||
var el = root.querySelector("[data-aksrt-review-countdown]");
|
||||
if (!el) return;
|
||||
var remain = parseInt(el.getAttribute("data-aksrt-review-countdown") || "0", 10) || 0;
|
||||
if (remain <= 0) return;
|
||||
el.textContent = formatRemain(remain);
|
||||
var timer = window.setInterval(function () {
|
||||
remain -= 1;
|
||||
if (remain <= 0) {
|
||||
window.clearInterval(timer);
|
||||
el.textContent = "交易已关闭";
|
||||
return;
|
||||
}
|
||||
el.textContent = formatRemain(remain);
|
||||
}, 1000);
|
||||
root._aksrtReviewTimer = timer;
|
||||
}
|
||||
|
||||
function fileToDataUrl(file) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function () { resolve(String(reader.result || "")); };
|
||||
reader.onerror = function () { reject(new Error("read failed")); };
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
function openOrderReviewModal(orderNumber, triggerBtn) {
|
||||
if (!window.AksrtRemoteModal || typeof window.AksrtRemoteModal.openRemote !== "function") {
|
||||
showToast("评价组件未加载");
|
||||
return;
|
||||
}
|
||||
if (triggerBtn) triggerBtn.disabled = true;
|
||||
var url = restRoot() + "/shop/frontend/orders/" + encodeURIComponent(orderNumber) + "/review-modal";
|
||||
window.AksrtRemoteModal.openRemote({
|
||||
url: url,
|
||||
className: "modal-mini full-sm",
|
||||
height: 380,
|
||||
mobileBottom: true
|
||||
}).then(function (shell) {
|
||||
if (triggerBtn) triggerBtn.disabled = false;
|
||||
if (!shell) return;
|
||||
var form = shell.querySelector("[data-aksrt-order-review-form]");
|
||||
if (!form) return;
|
||||
bindShopScore(shell);
|
||||
bindReviewImages(form);
|
||||
bindReviewCountdown(shell);
|
||||
|
||||
form.addEventListener("submit", function (e) {
|
||||
e.preventDefault();
|
||||
var errBox = form.querySelector("[data-aksrt-review-error]");
|
||||
function showErr(msg) {
|
||||
if (errBox) {
|
||||
errBox.hidden = false;
|
||||
errBox.textContent = msg;
|
||||
} else {
|
||||
showToast(msg);
|
||||
}
|
||||
}
|
||||
if (errBox) {
|
||||
errBox.hidden = true;
|
||||
errBox.textContent = "";
|
||||
}
|
||||
|
||||
var productId = parseInt(form.getAttribute("data-product-id") || "0", 10) || 0;
|
||||
var needShipping = form.getAttribute("data-need-shipping") === "1";
|
||||
var scoreProduct = parseInt((form.querySelector('input[name="score[product]"]') || {}).value || "0", 10) || 0;
|
||||
var scoreService = parseInt((form.querySelector('input[name="score[service]"]') || {}).value || "0", 10) || 0;
|
||||
var scoreShipping = parseInt((form.querySelector('input[name="score[shipping]"]') || {}).value || "0", 10) || 0;
|
||||
if (scoreProduct < 1 || scoreService < 1) {
|
||||
showErr("请对商品进行评分");
|
||||
return;
|
||||
}
|
||||
if (needShipping && scoreShipping < 1) {
|
||||
showErr("请对物流服务进行评分");
|
||||
return;
|
||||
}
|
||||
|
||||
var comment = (form.querySelector('textarea[name="comment"]') || {}).value || "";
|
||||
var submitBtn = form.querySelector("[data-aksrt-review-submit]");
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = "提交中…";
|
||||
}
|
||||
|
||||
var pending = form._aksrtReviewFiles || [];
|
||||
Promise.all(pending.map(function (item) {
|
||||
return item.file ? fileToDataUrl(item.file) : Promise.resolve(item.url || "");
|
||||
})).then(function (images) {
|
||||
images = images.filter(Boolean);
|
||||
return request("/shop/frontend/orders/" + orderNumber + "/review", {
|
||||
product_id: productId,
|
||||
content: comment,
|
||||
rating_product: scoreProduct,
|
||||
rating_service: scoreService,
|
||||
rating_shipping: needShipping ? scoreShipping : scoreService,
|
||||
images: images
|
||||
});
|
||||
}).then(function (result) {
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fa fa-check" aria-hidden="true"></i> 确认提交';
|
||||
}
|
||||
if (!result || !result.success) {
|
||||
showErr((result && result.message) || "评价失败");
|
||||
return;
|
||||
}
|
||||
showToast((result.message) || "评价成功");
|
||||
if (window.AksrtRemoteModal) window.AksrtRemoteModal.close();
|
||||
var card = findOrderCard(orderNumber);
|
||||
updateCardStatus(card, "completed", "已完成");
|
||||
if (card) {
|
||||
var btn = card.querySelector("[data-aksrt-order-review]");
|
||||
if (btn) btn.remove();
|
||||
}
|
||||
}).catch(function () {
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fa fa-check" aria-hidden="true"></i> 确认提交';
|
||||
}
|
||||
showErr("网络错误");
|
||||
});
|
||||
});
|
||||
}).catch(function (err) {
|
||||
if (triggerBtn) triggerBtn.disabled = false;
|
||||
showToast((err && err.message) || "无法打开评价");
|
||||
});
|
||||
}
|
||||
|
||||
/* ── 扫码支付弹层(微信 Native code_url 等)── */
|
||||
function showPaymentQr(content, orderNumber) {
|
||||
var existing = document.getElementById("aksrt-pay-qr-overlay");
|
||||
@@ -280,47 +526,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Review submit modal
|
||||
// Review modal (zibll order_comment_modal 1:1 UX)
|
||||
var reviewBtn = event.target.closest("[data-aksrt-order-review]");
|
||||
if (reviewBtn) {
|
||||
var reviewOrder = reviewBtn.getAttribute("data-aksrt-order-review");
|
||||
var card = reviewBtn.closest("[data-aksrt-order-card]");
|
||||
var productId = 0;
|
||||
if (card) {
|
||||
var firstItem = card.querySelector("[data-product-id]");
|
||||
if (firstItem) productId = Number(firstItem.getAttribute("data-product-id") || 0);
|
||||
}
|
||||
if (!productId) {
|
||||
// try parse from item title block dataset if present
|
||||
var any = card ? card.querySelector(".aksrt-orders-page__card-item") : null;
|
||||
if (any && any.getAttribute("data-product-id")) {
|
||||
productId = Number(any.getAttribute("data-product-id") || 0);
|
||||
}
|
||||
}
|
||||
if (!reviewOrder) return;
|
||||
var content = window.prompt("请输入评价内容(可选)", "好评!");
|
||||
if (content === null) return;
|
||||
var rating = window.prompt("商品评分 1-5", "5");
|
||||
var r = Math.max(1, Math.min(5, parseInt(rating || "5", 10) || 5));
|
||||
reviewBtn.disabled = true;
|
||||
request("/shop/frontend/orders/" + reviewOrder + "/review", {
|
||||
product_id: productId,
|
||||
content: content || "好评!",
|
||||
rating_product: r,
|
||||
rating_service: r,
|
||||
rating_shipping: r
|
||||
}).then(function (result) {
|
||||
reviewBtn.disabled = false;
|
||||
if (!result || !result.success) {
|
||||
showToast((result && result.message) || "评价失败");
|
||||
return;
|
||||
}
|
||||
showToast(result.message || "评价已提交");
|
||||
updateCardStatus(findOrderCard(reviewOrder), "completed", "已完成");
|
||||
}).catch(function () {
|
||||
reviewBtn.disabled = false;
|
||||
showToast("网络错误");
|
||||
});
|
||||
openOrderReviewModal(reviewOrder, reviewBtn);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user