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:
2026-07-18 16:26:28 +08:00
parent 081da99528
commit e2587ca3a0
12 changed files with 1499 additions and 48 deletions
+248 -37
View File
@@ -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="删除">&times;</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;
}