perf(modal): unify cancellable frontend modal flows
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
25ce1d8f43
commit
5375ad6429
36 files changed
+2623
-1806
No files matched your search
+144
-96
@@ -8,12 +8,11 @@
|
||||
"use strict";
|
||||
|
||||
var page = document.querySelector("[data-aksrt-order-center]");
|
||||
if (!page || !window.aksrtData) return;
|
||||
if (!page) return;
|
||||
if (!window.AksrtModalRuntime.isRolloutEnabled("order")) return;
|
||||
|
||||
var detailModal = page.querySelector("[data-aksrt-order-detail-modal]");
|
||||
var detailBody = page.querySelector("[data-aksrt-order-detail-body]");
|
||||
var detailFooter = page.querySelector("[data-aksrt-order-detail-footer]");
|
||||
var detailOrderNumber = "";
|
||||
var detailAutoRefreshStarted = false;
|
||||
|
||||
function toast(message, type) {
|
||||
window.AksrtToast(message, type || "info");
|
||||
@@ -22,21 +21,18 @@
|
||||
function request(path, body, method) {
|
||||
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 : "请求失败",
|
||||
});
|
||||
});
|
||||
return window.AksrtRequest.request(path, options);
|
||||
}
|
||||
|
||||
function orderPath(orderNumber, suffix) {
|
||||
return "/orders/" + encodeURIComponent(orderNumber || "") + (suffix || "");
|
||||
}
|
||||
|
||||
function errorMessage(error, fallback) {
|
||||
if (error && error.message) return String(error.message);
|
||||
return fallback || "请求失败";
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value == null ? "" : value)
|
||||
.replace(/&/g, "&").replace(/</g, "<")
|
||||
@@ -51,15 +47,33 @@
|
||||
: "¥" + amount.toFixed(2);
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
if (detailModal) detailModal.hidden = true;
|
||||
detailOrderNumber = "";
|
||||
document.documentElement.classList.remove("aksrt-modal-open");
|
||||
function activeDetail(number) {
|
||||
return detailOrderNumber === number && !!window.AksrtModalRuntime.getActive();
|
||||
}
|
||||
|
||||
function renderDetail(order) {
|
||||
if (!detailBody || !detailFooter) return;
|
||||
var actions = order.actions || {};
|
||||
function orderTag(orderNumber) {
|
||||
return "order:" + String(orderNumber || "").toLowerCase();
|
||||
}
|
||||
|
||||
function invalidateOrder(orderNumber, order) {
|
||||
var tags = order && Array.isArray(order.invalidation_tags) ? order.invalidation_tags : [
|
||||
"orders",
|
||||
orderTag(orderNumber),
|
||||
];
|
||||
window.AksrtModalRuntime.invalidate(tags);
|
||||
}
|
||||
|
||||
function detailSkeleton() {
|
||||
return '<div class="aksrt-order-detail__skeleton" aria-live="polite" aria-busy="true">' +
|
||||
'<span class="aksrt-modal-runtime__skeleton-line is-wide"></span>' +
|
||||
'<span class="aksrt-modal-runtime__skeleton-line"></span>' +
|
||||
'<span class="aksrt-modal-runtime__skeleton-block"></span>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderDetail(order, session) {
|
||||
var allowedActions = order.allowed_actions || {};
|
||||
var reconciliation = order.reconciliation || {};
|
||||
var image = order.image || "";
|
||||
var title = order.product_name || "订单";
|
||||
var status = order.status_group || "closed";
|
||||
@@ -67,7 +81,9 @@
|
||||
html += '<span class="aksrt-order-detail__status-pill is-' + escapeHtml(status) + '">' + escapeHtml(order.status_label || "") + "</span>";
|
||||
html += '<strong class="aksrt-order-detail__hero-amount">' + formatMoney(order) + "</strong></div>";
|
||||
html += '<div class="aksrt-order-detail__product">';
|
||||
html += image ? '<img class="aksrt-order-detail__thumb" src="' + escapeHtml(image) + '" alt="">' : '<span class="aksrt-order-detail__thumb aksrt-order-detail__thumb--empty"><i class="fas fa-book-open"></i></span>';
|
||||
html += image
|
||||
? '<img class="aksrt-order-detail__thumb" src="' + escapeHtml(image) + '" alt="">'
|
||||
: '<span class="aksrt-order-detail__thumb aksrt-order-detail__thumb--empty"><i class="fas fa-book-open"></i></span>';
|
||||
html += '<div><strong class="aksrt-order-detail__product-title">' + escapeHtml(title) + "</strong>";
|
||||
html += '<span class="aksrt-order-detail__product-meta">' + escapeHtml(order.product_type_label || "") + "</span></div></div>";
|
||||
html += '<dl class="aksrt-order-detail__list">';
|
||||
@@ -75,42 +91,60 @@
|
||||
html += "<div><dt>" + escapeHtml(row[0]) + "</dt><dd>" + escapeHtml(row[1] || "—") + "</dd></div>";
|
||||
});
|
||||
html += "</dl>";
|
||||
detailBody.innerHTML = html;
|
||||
if (reconciliation.state === "refresh_available") {
|
||||
html += '<div class="aksrt-order-detail__refresh-state" role="status">支付状态可能仍在同步,请刷新后查看。';
|
||||
html += '<button class="btn-outline" type="button" data-aksrt-order-refresh="' + escapeHtml(order.order_number) + '">刷新支付状态</button></div>';
|
||||
}
|
||||
html += '<footer class="aksrt-modal__footer aksrt-order-detail__actions">';
|
||||
html += '<button class="btn-outline" type="button" data-aksrt-modal-close>关闭</button>';
|
||||
if (allowedActions.can_cancel) html += '<button class="btn-ghost" type="button" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '">关闭订单</button>';
|
||||
if (allowedActions.can_pay) html += '<button class="btn-primary" type="button" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml((order.payment_method || {}).value || "") + '">继续支付</button>';
|
||||
if (allowedActions.can_open && order.permalink) html += '<a class="btn-primary" href="' + escapeHtml(order.permalink) + '">' + escapeHtml(order.entitlement_label || "打开已购内容") + "</a>";
|
||||
html += "</footer>";
|
||||
session.content.innerHTML = html;
|
||||
}
|
||||
|
||||
var footer = '<button class="btn-outline" type="button" data-aksrt-order-detail-close>关闭</button>';
|
||||
if (actions.can_cancel) footer += '<button class="btn-ghost" type="button" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '">关闭订单</button>';
|
||||
if (actions.can_pay) footer += '<button class="btn-primary" type="button" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml((order.payment_method || {}).value || "") + '">继续支付</button>';
|
||||
if (actions.can_open && order.permalink) footer += '<a class="btn-primary" href="' + escapeHtml(order.permalink) + '">' + escapeHtml(order.entitlement_label || "打开已购内容") + "</a>";
|
||||
detailFooter.innerHTML = footer;
|
||||
function detailOptions(orderNumber, refresh) {
|
||||
return {
|
||||
url: orderPath(orderNumber, refresh ? "/refresh" : ""),
|
||||
method: refresh ? "POST" : "GET",
|
||||
kind: "orders",
|
||||
title: "订单详情",
|
||||
responseType: "json",
|
||||
cache: !refresh,
|
||||
cacheKey: "orders:detail:" + orderNumber,
|
||||
cacheTags: ["orders", orderTag(orderNumber)],
|
||||
skeleton: detailSkeleton(),
|
||||
renderData: function (data, session) {
|
||||
if (orderNumber !== detailOrderNumber) return false;
|
||||
if (!data || typeof data !== "object") {
|
||||
session.content.innerHTML = '<p class="c-red">订单不存在</p>';
|
||||
return;
|
||||
}
|
||||
renderDetail(data, session);
|
||||
if (!refresh && !detailAutoRefreshStarted && data.reconciliation && data.reconciliation.state === "refresh_available") {
|
||||
detailAutoRefreshStarted = true;
|
||||
window.setTimeout(function () {
|
||||
if (activeDetail(orderNumber)) refreshDetail(orderNumber);
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
onData: function (data) {
|
||||
if (refresh) invalidateOrder(orderNumber, data);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function openDetail(orderNumber) {
|
||||
if (!detailModal || !detailBody) return;
|
||||
if (!orderNumber) return;
|
||||
detailOrderNumber = orderNumber;
|
||||
var telemetry = window.AksrtModalTelemetry;
|
||||
var telemetryId = telemetry && typeof telemetry.start === "function" ? telemetry.start("orders") : "";
|
||||
function telemetryMark(event) {
|
||||
if (telemetry && telemetryId && typeof telemetry.mark === "function") telemetry.mark(telemetryId, event);
|
||||
}
|
||||
detailBody.innerHTML = '<p class="muted-2-color">加载中…</p>';
|
||||
detailFooter.innerHTML = "";
|
||||
detailModal.hidden = false;
|
||||
document.documentElement.classList.add("aksrt-modal-open");
|
||||
telemetryMark("shell");
|
||||
request(orderPath(orderNumber), undefined, "GET").then(function (result) {
|
||||
if (orderNumber !== detailOrderNumber) return;
|
||||
if (!result.success || !result.data) {
|
||||
telemetryMark("error");
|
||||
detailBody.innerHTML = '<p class="c-red">' + escapeHtml(result.message || "订单不存在") + "</p>";
|
||||
return;
|
||||
}
|
||||
telemetryMark("first-content");
|
||||
renderDetail(result.data);
|
||||
telemetryMark("ready");
|
||||
}).catch(function () {
|
||||
telemetryMark("error");
|
||||
detailBody.innerHTML = '<p class="c-red">网络错误,请稍后重试</p>';
|
||||
});
|
||||
detailAutoRefreshStarted = false;
|
||||
window.AksrtModalRuntime.openRemote(detailOptions(orderNumber, false));
|
||||
}
|
||||
|
||||
function refreshDetail(orderNumber) {
|
||||
if (!orderNumber || !activeDetail(orderNumber)) return;
|
||||
window.AksrtModalRuntime.openRemote(detailOptions(orderNumber, true));
|
||||
}
|
||||
|
||||
function currentViewPath() {
|
||||
@@ -122,11 +156,11 @@
|
||||
}
|
||||
|
||||
function actionHtml(order) {
|
||||
var actions = order.actions || {};
|
||||
var allowedActions = order.allowed_actions || {};
|
||||
var html = '<button class="btn-outline" type="button" data-aksrt-order-detail="' + escapeHtml(order.order_number) + '">订单详情</button>';
|
||||
if (actions.can_cancel) html += '<button class="btn-ghost" type="button" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '">关闭订单</button>';
|
||||
if (actions.can_pay) html += '<button class="btn-primary" type="button" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml((order.payment_method || {}).value || '') + '">继续支付</button>';
|
||||
if (actions.can_open && order.permalink) html += '<a class="btn-primary is-entitlement" href="' + escapeHtml(order.permalink) + '">' + escapeHtml(order.entitlement_label || '打开已购内容') + '</a>';
|
||||
if (allowedActions.can_cancel) html += '<button class="btn-ghost" type="button" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '">关闭订单</button>';
|
||||
if (allowedActions.can_pay) html += '<button class="btn-primary" type="button" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml((order.payment_method || {}).value || "") + '">继续支付</button>';
|
||||
if (allowedActions.can_open && order.permalink) html += '<a class="btn-primary is-entitlement" href="' + escapeHtml(order.permalink) + '">' + escapeHtml(order.entitlement_label || "打开已购内容") + "</a>";
|
||||
return html;
|
||||
}
|
||||
|
||||
@@ -136,15 +170,15 @@
|
||||
: '<div class="aksrt-orders-page__card-art" aria-hidden="true"><i class="fas fa-book-open"></i></div>';
|
||||
var countdown = Number(order.time_left || 0) > 0
|
||||
? '<span class="aksrt-orders-page__countdown" data-aksrt-order-countdown="' + Number(order.time_left) + '"></span>'
|
||||
: '';
|
||||
return '<article class="aksrt-orders-page__card" data-aksrt-order-card="' + escapeHtml(order.order_number) + '" data-order-status="' + escapeHtml(order.status_group || '') + '">' +
|
||||
'<div class="aksrt-orders-page__card-rail"><span>' + escapeHtml(order.product_type_label || '') + '</span><time>' + escapeHtml(order.created_at || '') + '</time><span class="aksrt-orders-page__card-status is-' + escapeHtml(order.status_group || '') + '">' + escapeHtml(order.status_label || '') + '</span></div>' +
|
||||
'<div class="aksrt-orders-page__card-main">' + image + '<div class="aksrt-orders-page__card-copy"><h3>' + escapeHtml(order.product_name || '订单') + '</h3><p>订单号 ' + escapeHtml(order.order_number) + '</p></div><div class="aksrt-orders-page__card-value"><strong>' + formatMoney(order) + '</strong><span>实付</span></div></div>' +
|
||||
: "";
|
||||
return '<article class="aksrt-orders-page__card" data-aksrt-order-card="' + escapeHtml(order.order_number) + '" data-order-status="' + escapeHtml(order.status_group || "") + '">' +
|
||||
'<div class="aksrt-orders-page__card-rail"><span>' + escapeHtml(order.product_type_label || "") + '</span><time>' + escapeHtml(order.created_at || "") + '</time><span class="aksrt-orders-page__card-status is-' + escapeHtml(order.status_group || "") + '">' + escapeHtml(order.status_label || "") + '</span></div>' +
|
||||
'<div class="aksrt-orders-page__card-main">' + image + '<div class="aksrt-orders-page__card-copy"><h3>' + escapeHtml(order.product_name || "订单") + '</h3><p>订单号 ' + escapeHtml(order.order_number) + '</p></div><div class="aksrt-orders-page__card-value"><strong>' + formatMoney(order) + '</strong><span>实付</span></div></div>' +
|
||||
'<footer class="aksrt-orders-page__card-actions">' + countdown + actionHtml(order) + '</footer></article>';
|
||||
}
|
||||
|
||||
function renderStats(stats) {
|
||||
var root = page.querySelector('[data-aksrt-order-stats]');
|
||||
var root = page.querySelector("[data-aksrt-order-stats]");
|
||||
if (!root) return;
|
||||
root.innerHTML = '<div class="aksrt-orders-page__stat"><span>全部</span><strong>' + Number(stats.total || 0) + '</strong></div>' +
|
||||
'<div class="aksrt-orders-page__stat"><span>待支付</span><strong class="is-pending">' + Number(stats.pending || 0) + '</strong></div>' +
|
||||
@@ -152,95 +186,93 @@
|
||||
}
|
||||
|
||||
function renderPagination(view) {
|
||||
var existing = page.querySelector('.aksrt-orders-page__pagination');
|
||||
var existing = page.querySelector(".aksrt-orders-page__pagination");
|
||||
if (existing) existing.remove();
|
||||
if (Number(view.total_pages || 1) <= 1) return;
|
||||
var root = document.createElement('nav');
|
||||
root.className = 'aksrt-orders-page__pagination';
|
||||
root.setAttribute('aria-label', '订单分页');
|
||||
var root = document.createElement("nav");
|
||||
root.className = "aksrt-orders-page__pagination";
|
||||
root.setAttribute("aria-label", "订单分页");
|
||||
var base = new URL(window.location.href);
|
||||
for (var i = 1; i <= Number(view.total_pages); i++) {
|
||||
base.searchParams.set('order_page', String(i));
|
||||
var link = document.createElement('a');
|
||||
link.className = 'aksrt-orders-page__page-chip' + (i === Number(view.page) ? ' is-active' : '');
|
||||
base.searchParams.set("order_page", String(i));
|
||||
var link = document.createElement("a");
|
||||
link.className = "aksrt-orders-page__page-chip" + (i === Number(view.page) ? " is-active" : "");
|
||||
link.href = base.toString();
|
||||
link.textContent = String(i);
|
||||
root.appendChild(link);
|
||||
}
|
||||
var list = page.querySelector('[data-aksrt-order-list]');
|
||||
var list = page.querySelector("[data-aksrt-order-list]");
|
||||
if (list) list.after(root);
|
||||
}
|
||||
|
||||
function renderView(view) {
|
||||
renderStats(view.stats || {});
|
||||
var list = page.querySelector('[data-aksrt-order-list]');
|
||||
var list = page.querySelector("[data-aksrt-order-list]");
|
||||
if (!list) return;
|
||||
var items = Array.isArray(view.items) ? view.items : [];
|
||||
if (!items.length) {
|
||||
list.innerHTML = '<div class="aksrt-orders-page__empty"><i class="fas fa-receipt" aria-hidden="true"></i><h3>没有匹配的订单</h3><p>换个条件试试,或清除当前筛选。</p></div>';
|
||||
list.classList.add('is-empty');
|
||||
list.classList.add("is-empty");
|
||||
} else {
|
||||
list.classList.remove('is-empty');
|
||||
list.innerHTML = items.map(cardHtml).join('');
|
||||
list.classList.remove("is-empty");
|
||||
list.innerHTML = items.map(cardHtml).join("");
|
||||
}
|
||||
renderPagination(view);
|
||||
if (detailOrderNumber) {
|
||||
var detail = items.filter(function (item) { return item.order_number === detailOrderNumber; })[0];
|
||||
if (detail) renderDetail(detail); else closeDetail();
|
||||
}
|
||||
updateCountdowns();
|
||||
}
|
||||
|
||||
function refreshView() {
|
||||
return request(currentViewPath(), undefined, "GET").then(function (result) {
|
||||
if (!result.success || !result.data) throw new Error(result.message || "刷新失败");
|
||||
if (!result || !result.data) throw new Error("刷新失败");
|
||||
renderView(result.data);
|
||||
return result.data;
|
||||
}).catch(function (error) {
|
||||
toast(error.message || "订单状态已变更,请刷新后查看");
|
||||
toast(errorMessage(error, "订单状态已变更,请刷新后查看"));
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
function startPayment(button) {
|
||||
var number = button.getAttribute("data-aksrt-order-pay") || "";
|
||||
var method = button.getAttribute("data-aksrt-order-method") || "";
|
||||
if (!number) return;
|
||||
if (!number || button.disabled) return;
|
||||
button.disabled = true;
|
||||
request(orderPath(number, "/pay"), method ? { payment_method: method } : {}).then(function (result) {
|
||||
if (!result.success) throw new Error(result.message || "支付失败");
|
||||
window.AksrtModalRuntime.invalidate(["orders", "vip"]);
|
||||
request(orderPath(number, "/pay"), method ? { payment_method: method } : {}, "POST").then(function (result) {
|
||||
var data = result.data || {};
|
||||
invalidateOrder(number, data.order);
|
||||
if (data.redirect && data.payment_url) {
|
||||
window.location.assign(data.payment_url);
|
||||
return;
|
||||
}
|
||||
toast(data.message || "支付成功");
|
||||
toast(data.message || "支付成功", "success");
|
||||
refreshView();
|
||||
if (activeDetail(number)) openDetail(number);
|
||||
}).catch(function (error) {
|
||||
button.disabled = false;
|
||||
toast(error.message || "网络错误,请稍后重试");
|
||||
toast(errorMessage(error, "网络错误,请稍后重试"), "error");
|
||||
});
|
||||
}
|
||||
|
||||
function closeOrder(button) {
|
||||
var number = button.getAttribute("data-aksrt-order-cancel") || "";
|
||||
if (!number) return;
|
||||
if (!number || button.disabled) return;
|
||||
if (button.getAttribute("data-aksrt-confirm") !== "true") {
|
||||
button.setAttribute("data-aksrt-confirm", "true");
|
||||
button.textContent = "确认关闭";
|
||||
return;
|
||||
}
|
||||
button.disabled = true;
|
||||
request(orderPath(number, "/cancel")).then(function (result) {
|
||||
if (!result.success) throw new Error(result.message || "关闭订单失败");
|
||||
window.AksrtModalRuntime.invalidate(["orders", "vip"]);
|
||||
toast("订单已关闭");
|
||||
request(orderPath(number, "/cancel"), {}, "POST").then(function (result) {
|
||||
var data = result.data || {};
|
||||
invalidateOrder(number, data.order);
|
||||
toast("订单已关闭", "success");
|
||||
refreshView();
|
||||
if (activeDetail(number)) openDetail(number);
|
||||
}).catch(function (error) {
|
||||
button.disabled = false;
|
||||
button.removeAttribute("data-aksrt-confirm");
|
||||
button.textContent = "关闭订单";
|
||||
toast(error.message || "网络错误,请稍后重试");
|
||||
toast(errorMessage(error, "网络错误,请稍后重试"), "error");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -263,15 +295,31 @@
|
||||
}
|
||||
var detail = event.target.closest("[data-aksrt-order-detail]");
|
||||
if (detail) { openDetail(detail.getAttribute("data-aksrt-order-detail")); return; }
|
||||
if (event.target.closest("[data-aksrt-order-detail-close]")) { closeDetail(); return; }
|
||||
var pay = event.target.closest("[data-aksrt-order-pay]");
|
||||
if (pay) { startPayment(pay); return; }
|
||||
if (pay) { event.preventDefault(); startPayment(pay); return; }
|
||||
var cancel = event.target.closest("[data-aksrt-order-cancel]");
|
||||
if (cancel) closeOrder(cancel);
|
||||
if (cancel) { event.preventDefault(); closeOrder(cancel); }
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape" && detailModal && !detailModal.hidden) closeDetail();
|
||||
document.addEventListener("click", function (event) {
|
||||
var refresh = event.target.closest("[data-aksrt-order-refresh]");
|
||||
if (refresh) {
|
||||
event.preventDefault();
|
||||
refreshDetail(refresh.getAttribute("data-aksrt-order-refresh") || "");
|
||||
return;
|
||||
}
|
||||
var pay = event.target.closest("[data-aksrt-order-pay]");
|
||||
if (pay && !page.contains(pay)) {
|
||||
event.preventDefault();
|
||||
startPayment(pay);
|
||||
return;
|
||||
}
|
||||
var cancel = event.target.closest("[data-aksrt-order-cancel]");
|
||||
if (cancel && !page.contains(cancel)) {
|
||||
event.preventDefault();
|
||||
closeOrder(cancel);
|
||||
}
|
||||
});
|
||||
|
||||
window.setInterval(updateCountdowns, 1000);
|
||||
}());
|
||||
Reference in new issue
Block a user