perf(modal): unify cancellable frontend modal flows
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -230,14 +230,18 @@
|
||||
}, 220);
|
||||
}
|
||||
|
||||
function responseData(result) {
|
||||
if (!result) return null;
|
||||
if (result.data !== undefined && result.data !== null) return result.data;
|
||||
if (result.json && result.json.data !== undefined) return result.json.data;
|
||||
return result.json;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return typeof result.text === "string" ? result.text : "";
|
||||
var data = responseData(result);
|
||||
if (data && typeof data.html === "string") return data.html;
|
||||
if (result && result.json && typeof result.json.html === "string") return result.json.html;
|
||||
return result && typeof result.text === "string" ? result.text : "";
|
||||
}
|
||||
|
||||
function errorMessage(error) {
|
||||
@@ -262,6 +266,27 @@
|
||||
|
||||
function renderResult(session, result) {
|
||||
if (!session || session.closed || active !== session) return;
|
||||
|
||||
if (session.options.responseType === "json" || typeof session.options.renderData === "function") {
|
||||
var data = responseData(result);
|
||||
if (data === null || data === undefined) {
|
||||
setState(session, "empty");
|
||||
telemetryMark(session, "first-content");
|
||||
telemetryMark(session, "ready");
|
||||
session.content.innerHTML = '<div class="aksrt-modal-runtime__error-body">暂无内容</div>';
|
||||
return;
|
||||
}
|
||||
if (typeof session.options.renderData === "function") {
|
||||
session.options.renderData(data, session, result);
|
||||
}
|
||||
setState(session, "ready");
|
||||
telemetryMark(session, "first-content");
|
||||
rehydrate(session);
|
||||
telemetryMark(session, "ready");
|
||||
if (typeof session.options.onData === "function") session.options.onData(data, session, result);
|
||||
return;
|
||||
}
|
||||
|
||||
var html = responseHtml(result);
|
||||
if (!html) {
|
||||
setState(session, "empty");
|
||||
@@ -379,6 +404,13 @@
|
||||
return window.AksrtModalCache.invalidate(tags);
|
||||
}
|
||||
|
||||
function rolloutEnabled(kind) {
|
||||
var config = window.aksrtData || window.aksrt || {};
|
||||
var rollout = config.frontendModalRollout;
|
||||
if (!rollout || typeof rollout[kind] === "undefined") return true;
|
||||
return rollout[kind] === true;
|
||||
}
|
||||
|
||||
window.AksrtModalRuntime = {
|
||||
open: open,
|
||||
openRemote: openRemote,
|
||||
@@ -387,6 +419,7 @@
|
||||
registerLoader: registerLoader,
|
||||
openLoader: openLoader,
|
||||
invalidate: invalidate,
|
||||
isRolloutEnabled: rolloutEnabled,
|
||||
getActive: function () { return active ? active.shell : null; },
|
||||
getState: function () { return active ? active.state : null; },
|
||||
};
|
||||
|
||||
+81
-105
@@ -14,49 +14,23 @@
|
||||
if (!aksrt) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer restUrl/apiUrl from wp_localize (handles plain permalinks:
|
||||
// /?rest_route=/aksrt/v1/...). Never hardcode /wp-json/ under siteUrl.
|
||||
var REST = '';
|
||||
if (aksrt.restUrl) {
|
||||
REST = String(aksrt.restUrl).replace(/\/?$/, '');
|
||||
} else if (aksrt.apiUrl) {
|
||||
REST = String(aksrt.apiUrl).replace(/\/?$/, '');
|
||||
} else if (aksrt.siteUrl) {
|
||||
var siteUrl = String(aksrt.siteUrl);
|
||||
if (window.location.protocol === 'https:' && siteUrl.substring(0, 5) === 'http:') {
|
||||
siteUrl = 'https:' + siteUrl.substring(5);
|
||||
}
|
||||
REST = siteUrl.replace(/\/$/, '') + '/wp-json/aksrt/v1';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
// Force HTTPS if page is HTTPS (mixed content).
|
||||
if (window.location.protocol === 'https:' && REST.substring(0, 5) === 'http:') {
|
||||
REST = 'https:' + REST.substring(5);
|
||||
}
|
||||
var NONCE_HEADER = aksrt.restNonce ? { 'X-WP-Nonce': aksrt.restNonce } : (aksrt.nonce ? { 'X-WP-Nonce': aksrt.nonce } : {});
|
||||
var activePoll = null;
|
||||
|
||||
function api(path, options) {
|
||||
var merged = Object.assign({
|
||||
credentials: 'same-origin',
|
||||
headers: Object.assign({ 'Content-Type': 'application/json' }, NONCE_HEADER),
|
||||
}, options || {});
|
||||
return fetch(REST + path, merged).then(function (r) {
|
||||
// 401 时去掉 nonce 重试(页面缓存可能导致 nonce 过期,GET 请求可用 cookie 认证)
|
||||
if (r.status === 401 && (!options || !options.method || options.method === 'GET')) {
|
||||
var retry = Object.assign({}, merged, {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
return fetch(REST + path, retry).then(function (r2) {
|
||||
return r2.json().catch(function () {
|
||||
return { message: '服务器返回了非 JSON 响应 (HTTP ' + r2.status + ')' };
|
||||
}).then(function (j) { return { ok: r2.ok, status: r2.status, json: j }; });
|
||||
});
|
||||
}
|
||||
return r.json().catch(function () {
|
||||
return { message: '服务器返回了非 JSON 响应 (HTTP ' + r.status + ')' };
|
||||
}).then(function (j) { return { ok: r.ok, status: r.status, json: j }; });
|
||||
return window.AksrtRequest.request(path, options || {}).then(function (response) {
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
json: response.json || {},
|
||||
data: response.data,
|
||||
};
|
||||
}).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 }),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -411,12 +385,13 @@
|
||||
|
||||
|
||||
|
||||
// ===== VIP 积分兑换 =====
|
||||
// ===== VIP 积分兑换 =====
|
||||
function initVipPointsExchange() {
|
||||
var root = document.querySelector('[data-aksrt-vip-points]');
|
||||
if (!root) return;
|
||||
if (!root || !window.AksrtModalRuntime.isRolloutEnabled('vip')) return;
|
||||
var tip = root.querySelector('[data-aksrt-vip-points-tip]');
|
||||
var busy = false;
|
||||
var coordinator = window.AksrtVipPurchaseCoordinator;
|
||||
function setTip(text, isError) {
|
||||
if (!tip) return;
|
||||
tip.textContent = text || '';
|
||||
@@ -431,9 +406,9 @@
|
||||
busy = true;
|
||||
btn.classList.add('is-loading');
|
||||
setTip('正在兑换…');
|
||||
api('/pay/vip/points-exchange', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ package_id: packageId })
|
||||
coordinator.exchange({
|
||||
packageId: packageId,
|
||||
tags: ['vip', 'orders']
|
||||
}).then(function (r) {
|
||||
busy = false;
|
||||
btn.classList.remove('is-loading');
|
||||
@@ -455,9 +430,10 @@
|
||||
// ===== VIP 开通 =====
|
||||
function initVipShop() {
|
||||
var root = document.querySelector('[data-aksrt-vip-shop]');
|
||||
if (!root) return;
|
||||
if (!root || !window.AksrtModalRuntime.isRolloutEnabled('vip')) return;
|
||||
var tip = root.querySelector('[data-aksrt-vip-tip]');
|
||||
var busy = false;
|
||||
var coordinator = window.AksrtVipPurchaseCoordinator;
|
||||
|
||||
function setTip(text, isError) {
|
||||
if (!tip) return;
|
||||
@@ -474,23 +450,6 @@
|
||||
return first ? first.value : 'balance';
|
||||
}
|
||||
|
||||
function pollOrder(orderNumber) {
|
||||
if (!orderNumber) return;
|
||||
var tries = 0;
|
||||
var maxTries = 40;
|
||||
var timer = setInterval(function () {
|
||||
tries += 1;
|
||||
api('/pay/content/order/' + encodeURIComponent(orderNumber)).then(function (r) {
|
||||
if (r.ok && r.json && r.json.paid) {
|
||||
clearInterval(timer);
|
||||
setTip('支付成功,正在刷新…');
|
||||
window.location.reload();
|
||||
}
|
||||
}).catch(function () { /* ignore */ });
|
||||
if (tries >= maxTries) clearInterval(timer);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
root.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('[data-aksrt-vip-buy]');
|
||||
if (!btn || busy) return;
|
||||
@@ -498,38 +457,46 @@
|
||||
var packageId = parseInt(btn.getAttribute('data-package-id') || '0', 10);
|
||||
if (!packageId) return;
|
||||
busy = true;
|
||||
btn.disabled = true;
|
||||
btn.classList.add('is-loading');
|
||||
setTip('正在创建订单…');
|
||||
|
||||
api('/pay/vip', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
package_id: packageId,
|
||||
payment_method: selectedMethod()
|
||||
})
|
||||
}).then(function (r) {
|
||||
busy = false;
|
||||
btn.classList.remove('is-loading');
|
||||
if (!r.ok || (r.json && r.json.code && !r.json.success)) {
|
||||
var msg = (r.json && r.json.message) || '开通失败';
|
||||
setTip(msg, true);
|
||||
return;
|
||||
}
|
||||
var data = (r.json && r.json.data) ? r.json.data : r.json;
|
||||
if (data && data.redirect && (data.payment_url || data.qr_content)) {
|
||||
if (data.payment_url) {
|
||||
window.open(data.payment_url, '_blank');
|
||||
}
|
||||
coordinator.start({
|
||||
packageId: packageId,
|
||||
paymentMethod: selectedMethod(),
|
||||
tags: ['vip', 'orders'],
|
||||
onPending: function (data) {
|
||||
busy = false;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('is-loading');
|
||||
if (data.payment_url) window.open(data.payment_url, '_blank');
|
||||
setTip(data.message || '请完成支付,支付成功后将自动刷新。');
|
||||
if (data.order_number) pollOrder(data.order_number);
|
||||
return;
|
||||
},
|
||||
onSuccess: function (result) {
|
||||
busy = false;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('is-loading');
|
||||
setTip((result.data && result.data.message) || '开通成功,正在刷新…');
|
||||
window.location.reload();
|
||||
},
|
||||
onFailure: function () {
|
||||
busy = false;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('is-loading');
|
||||
setTip('支付未完成,请重试。', true);
|
||||
},
|
||||
onTimeout: function () {
|
||||
busy = false;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('is-loading');
|
||||
setTip('支付状态暂未确认,请稍后在订单中心查看。', true);
|
||||
},
|
||||
onError: function (response) {
|
||||
busy = false;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('is-loading');
|
||||
setTip((response.json && response.json.message) || '开通失败', true);
|
||||
}
|
||||
setTip((data && data.message) || '开通成功,正在刷新…');
|
||||
window.location.reload();
|
||||
}).catch(function () {
|
||||
busy = false;
|
||||
btn.classList.remove('is-loading');
|
||||
setTip('网络错误,请重试', true);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -588,6 +555,9 @@
|
||||
return;
|
||||
}
|
||||
var data = (r.json && r.json.data) ? r.json.data : r.json;
|
||||
if (type === 'vip' && window.AksrtModalRuntime && typeof window.AksrtModalRuntime.invalidate === 'function') {
|
||||
window.AksrtModalRuntime.invalidate(['vip', 'orders']);
|
||||
}
|
||||
setTip((data && data.message) || '兑换成功,正在刷新…');
|
||||
window.location.reload();
|
||||
}).catch(function () {
|
||||
@@ -608,26 +578,28 @@
|
||||
}
|
||||
function closeModal(el) {
|
||||
if (!el) return;
|
||||
if (activePoll && typeof activePoll.abort === 'function') activePoll.abort();
|
||||
activePoll = null;
|
||||
el.hidden = true;
|
||||
el.classList.remove('is-open');
|
||||
if (!document.querySelector('.uc-modal.is-open')) {
|
||||
document.body.classList.remove('uc-modal-open');
|
||||
}
|
||||
}
|
||||
function pollPaidOrder(orderNumber) {
|
||||
function pollPaidOrder(orderNumber, setTip, tags) {
|
||||
var coordinator = window.AksrtVipPurchaseCoordinator;
|
||||
if (!orderNumber) return;
|
||||
var tries = 0;
|
||||
var maxTries = 40;
|
||||
var timer = setInterval(function () {
|
||||
tries += 1;
|
||||
api('/pay/content/order/' + encodeURIComponent(orderNumber)).then(function (r) {
|
||||
if (r.ok && r.json && r.json.paid) {
|
||||
clearInterval(timer);
|
||||
var handle = coordinator.poll(orderNumber, {
|
||||
tags: tags || ['credits', 'orders'],
|
||||
onTerminal: function (result) {
|
||||
activePoll = null;
|
||||
if (result.success) {
|
||||
if (typeof setTip === 'function') setTip('支付成功,正在刷新…');
|
||||
window.location.reload();
|
||||
}
|
||||
}).catch(function () {});
|
||||
if (tries >= maxTries) clearInterval(timer);
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
return handle;
|
||||
}
|
||||
|
||||
function initBalanceCharge() {
|
||||
@@ -645,7 +617,11 @@
|
||||
btn.addEventListener('click', function () { openModal(modal); });
|
||||
});
|
||||
modal.querySelectorAll('[data-aksrt-charge-close]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () { closeModal(modal); });
|
||||
btn.addEventListener('click', function () {
|
||||
if (activePoll && typeof activePoll.abort === 'function') activePoll.abort();
|
||||
activePoll = null;
|
||||
closeModal(modal);
|
||||
});
|
||||
});
|
||||
modal.querySelectorAll('.uc-charge-package input').forEach(function (inp) {
|
||||
inp.addEventListener('change', function () {
|
||||
@@ -694,7 +670,7 @@
|
||||
if (data && data.redirect && (data.payment_url || data.qr_content)) {
|
||||
if (data.payment_url) window.open(data.payment_url, '_blank');
|
||||
setTip(data.message || '请完成支付,支付成功后将自动刷新。');
|
||||
if (data.order_number) pollPaidOrder(data.order_number);
|
||||
if (data.order_number) activePoll = pollPaidOrder(data.order_number, setTip, ['credits', 'orders']);
|
||||
return;
|
||||
}
|
||||
setTip((data && data.message) || '充值成功,正在刷新…');
|
||||
@@ -917,7 +893,7 @@
|
||||
if (data && data.redirect && (data.payment_url || data.qr_content)) {
|
||||
if (data.payment_url) window.open(data.payment_url, '_blank');
|
||||
setTip(data.message || '请完成支付,支付成功后将自动刷新。');
|
||||
if (data.order_number) pollPaidOrder(data.order_number);
|
||||
if (data.order_number) activePoll = pollPaidOrder(data.order_number, setTip, ['credits', 'orders']);
|
||||
return;
|
||||
}
|
||||
setTip((data && data.message) || '购买成功,正在刷新…');
|
||||
|
||||
+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);
|
||||
}());
|
||||
|
||||
+348
-186
@@ -1,72 +1,246 @@
|
||||
/**
|
||||
* AKSRT VIP pay modal — zibll .pay-vip parity.
|
||||
* Loads modal HTML from GET /aksrt/v1/pay/vip/modal then purchases via POST /pay/vip.
|
||||
* AKSRT VIP pay modal — JSON view model + canonical purchase coordinator.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
function api(path, options) {
|
||||
return window.AksrtRequest.request(path, Object.assign({}, 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 text(value) {
|
||||
return String(value == null ? "" : value);
|
||||
}
|
||||
|
||||
function node(tag, className, value) {
|
||||
var el = document.createElement(tag);
|
||||
if (className) el.className = className;
|
||||
if (value !== undefined && value !== null) el.textContent = text(value);
|
||||
return el;
|
||||
}
|
||||
|
||||
function append(parent, child) {
|
||||
if (child) parent.appendChild(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
function levelMap(levels) {
|
||||
var map = Object.create(null);
|
||||
(Array.isArray(levels) ? levels : []).forEach(function (level) {
|
||||
if (level && level.level) map[String(level.level)] = level;
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
var activeVipShell = null;
|
||||
|
||||
function openModal() {
|
||||
activeVipShell = window.AksrtModalRuntime.open({
|
||||
className: "aksrt-vip-modal-shell",
|
||||
mobileBottom: false,
|
||||
skeleton: '<div class="aksrt-vip-modal-panel"><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 actionLabel(action) {
|
||||
if (action === "renew") return "续费";
|
||||
if (action === "upgrade") return "升级";
|
||||
return "开通";
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
window.AksrtModalRuntime.close("vip");
|
||||
activeVipShell = null;
|
||||
function actionKey(action) {
|
||||
return action === "pay" ? "purchase" : action;
|
||||
}
|
||||
|
||||
function bindModalUi(root) {
|
||||
if (!root || root.dataset.aksrtVipBound === "1") return;
|
||||
root.dataset.aksrtVipBound = "1";
|
||||
function actionReason(reason) {
|
||||
if (reason === "no_payment_method") return "暂无可用支付方式";
|
||||
if (reason === "no_package") return "暂无可用套餐";
|
||||
if (reason === "not_available") return "当前不可用";
|
||||
return "";
|
||||
}
|
||||
|
||||
root.addEventListener("click", function (e) {
|
||||
var tab = e.target.closest("[data-vip-tab]");
|
||||
if (tab) {
|
||||
e.preventDefault();
|
||||
var id = tab.getAttribute("data-vip-tab");
|
||||
root.querySelectorAll(".aksrt-vip-tab").forEach(function (a) {
|
||||
a.parentElement && a.parentElement.classList.remove("active");
|
||||
});
|
||||
tab.parentElement && tab.parentElement.classList.add("active");
|
||||
root.querySelectorAll(".aksrt-vip-pane").forEach(function (p) {
|
||||
var on = p.id === id;
|
||||
p.hidden = !on;
|
||||
p.classList.toggle("active", on);
|
||||
});
|
||||
return;
|
||||
}
|
||||
function renderEquity(parent, value) {
|
||||
if (!value) return;
|
||||
var equity = node("div");
|
||||
// The server sanitizes and wraps this admin-authored fragment before JSON.
|
||||
equity.innerHTML = text(value);
|
||||
while (equity.firstChild) parent.appendChild(equity.firstChild);
|
||||
}
|
||||
|
||||
var submit = e.target.closest("[data-vip-submit]");
|
||||
if (submit) {
|
||||
e.preventDefault();
|
||||
purchaseFromPane(submit.closest(".aksrt-vip-pane") || root, submit);
|
||||
function renderProducts(parent, items) {
|
||||
if (!Array.isArray(items) || !items.length) {
|
||||
append(parent, node("div", "muted-2-color text-center padding-10", "暂无可用套餐"));
|
||||
return;
|
||||
}
|
||||
var wrap = node("div", "mb10 label-box charge-box aksrt-vip-products");
|
||||
items.forEach(function (item, index) {
|
||||
if (!item || !item.package_id) return;
|
||||
var label = node("label", "aksrt-vip-product-label");
|
||||
var input = node("input");
|
||||
input.className = "hide vip-product-input";
|
||||
input.type = "radio";
|
||||
input.name = "vip_product_id_" + String(item.level || "group");
|
||||
input.setAttribute("data-aksrt-vip-package", "");
|
||||
input.value = String(item.package_id);
|
||||
input.checked = index === 0;
|
||||
label.appendChild(input);
|
||||
|
||||
var product = node("div", "zib-widget vip-product relative text-center product-box");
|
||||
if (item.discount_tag) append(product, node("div", "abs-right vip-tag badg jb-yellow", item.discount_tag));
|
||||
if (item.title) append(product, node("div", "em09 mb3", item.title));
|
||||
var priceRow = node("div", "flex abl gap6 jc");
|
||||
append(priceRow, node("div", "product-price c-red", item.price_label || ""));
|
||||
if (item.show_price_label) {
|
||||
var original = node("div", "original-price relative muted-2-color");
|
||||
var strike = node("s", "", item.show_price_label);
|
||||
append(original, strike);
|
||||
append(priceRow, original);
|
||||
}
|
||||
append(product, priceRow);
|
||||
if (item.days_label) append(product, node("div", "muted-2-color c-blue em09 p2-10 badg radius", item.days_label));
|
||||
append(label, product);
|
||||
append(wrap, label);
|
||||
});
|
||||
append(parent, wrap);
|
||||
}
|
||||
|
||||
function renderMethods(parent, methods) {
|
||||
methods = Array.isArray(methods) ? methods : [];
|
||||
if (!methods.length) {
|
||||
append(parent, node("div", "muted-2-color text-center padding-10", "暂无可用支付方式"));
|
||||
return;
|
||||
}
|
||||
if (methods.length === 1) {
|
||||
var one = node("input");
|
||||
one.type = "hidden";
|
||||
one.name = "aksrt_vip_pay_method";
|
||||
one.value = text(methods[0].id || "balance");
|
||||
one.setAttribute("data-aksrt-vip-method-fixed", "");
|
||||
append(parent, one);
|
||||
return;
|
||||
}
|
||||
var wrap = node("div", "uc-vip-shop__methods mb10");
|
||||
wrap.setAttribute("data-aksrt-vip-methods", "");
|
||||
append(wrap, node("span", "uc-vip-shop__methods-label", "支付方式"));
|
||||
methods.forEach(function (method, index) {
|
||||
var label = node("label", "uc-vip-shop__method");
|
||||
var input = node("input");
|
||||
input.type = "radio";
|
||||
input.name = "aksrt_vip_pay_method";
|
||||
input.value = text(method.id || "balance");
|
||||
input.checked = index === 0;
|
||||
append(label, input);
|
||||
append(label, node("span", "", method.name || method.id || ""));
|
||||
append(wrap, label);
|
||||
});
|
||||
append(parent, wrap);
|
||||
}
|
||||
|
||||
function renderModalView(body, view) {
|
||||
view = view || {};
|
||||
if (body.replaceChildren) body.replaceChildren();
|
||||
else body.innerHTML = "";
|
||||
|
||||
if (!view.enabled) {
|
||||
append(body, node("div", "box-body payvip-modal text-center padding-20", "会员功能未启用。"));
|
||||
return;
|
||||
}
|
||||
|
||||
var catalog = view.catalog || {};
|
||||
var member = view.member || {};
|
||||
var levels = levelMap(view.levels);
|
||||
var groups = Array.isArray(catalog.level_packages) ? catalog.level_packages : [];
|
||||
var methods = Array.isArray(view.payment_methods) ? view.payment_methods : [];
|
||||
var allowed = view.allowed_actions || {};
|
||||
var reasons = view.action_reasons || {};
|
||||
var modal = node("div", "box-body payvip-modal");
|
||||
modal.setAttribute("data-aksrt-vip-modal", "");
|
||||
if (view.version && view.version.catalog) modal.setAttribute("data-aksrt-vip-version", view.version.catalog);
|
||||
|
||||
var close = node("button", "aksrt-modal-close", "×");
|
||||
close.type = "button";
|
||||
close.setAttribute("data-aksrt-modal-close", "");
|
||||
close.setAttribute("aria-label", "关闭");
|
||||
append(modal, close);
|
||||
|
||||
var userBox = node("ul", "list-inline user-box flex ac mb10");
|
||||
var avatarItem = node("li", "avatar-img");
|
||||
if (member.avatar_url) {
|
||||
var avatar = node("img");
|
||||
avatar.src = text(member.avatar_url);
|
||||
avatar.alt = "";
|
||||
avatar.width = 42;
|
||||
avatar.height = 42;
|
||||
append(avatarItem, avatar);
|
||||
}
|
||||
append(userBox, avatarItem);
|
||||
var userInfo = node("li");
|
||||
append(userInfo, node("b", "em12", member.name || ""));
|
||||
if (member.level > 0) {
|
||||
var expiry = node("div", "muted-2-color em09");
|
||||
var badge = node("span", "badg c-yellow vip-expdate-tag", member.expires_label || "");
|
||||
append(expiry, badge);
|
||||
append(userInfo, expiry);
|
||||
} else if (member.state === "expired") {
|
||||
append(userInfo, node("div", "muted-2-color em09", "已过期"));
|
||||
} else if (catalog.desc) {
|
||||
append(userInfo, node("div", "muted-2-color em09", catalog.desc));
|
||||
}
|
||||
append(userBox, userInfo);
|
||||
append(modal, userBox);
|
||||
|
||||
var tabs = node("ul", "flex jsb mt10 mb20 vip-cardminis gutters-5 nav");
|
||||
var content = node("div", "tab-content mt10");
|
||||
var activeIndex = -1;
|
||||
groups.forEach(function (group, index) {
|
||||
if (activeIndex >= 0 || !group) return;
|
||||
if (Number(group.level) === Number(view.prefer_level)) activeIndex = index;
|
||||
});
|
||||
if (activeIndex < 0 && groups.length) activeIndex = 0;
|
||||
|
||||
groups.forEach(function (group, index) {
|
||||
if (!group || !Array.isArray(group.items) || !group.items.length) return;
|
||||
var level = Number(group.level || 0);
|
||||
var action = text(group.action || "pay");
|
||||
var tabId = "tab-payvip-" + level + "-" + action;
|
||||
var active = index === activeIndex;
|
||||
var tabItem = node("li", "relative" + (active ? " active" : ""));
|
||||
var tab = node("a", "aksrt-vip-tab", actionLabel(action) + (group.name || "VIP" + level));
|
||||
tab.href = "javascript:;";
|
||||
tab.setAttribute("data-vip-tab", tabId);
|
||||
append(tabItem, tab);
|
||||
append(tabs, tabItem);
|
||||
|
||||
var pane = node("div", "tab-pane aksrt-vip-pane" + (active ? " active" : ""));
|
||||
pane.id = tabId;
|
||||
pane.hidden = !active;
|
||||
var row = node("div", "pay-vip-form-row");
|
||||
var info = node("div", "pay-vip-info level-" + level);
|
||||
var levelInfo = levels[String(level)] || {};
|
||||
append(info, node("div", "v-name font-bold", group.name || levelInfo.name || "VIP" + level));
|
||||
if (levelInfo.desc) append(info, node("div", "muted-2-color em09 mb6", levelInfo.desc));
|
||||
renderEquity(info, levelInfo.equity);
|
||||
append(row, info);
|
||||
|
||||
var products = node("div", "pay-vip-products");
|
||||
renderProducts(products, group.items);
|
||||
renderMethods(products, methods);
|
||||
var actionName = actionKey(action);
|
||||
var actionAllowed = allowed[actionName] === true;
|
||||
var reason = actionReason(reasons[actionName]);
|
||||
var canPurchase = actionAllowed && methods.length > 0;
|
||||
var submit = node("button", "but jb-red btn-block padding-lg radius aksrt-vip-submit", canPurchase ? (group.items[0].cta_label || actionLabel(action)) : (reason || "暂无可用支付方式"));
|
||||
submit.type = "button";
|
||||
submit.disabled = !canPurchase;
|
||||
submit.setAttribute("data-vip-submit", "");
|
||||
append(products, submit);
|
||||
append(products, node("p", "aksrt-vip-modal-tip muted-2-color em09 mt10"));
|
||||
append(row, products);
|
||||
append(pane, row);
|
||||
append(content, pane);
|
||||
});
|
||||
append(modal, tabs);
|
||||
append(modal, content);
|
||||
|
||||
if (catalog.help_text) append(modal, node("div", "muted-2-color em09 mb6", catalog.help_text));
|
||||
if (catalog.more) append(modal, node("div", "muted-2-color mb10 em09 muted-box padding-10 mini-radius", catalog.more));
|
||||
var points = catalog.points_exchange || {};
|
||||
if (allowed.points_exchange !== false && points.enabled && Array.isArray(points.packages) && points.packages.length && catalog.vip_url) {
|
||||
var pointsWrap = node("div", "mt10");
|
||||
var pointsLink = node("a", "but c-green btn-block", "积分兑换会员");
|
||||
pointsLink.href = text(catalog.vip_url);
|
||||
append(pointsWrap, pointsLink);
|
||||
append(modal, pointsWrap);
|
||||
}
|
||||
append(body, modal);
|
||||
}
|
||||
|
||||
function selectedMethod(root) {
|
||||
@@ -79,165 +253,155 @@
|
||||
}
|
||||
|
||||
function selectedPackage(root) {
|
||||
var checked = root.querySelector('input[name="vip_product_id"]:checked');
|
||||
var checked = root.querySelector('[data-aksrt-vip-package]:checked');
|
||||
return checked ? parseInt(checked.value, 10) : 0;
|
||||
}
|
||||
|
||||
function setTip(root, text, isError) {
|
||||
var tip = root.querySelector("[data-aksrt-vip-tip]");
|
||||
function setTip(root, message, isError) {
|
||||
var tip = root.querySelector("[data-aksrt-vip-tip]") || root.querySelector(".aksrt-vip-modal-tip");
|
||||
if (!tip) return;
|
||||
tip.textContent = text || "";
|
||||
tip.textContent = message || "";
|
||||
tip.classList.toggle("is-error", !!isError);
|
||||
}
|
||||
|
||||
function pollOrder(orderNumber) {
|
||||
if (!orderNumber) return;
|
||||
var tries = 0;
|
||||
var timer = setInterval(function () {
|
||||
tries += 1;
|
||||
api("/pay/content/order/" + encodeURIComponent(orderNumber))
|
||||
.then(function (r) {
|
||||
if (r.ok && r.json && r.json.paid) {
|
||||
clearInterval(timer);
|
||||
window.location.reload();
|
||||
}
|
||||
})
|
||||
.catch(function () {});
|
||||
if (tries >= 40) clearInterval(timer);
|
||||
}, 3000);
|
||||
var activePurchase = null;
|
||||
|
||||
function restoreButton(button) {
|
||||
if (!button) return;
|
||||
button.disabled = false;
|
||||
button.classList.remove("is-loading");
|
||||
}
|
||||
|
||||
function purchaseFromPane(pane, btn) {
|
||||
function purchaseFromPane(pane, button) {
|
||||
var packageId = selectedPackage(pane);
|
||||
if (!packageId) {
|
||||
setTip(pane, "请选择套餐", true);
|
||||
return;
|
||||
}
|
||||
if (btn.disabled) return;
|
||||
btn.disabled = true;
|
||||
btn.classList.add("is-loading");
|
||||
if (!button || button.disabled) return;
|
||||
var coordinator = window.AksrtVipPurchaseCoordinator;
|
||||
|
||||
button.disabled = true;
|
||||
button.classList.add("is-loading");
|
||||
setTip(pane, "正在创建订单…");
|
||||
|
||||
api("/pay/vip", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
package_id: packageId,
|
||||
payment_method: selectedMethod(pane),
|
||||
}),
|
||||
})
|
||||
.then(function (r) {
|
||||
btn.disabled = false;
|
||||
btn.classList.remove("is-loading");
|
||||
if (!r.ok || (r.json && r.json.code && !r.json.success && r.json.success !== true)) {
|
||||
var msg = (r.json && r.json.message) || "开通失败";
|
||||
if (r.status === 401) {
|
||||
msg = "请先登录";
|
||||
document.querySelector(".signin-loader") && document.querySelector(".signin-loader").click();
|
||||
}
|
||||
setTip(pane, msg, true);
|
||||
return;
|
||||
}
|
||||
var data = r.json && r.json.data ? r.json.data : r.json;
|
||||
if (data && data.redirect && (data.payment_url || data.qr_content)) {
|
||||
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;
|
||||
}
|
||||
window.AksrtModalRuntime.invalidate(["vip", "orders"]);
|
||||
setTip(pane, (data && data.message) || "开通成功,正在刷新…");
|
||||
setTimeout(function () {
|
||||
window.location.reload();
|
||||
}, 400);
|
||||
})
|
||||
.catch(function () {
|
||||
btn.disabled = false;
|
||||
btn.classList.remove("is-loading");
|
||||
setTip(pane, "网络错误,请重试", true);
|
||||
});
|
||||
}
|
||||
|
||||
var loading = false;
|
||||
|
||||
function vipPay(trigger) {
|
||||
var level = 1;
|
||||
if (trigger) {
|
||||
level = parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1;
|
||||
}
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
var telemetry = window.AksrtModalTelemetry;
|
||||
var telemetryId = telemetry && typeof telemetry.start === "function" ? telemetry.start("vip") : "";
|
||||
function telemetryMark(event) {
|
||||
if (telemetry && telemetryId && typeof telemetry.mark === "function") telemetry.mark(telemetryId, event);
|
||||
}
|
||||
var shell = openModal();
|
||||
var body = shell.querySelector("[data-aksrt-vip-modal-body]");
|
||||
telemetryMark("shell");
|
||||
|
||||
api("/pay/vip/modal", { query: { vip_level: level } })
|
||||
.then(function (r) {
|
||||
loading = false;
|
||||
if (r.status === 401) {
|
||||
telemetryMark("unauthorized");
|
||||
closeModal();
|
||||
activePurchase = coordinator.start({
|
||||
packageId: packageId,
|
||||
paymentMethod: selectedMethod(pane),
|
||||
tags: ["vip", "orders"],
|
||||
onPending: function (data) {
|
||||
restoreButton(button);
|
||||
if (data.payment_url) window.open(data.payment_url, "_blank");
|
||||
setTip(pane, data.message || "请完成支付,支付成功后将自动刷新。");
|
||||
},
|
||||
onSuccess: function (result) {
|
||||
activePurchase = null;
|
||||
restoreButton(button);
|
||||
setTip(pane, (result.data && result.data.message) || "开通成功,正在刷新…");
|
||||
window.setTimeout(function () { window.location.reload(); }, 400);
|
||||
},
|
||||
onFailure: function (result) {
|
||||
activePurchase = null;
|
||||
restoreButton(button);
|
||||
setTip(pane, result.data && result.data.message ? result.data.message : "支付未完成,请重试。", true);
|
||||
},
|
||||
onTimeout: function () {
|
||||
activePurchase = null;
|
||||
restoreButton(button);
|
||||
setTip(pane, "支付状态暂未确认,请稍后在订单中心查看。", true);
|
||||
},
|
||||
onError: function (response) {
|
||||
activePurchase = null;
|
||||
restoreButton(button);
|
||||
var message = response && response.json && response.json.message ? response.json.message : "开通失败";
|
||||
if (response && response.status === 401) {
|
||||
message = "请先登录";
|
||||
var login = document.querySelector(".signin-loader");
|
||||
if (login) login.click();
|
||||
return;
|
||||
}
|
||||
if (!r.ok || !r.json || !r.json.html) {
|
||||
telemetryMark("error");
|
||||
if (body) {
|
||||
body.innerHTML =
|
||||
'<div class="text-center padding-20 c-red">' +
|
||||
((r.json && r.json.message) || "加载失败") +
|
||||
"</div>";
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (body) {
|
||||
body.innerHTML = r.json.html;
|
||||
telemetryMark("first-content");
|
||||
window.AksrtFrontend.rehydrate(body, "modal");
|
||||
bindModalUi(body);
|
||||
telemetryMark("ready");
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
loading = false;
|
||||
telemetryMark("error");
|
||||
if (body) {
|
||||
body.innerHTML = '<div class="text-center padding-20 c-red">网络错误</div>';
|
||||
}
|
||||
});
|
||||
setTip(pane, message, true);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function onDocClick(e) {
|
||||
// Header CTA, float rail, UC buttons, VIP hover cards.
|
||||
var t = e.target.closest(".pay-vip, a.pay-vip, .float-btn.pay-vip, [data-aksrt-pay-vip]");
|
||||
if (!t) return;
|
||||
// Real external href without modal intent: leave alone.
|
||||
var href = (t.getAttribute("href") || "").trim();
|
||||
if (href && href !== "#" && href.indexOf("javascript:") !== 0 && !t.classList.contains("pay-vip") && !t.hasAttribute("data-aksrt-pay-vip") && !t.hasAttribute("vip-level") && !t.hasAttribute("data-vip-level")) {
|
||||
function bindModalUi(root) {
|
||||
if (!root || root.dataset.aksrtVipBound === "1") return;
|
||||
root.dataset.aksrtVipBound = "1";
|
||||
root.addEventListener("click", function (event) {
|
||||
var tab = event.target.closest("[data-vip-tab]");
|
||||
if (tab) {
|
||||
event.preventDefault();
|
||||
var id = tab.getAttribute("data-vip-tab");
|
||||
root.querySelectorAll(".aksrt-vip-tab").forEach(function (item) {
|
||||
if (item.parentElement) item.parentElement.classList.toggle("active", item === tab);
|
||||
});
|
||||
root.querySelectorAll(".aksrt-vip-pane").forEach(function (pane) {
|
||||
var active = pane.id === id;
|
||||
pane.hidden = !active;
|
||||
pane.classList.toggle("active", active);
|
||||
});
|
||||
return;
|
||||
}
|
||||
var submit = event.target.closest("[data-vip-submit]");
|
||||
if (submit) {
|
||||
event.preventDefault();
|
||||
purchaseFromPane(submit.closest(".aksrt-vip-pane") || root, submit);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function vipPay(trigger) {
|
||||
if (!window.AksrtModalRuntime.isRolloutEnabled("vip")) return;
|
||||
var level = trigger
|
||||
? parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1
|
||||
: 1;
|
||||
window.AksrtModalRuntime.openRemote({
|
||||
url: "/pay/vip/modal",
|
||||
query: { vip_level: level },
|
||||
className: "aksrt-vip-modal-shell",
|
||||
mobileBottom: false,
|
||||
skeleton: '<div class="aksrt-vip-modal-panel"><div class="aksrt-vip-modal-content" data-aksrt-vip-modal-body><div class="text-center padding-20 muted-2-color">加载中…</div></div></div>',
|
||||
telemetry: "vip",
|
||||
kind: "vip",
|
||||
responseType: "json",
|
||||
cacheTtl: 15000,
|
||||
cacheTags: ["vip"],
|
||||
renderData: function (data, session) {
|
||||
renderModalView(session.content, data);
|
||||
bindModalUi(session.content);
|
||||
},
|
||||
onError: function (error) {
|
||||
if (!error || error.code !== "unauthorized") return;
|
||||
window.AksrtModalRuntime.close("unauthorized");
|
||||
var login = document.querySelector(".signin-loader");
|
||||
if (login) login.click();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function onDocClick(event) {
|
||||
var trigger = event.target.closest(".pay-vip, a.pay-vip, .float-btn.pay-vip, [data-aksrt-pay-vip]");
|
||||
if (!trigger) return;
|
||||
var href = (trigger.getAttribute("href") || "").trim();
|
||||
if (href && href !== "#" && href.indexOf("javascript:") !== 0 && !trigger.classList.contains("pay-vip") && !trigger.hasAttribute("data-aksrt-pay-vip") && !trigger.hasAttribute("vip-level") && !trigger.hasAttribute("data-vip-level")) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
vipPay(t);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
vipPay(trigger);
|
||||
}
|
||||
|
||||
function init() {
|
||||
// Capture phase so float/header anchors with href never navigate first.
|
||||
document.addEventListener("click", onDocClick, true);
|
||||
}
|
||||
document.addEventListener("aksrt:modal:close", function (event) {
|
||||
var detail = event && event.detail ? event.detail : {};
|
||||
var options = detail.session && detail.session.options ? detail.session.options : {};
|
||||
if (options.kind !== "vip" || !activePurchase) return;
|
||||
if (typeof activePurchase.abort === "function") activePurchase.abort();
|
||||
activePurchase = null;
|
||||
});
|
||||
|
||||
// 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();
|
||||
document.addEventListener("click", onDocClick, true);
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
@@ -245,6 +409,4 @@
|
||||
} else {
|
||||
bindOnce();
|
||||
}
|
||||
|
||||
window.aksrtVipPay = vipPay;
|
||||
})();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
(function () {
|
||||
(function (window, document) {
|
||||
'use strict';
|
||||
|
||||
function parseConfig(root) {
|
||||
@@ -13,20 +13,22 @@
|
||||
});
|
||||
}
|
||||
|
||||
function request(url, config, options) {
|
||||
options = options || {};
|
||||
function request(path, config, options) {
|
||||
options = Object.assign({}, options || {});
|
||||
options.credentials = 'same-origin';
|
||||
options.headers = Object.assign({ 'X-WP-Nonce': config.nonce || '' }, options.headers || {});
|
||||
return fetch(url, options).then(function (response) {
|
||||
return response.json().then(function (payload) {
|
||||
if (!response.ok || !payload || payload.success === false) {
|
||||
throw new Error((payload && payload.message) || '请求失败');
|
||||
}
|
||||
return payload.data;
|
||||
});
|
||||
return window.AksrtRequest.request(path, options).then(function (response) {
|
||||
return response.data !== undefined ? response.data : response.json;
|
||||
});
|
||||
}
|
||||
|
||||
function errorMessage(error, fallback) {
|
||||
if (error && error.json) {
|
||||
return error.json.message || (error.json.data && error.json.data.message) || fallback;
|
||||
}
|
||||
return error && error.message ? error.message : fallback;
|
||||
}
|
||||
|
||||
function loadScript(src) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var existing = document.querySelector('script[src="' + src + '"]');
|
||||
@@ -57,81 +59,194 @@
|
||||
function init(root) {
|
||||
if (!root || root.dataset.identityBound === '1') return;
|
||||
root.dataset.identityBound = '1';
|
||||
|
||||
var config = parseConfig(root);
|
||||
var modal = root.querySelector('#uc-auth-modal');
|
||||
if (!modal) return;
|
||||
var template = root.querySelector('[data-auth-modal-template]');
|
||||
var runtime = window.AksrtModalRuntime;
|
||||
if (!template) return;
|
||||
|
||||
var activeShell = null;
|
||||
var currentUuid = config.currentUuid || '';
|
||||
var currentStage = config.currentStage || '';
|
||||
var pollTimer = null;
|
||||
var shouldPoll = false;
|
||||
var telemetry = window.AksrtModalTelemetry;
|
||||
var telemetryId = '';
|
||||
var pollBackoff = 0;
|
||||
var currentController = null;
|
||||
var refreshController = null;
|
||||
var refreshTimeout = null;
|
||||
var refreshInFlight = false;
|
||||
var openSequence = 0;
|
||||
|
||||
function telemetryMark(event) {
|
||||
if (telemetry && telemetryId && typeof telemetry.mark === 'function') telemetry.mark(telemetryId, event);
|
||||
function isOpen(shell) {
|
||||
return !!shell && shell.classList.contains('is-open') && document.contains(shell);
|
||||
}
|
||||
|
||||
function open() {
|
||||
telemetryId = telemetry && typeof telemetry.start === 'function' ? telemetry.start('identity') : '';
|
||||
modal.classList.add('is-open');
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
document.body.style.overflow = 'hidden';
|
||||
telemetryMark('shell');
|
||||
function stopPolling() {
|
||||
if (pollTimer) window.clearTimeout(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
function close() { modal.classList.remove('is-open'); modal.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; }
|
||||
root.querySelectorAll('[data-auth-modal-open]').forEach(function (button) { button.addEventListener('click', open); });
|
||||
modal.querySelectorAll('[data-auth-modal-close]').forEach(function (button) { button.addEventListener('click', close); });
|
||||
|
||||
var statusBox = modal.querySelector('[data-auth-status]');
|
||||
var createForm = modal.querySelector('[data-auth-create]');
|
||||
var manualForm = modal.querySelector('[data-auth-manual]');
|
||||
var stageNode = modal.querySelector('[data-auth-stage]');
|
||||
var messageNode = modal.querySelector('[data-auth-status-message]');
|
||||
var continueLink = modal.querySelector('[data-auth-continue]');
|
||||
var refreshButton = modal.querySelector('[data-auth-refresh]');
|
||||
var manualButton = modal.querySelector('[data-auth-manual-open]');
|
||||
function abortCurrent() {
|
||||
if (currentController) {
|
||||
try { currentController.abort(); } catch (_) {}
|
||||
}
|
||||
currentController = null;
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
if (!data) return;
|
||||
telemetryMark('first-content');
|
||||
function abortRefresh() {
|
||||
if (refreshTimeout) window.clearTimeout(refreshTimeout);
|
||||
refreshTimeout = null;
|
||||
if (refreshController) {
|
||||
try { refreshController.abort(); } catch (_) {}
|
||||
}
|
||||
refreshController = null;
|
||||
refreshInFlight = false;
|
||||
}
|
||||
|
||||
function setMessage(shell, message) {
|
||||
var node = shell && shell.querySelector('[data-auth-status-message]');
|
||||
if (node) node.textContent = message || '';
|
||||
}
|
||||
|
||||
function updateSummary(data) {
|
||||
if (!data || data.status !== 'verified') return;
|
||||
var trigger = root.querySelector('[data-auth-modal-open]');
|
||||
if (trigger) {
|
||||
trigger.disabled = true;
|
||||
trigger.textContent = '已认证';
|
||||
}
|
||||
runtime.invalidate(['identity']);
|
||||
document.dispatchEvent(new CustomEvent('aksrt:identity:changed', { detail: { status: data.status } }));
|
||||
}
|
||||
|
||||
function schedulePoll(delay) {
|
||||
stopPolling();
|
||||
if (!currentUuid || !isOpen(activeShell) || document.visibilityState !== 'visible') return;
|
||||
var base = Math.max(5, Number(delay || config.pollInterval || 5));
|
||||
var seconds = Math.min(60, base * Math.pow(2, Math.min(4, pollBackoff)));
|
||||
pollTimer = window.setTimeout(function () {
|
||||
pollTimer = null;
|
||||
refresh(false);
|
||||
}, seconds * 1000);
|
||||
}
|
||||
|
||||
function render(data, shell) {
|
||||
if (!shell || shell !== activeShell || !isOpen(shell)) return;
|
||||
data = data || {};
|
||||
var previousStage = currentStage;
|
||||
currentUuid = data.uuid || currentUuid;
|
||||
if (statusBox) statusBox.hidden = false;
|
||||
if (createForm) createForm.hidden = true;
|
||||
if (stageNode) stageNode.textContent = data.stage || data.status || '认证处理中';
|
||||
currentStage = data.stage || currentStage;
|
||||
if (data.stage && data.stage !== previousStage) runtime.invalidate(['identity']);
|
||||
var lock = ['created', 'provider_pending', 'provider_processing', 'manual_pending'].indexOf(currentStage) !== -1;
|
||||
var retryable = data.next_action === 'manual_or_retry';
|
||||
var statusBox = shell.querySelector('[data-auth-status]');
|
||||
var createForm = shell.querySelector('[data-auth-create]');
|
||||
var stageNode = shell.querySelector('[data-auth-stage]');
|
||||
var messageNode = shell.querySelector('[data-auth-status-message]');
|
||||
var continueLink = shell.querySelector('[data-auth-continue]');
|
||||
var manualButton = shell.querySelector('[data-auth-manual-open]');
|
||||
|
||||
if (statusBox) statusBox.hidden = !data.uuid && !retryable;
|
||||
if (createForm) createForm.hidden = lock || data.status === 'verified';
|
||||
if (stageNode) stageNode.textContent = data.stage || data.status || '准备认证';
|
||||
if (messageNode) messageNode.textContent = data.message || '认证结果以站点服务端主动查询为准。';
|
||||
if (continueLink) {
|
||||
var canContinue = data.next_action === 'poll' && data.redirect_url;
|
||||
continueLink.hidden = !canContinue;
|
||||
if (canContinue) continueLink.href = data.redirect_url;
|
||||
}
|
||||
if (manualButton) manualButton.hidden = data.next_action !== 'manual_or_retry';
|
||||
if (data.next_action === 'manual_or_retry' && manualForm) manualForm.hidden = true;
|
||||
shouldPoll = data.next_action === 'poll';
|
||||
if (data.status === 'verified') {
|
||||
shouldPoll = false;
|
||||
stopPolling();
|
||||
window.setTimeout(function () { window.location.reload(); }, 500);
|
||||
} else if (shouldPoll) {
|
||||
schedulePoll(data.poll_interval || 5);
|
||||
} else {
|
||||
if (manualButton) manualButton.hidden = !retryable;
|
||||
|
||||
if (data.status === 'verified' || data.next_action !== 'poll') {
|
||||
pollBackoff = 0;
|
||||
stopPolling();
|
||||
} else if (isOpen(shell) && document.visibilityState === 'visible') {
|
||||
schedulePoll(data.poll_interval || config.pollInterval);
|
||||
}
|
||||
telemetryMark('ready');
|
||||
updateSummary(data);
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (!currentUuid) return Promise.resolve();
|
||||
return request(config.restBase + '/' + encodeURIComponent(currentUuid) + '/refresh', config, { method: 'POST' }).then(render);
|
||||
function refresh(explicit) {
|
||||
if (!currentUuid || !isOpen(activeShell) || document.visibilityState !== 'visible' || refreshInFlight) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
stopPolling();
|
||||
abortRefresh();
|
||||
refreshInFlight = true;
|
||||
refreshController = window.AbortController ? new window.AbortController() : null;
|
||||
var timedOut = false;
|
||||
var shell = activeShell;
|
||||
var timeoutMs = Math.max(5000, Number(config.refreshTimeout || 15000));
|
||||
refreshTimeout = window.setTimeout(function () {
|
||||
timedOut = true;
|
||||
if (refreshController) {
|
||||
try { refreshController.abort(); } catch (_) {}
|
||||
}
|
||||
}, timeoutMs);
|
||||
|
||||
return request(config.restBase + '/' + encodeURIComponent(currentUuid) + '/refresh', config, {
|
||||
method: 'POST',
|
||||
signal: refreshController ? refreshController.signal : undefined,
|
||||
}).then(function (data) {
|
||||
if (shell !== activeShell || !isOpen(shell)) return data;
|
||||
pollBackoff = explicit ? 0 : Math.min(4, pollBackoff + 1);
|
||||
render(data, shell);
|
||||
return data;
|
||||
}).catch(function (error) {
|
||||
var aborted = error && (error.name === 'AbortError' || error.code === 'ABORT_ERR');
|
||||
if (shell !== activeShell || !isOpen(shell) || (aborted && !timedOut)) return null;
|
||||
setMessage(shell, timedOut ? '云端刷新超时,请稍后重试。' : errorMessage(error, '刷新失败,请稍后重试。'));
|
||||
pollBackoff = Math.min(4, pollBackoff + 1);
|
||||
schedulePoll(config.pollInterval || 5);
|
||||
return null;
|
||||
}).finally(function () {
|
||||
if (refreshTimeout) window.clearTimeout(refreshTimeout);
|
||||
refreshTimeout = null;
|
||||
refreshController = null;
|
||||
refreshInFlight = false;
|
||||
});
|
||||
}
|
||||
function schedulePoll(seconds) { stopPolling(); shouldPoll = true; pollTimer = window.setTimeout(function () { refresh().catch(function () {}).finally(function () { if (shouldPoll && currentUuid) schedulePoll(seconds); }); }, Math.max(3, seconds) * 1000); }
|
||||
function stopPolling() { shouldPoll = false; if (pollTimer) window.clearTimeout(pollTimer); pollTimer = null; }
|
||||
if (refreshButton) refreshButton.addEventListener('click', function () { refresh().catch(function (error) { if (messageNode) messageNode.textContent = error.message; }); });
|
||||
document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'visible' && currentUuid) refresh().catch(function () {}); });
|
||||
|
||||
function loadCurrent(shell, sequence) {
|
||||
abortCurrent();
|
||||
currentController = window.AbortController ? new window.AbortController() : null;
|
||||
var controller = currentController;
|
||||
return request(config.restBase + '/current', config, {
|
||||
method: 'GET',
|
||||
signal: controller ? controller.signal : undefined,
|
||||
}).then(function (snapshot) {
|
||||
if (sequence !== openSequence || shell !== activeShell || !isOpen(shell)) return;
|
||||
var data = snapshot && (snapshot.current || snapshot.active || snapshot.latest);
|
||||
if (!data) {
|
||||
currentUuid = '';
|
||||
currentStage = '';
|
||||
var statusBox = shell.querySelector('[data-auth-status]');
|
||||
var createForm = shell.querySelector('[data-auth-create]');
|
||||
if (statusBox) statusBox.hidden = true;
|
||||
if (createForm) createForm.hidden = false;
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
render(data, shell);
|
||||
}).catch(function (error) {
|
||||
var aborted = error && (error.name === 'AbortError' || error.code === 'ABORT_ERR');
|
||||
if (aborted || sequence !== openSequence || shell !== activeShell || !isOpen(shell)) return;
|
||||
setMessage(shell, errorMessage(error, '状态加载失败,请稍后重试。'));
|
||||
}).finally(function () {
|
||||
if (currentController === controller) currentController = null;
|
||||
});
|
||||
}
|
||||
|
||||
function bindForm(shell) {
|
||||
var createForm = shell.querySelector('[data-auth-create]');
|
||||
var manualForm = shell.querySelector('[data-auth-manual]');
|
||||
var statusBox = shell.querySelector('[data-auth-status]');
|
||||
var refreshButton = shell.querySelector('[data-auth-refresh]');
|
||||
var manualButton = shell.querySelector('[data-auth-manual-open]');
|
||||
var typeSelect = createForm && createForm.querySelector('[name="type"]');
|
||||
var enterprise = createForm && createForm.querySelector('[data-enterprise-fields]');
|
||||
var personLabel = createForm && createForm.querySelector('[data-auth-person-label]');
|
||||
var idLabel = createForm && createForm.querySelector('[data-auth-id-label]');
|
||||
|
||||
if (createForm) {
|
||||
var typeSelect = createForm.querySelector('[name="type"]');
|
||||
var enterprise = createForm.querySelector('[data-enterprise-fields]');
|
||||
var personLabel = createForm.querySelector('[data-auth-person-label]');
|
||||
var idLabel = createForm.querySelector('[data-auth-id-label]');
|
||||
function syncEnterprise() {
|
||||
var enabled = typeSelect && typeSelect.value === 'enterprise';
|
||||
if (enterprise) enterprise.hidden = !enabled;
|
||||
@@ -139,45 +254,118 @@
|
||||
if (personLabel) personLabel.textContent = enabled ? '法定代表人姓名' : '真实姓名';
|
||||
if (idLabel) idLabel.textContent = enabled ? '法定代表人身份证号' : '身份证号码';
|
||||
}
|
||||
if (typeSelect) typeSelect.addEventListener('change', syncEnterprise);
|
||||
syncEnterprise();
|
||||
createForm.addEventListener('submit', function (event) {
|
||||
|
||||
if (typeSelect) {
|
||||
typeSelect.addEventListener('change', syncEnterprise);
|
||||
syncEnterprise();
|
||||
}
|
||||
if (refreshButton) refreshButton.addEventListener('click', function () { refresh(true); });
|
||||
if (manualButton) manualButton.addEventListener('click', function () { if (manualForm) manualForm.hidden = false; });
|
||||
|
||||
if (createForm) createForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
var button = createForm.querySelector('[type="submit"]');
|
||||
var message = createForm.querySelector('[data-auth-message]');
|
||||
var form = new FormData(createForm);
|
||||
var payload = {};
|
||||
form.forEach(function (value, key) { payload[key] = value; });
|
||||
payload.consent = payload.consent === '1';
|
||||
payload.client_request_id = uuid();
|
||||
var button = createForm.querySelector('[type="submit"]');
|
||||
if (button) button.disabled = true;
|
||||
if (message) message.textContent = '';
|
||||
aliyunMetaInfo(config).then(function (metaInfo) {
|
||||
if (metaInfo) payload.meta_info = metaInfo;
|
||||
return request(config.restBase, config, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}).then(render).catch(function (error) {
|
||||
var node = createForm.querySelector('[data-auth-message]');
|
||||
if (node) node.textContent = error.message;
|
||||
}).then(function (data) {
|
||||
render(data, shell);
|
||||
}).catch(function (error) {
|
||||
if (message) message.textContent = errorMessage(error, '创建认证会话失败,请重试。');
|
||||
}).finally(function () {
|
||||
if (button) button.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
if (manualForm) manualForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
if (!currentUuid) return;
|
||||
var files = manualForm.querySelector('[name="evidence[]"]');
|
||||
if (files && files.files.length > 3) {
|
||||
setMessage(shell, '证明材料最多上传 3 张');
|
||||
return;
|
||||
}
|
||||
var button = manualForm.querySelector('[type="submit"]');
|
||||
var body = new FormData();
|
||||
Array.prototype.forEach.call((files && files.files) || [], function (file) { body.append('evidence[]', file); });
|
||||
if (button) button.disabled = true;
|
||||
request(config.restBase + '/' + encodeURIComponent(currentUuid) + '/manual', config, {
|
||||
method: 'POST',
|
||||
body: body,
|
||||
}).then(function (data) {
|
||||
manualForm.hidden = true;
|
||||
render(data, shell);
|
||||
}).catch(function (error) {
|
||||
setMessage(shell, errorMessage(error, '人工审核提交失败,请重试。'));
|
||||
}).finally(function () {
|
||||
if (button) button.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
if (statusBox && statusBox.hidden && createForm) createForm.hidden = false;
|
||||
}
|
||||
|
||||
if (manualButton) manualButton.addEventListener('click', function () { if (manualForm) manualForm.hidden = false; });
|
||||
if (manualForm) manualForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
if (!currentUuid) return;
|
||||
var files = manualForm.querySelector('[name="evidence[]"]');
|
||||
if (files && files.files.length > 3) return window.alert('证明材料最多上传 3 张');
|
||||
var body = new FormData();
|
||||
Array.prototype.forEach.call((files && files.files) || [], function (file) { body.append('evidence[]', file); });
|
||||
request(config.restBase + '/' + encodeURIComponent(currentUuid) + '/manual', config, { method: 'POST', body: body }).then(render).catch(function (error) { window.alert(error.message); });
|
||||
function open() {
|
||||
if (!runtime.isRolloutEnabled('identity')) return;
|
||||
stopPolling();
|
||||
abortCurrent();
|
||||
abortRefresh();
|
||||
openSequence += 1;
|
||||
var sequence = openSequence;
|
||||
var shell = runtime.open({
|
||||
html: template.innerHTML,
|
||||
kind: 'identity',
|
||||
className: 'aksrt-identity-modal-shell',
|
||||
mobileBottom: true,
|
||||
onContent: function (_, session) {
|
||||
activeShell = session.shell;
|
||||
bindForm(session.shell);
|
||||
},
|
||||
});
|
||||
activeShell = shell;
|
||||
loadCurrent(shell, sequence);
|
||||
}
|
||||
|
||||
root.querySelectorAll('[data-auth-modal-open]').forEach(function (button) {
|
||||
button.addEventListener('click', function (event) {
|
||||
event.preventDefault();
|
||||
open();
|
||||
});
|
||||
});
|
||||
|
||||
if (currentUuid && ['provider_pending', 'provider_processing'].indexOf(config.currentStage) !== -1) schedulePoll(5);
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (!isOpen(activeShell)) return;
|
||||
if (document.visibilityState !== 'visible') {
|
||||
stopPolling();
|
||||
abortCurrent();
|
||||
abortRefresh();
|
||||
return;
|
||||
}
|
||||
if (currentUuid && ['provider_pending', 'provider_processing'].indexOf(currentStage) !== -1) {
|
||||
schedulePoll(config.pollInterval || 5);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('aksrt:modal:close', function (event) {
|
||||
if (!activeShell || !event.detail || event.detail.shell !== activeShell) return;
|
||||
stopPolling();
|
||||
abortCurrent();
|
||||
abortRefresh();
|
||||
activeShell = null;
|
||||
});
|
||||
}
|
||||
|
||||
window.AKSRTUserCenterAuth = { init: init };
|
||||
})();
|
||||
})(window, document);
|
||||
|
||||
+240
-322
@@ -1,16 +1,36 @@
|
||||
/**
|
||||
* AKSRT user badges — zibll medal UX parity.
|
||||
* AKSRT user badges — canonical JSON/runtime path.
|
||||
*
|
||||
* User-center modal: category-grouped medal card grid + in-modal detail
|
||||
* view (medal-background card, obtain time, wear action). Also provides the
|
||||
* global wear handler used inside runtime-loaded badge modals.
|
||||
* User-center and public-author triggers share one privacy-aware REST DTO,
|
||||
* short user-scoped runtime cache, and one in-modal list/detail renderer.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
(function () {
|
||||
(function (window, document) {
|
||||
'use strict';
|
||||
|
||||
var activeState = null;
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value == null ? '' : value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function toast(message, type) {
|
||||
window.AksrtToast(message, type || 'info');
|
||||
}
|
||||
|
||||
function api(path, options) {
|
||||
return window.AksrtRequest.request(path, options || {}).then(function (response) {
|
||||
return { ok: response.ok, status: response.status, json: response.json || {} };
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
json: response.json || {},
|
||||
};
|
||||
}).catch(function (error) {
|
||||
var response = error && error.response ? error.response : {};
|
||||
return {
|
||||
@@ -21,357 +41,255 @@
|
||||
});
|
||||
}
|
||||
|
||||
function toast(msg, type) {
|
||||
window.AksrtToast(msg, type || 'info');
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s == null ? '' : s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
/* ---------- shared partials ---------- */
|
||||
|
||||
function iconHtml(item, cls) {
|
||||
function iconHtml(item, className) {
|
||||
var icon = item && item.icon_url ? String(item.icon_url) : '';
|
||||
var label = item && item.label ? String(item.label) : '';
|
||||
if (icon) {
|
||||
return '<img class="medal-icon img-icon' + (cls ? ' ' + cls : '') + '" src="' + escapeHtml(icon) + '" alt="' + escapeHtml(label) + '" title="' + escapeHtml(label) + '" loading="lazy" decoding="async">';
|
||||
return '<img class="medal-icon img-icon' + (className ? ' ' + className : '') + '" src="' + escapeHtml(icon) + '" alt="' + escapeHtml(label) + '" title="' + escapeHtml(label) + '" loading="lazy" decoding="async">';
|
||||
}
|
||||
return '<span class="medal-icon medal-icon--fa' + (cls ? ' ' + cls : '') + '"><i class="fas fa-award" aria-hidden="true"></i></span>';
|
||||
return '<span class="medal-icon medal-icon--fa' + (className ? ' ' + className : '') + '"><i class="fas fa-award" aria-hidden="true"></i></span>';
|
||||
}
|
||||
|
||||
function emptyHtml(text, desc) {
|
||||
function emptyHtml() {
|
||||
return '<div class="empty-state empty-state--sm">' +
|
||||
'<div class="empty-state__icon"><i class="fas fa-award" aria-hidden="true"></i></div>' +
|
||||
'<div class="empty-state__title">' + escapeHtml(text) + '</div>' +
|
||||
(desc ? '<div class="empty-state__desc">' + escapeHtml(desc) + '</div>' : '') +
|
||||
'<div class="empty-state__title">暂无徽章</div>' +
|
||||
'<div class="empty-state__desc">积极参与社区互动即可获得徽章</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function findItem(state, slug) {
|
||||
var categories = state.data && Array.isArray(state.data.categories) ? state.data.categories : [];
|
||||
for (var i = 0; i < categories.length; i += 1) {
|
||||
var items = Array.isArray(categories[i].items) ? categories[i].items : [];
|
||||
for (var j = 0; j < items.length; j += 1) {
|
||||
if (String(items[j].slug || '') === String(slug || '')) return items[j];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function wornItem(state) {
|
||||
var wear = state.data && state.data.wear ? state.data.wear : '';
|
||||
return wear ? findItem(state, wear) : null;
|
||||
}
|
||||
|
||||
function cardHtml(item) {
|
||||
var cls = 'medal-card' + (item.owned ? ' is-has' : ' no-has') + (item.wear ? ' is-wear' : '');
|
||||
return '<div class="' + cls + '">' +
|
||||
var classes = 'medal-card' + (item.owned ? ' is-has' : ' no-has') + (item.wear ? ' is-wear' : '');
|
||||
return '<div class="' + classes + '">' +
|
||||
iconHtml(item) +
|
||||
'<div class="medal-card__name">' + escapeHtml(item.label) + '</div>' +
|
||||
(item.wear ? '<div class="medal-card__wear">' + escapeHtml('佩戴中') + '</div>' : '') +
|
||||
(item.wear ? '<div class="medal-card__wear">佩戴中</div>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
/* ---------- user-center modal ---------- */
|
||||
function listHtml(state) {
|
||||
var data = state.data || {};
|
||||
var categories = Array.isArray(data.categories) ? data.categories : [];
|
||||
var worn = wornItem(state);
|
||||
var title = data.is_self ? '我的徽章' : '徽章明细';
|
||||
var wearHtml = worn
|
||||
? '<span class="uc-badges-head__wear-tag" title="当前佩戴:' + escapeHtml(worn.label) + '">' + iconHtml(worn) + '<span class="uc-badges-head__wear-name">' + escapeHtml(worn.label) + '</span></span>'
|
||||
: '';
|
||||
var html = '<div class="medal-modal">' +
|
||||
'<div class="uc-badges-head">' +
|
||||
'<span class="uc-badges-head__icon" aria-hidden="true"><i class="fas fa-award"></i></span>' +
|
||||
'<div class="uc-badges-head__title">' + escapeHtml(title) + '<span class="uc-badges-head__count">' + (Number(data.count || 0) > 0 ? '共 ' + Number(data.count) + ' 枚' : '') + '</span></div>' +
|
||||
'<div class="uc-badges-head__wear">' + wearHtml + '</div>' +
|
||||
'</div>';
|
||||
|
||||
function init() {
|
||||
var openers = document.querySelectorAll('[data-aksrt-badges-open]');
|
||||
var modal = document.querySelector('[data-aksrt-badges-modal]');
|
||||
if (!modal || !openers.length) return;
|
||||
if (!categories.length) return html + '<div class="medal-modal__scroll mini-scrollbar">' + emptyHtml() + '</div></div>';
|
||||
|
||||
var userId = parseInt(modal.getAttribute('data-user-id') || '0', 10);
|
||||
var canWear = modal.getAttribute('data-can-wear') === '1';
|
||||
var medalBg = modal.getAttribute('data-medal-bg') || '';
|
||||
var body = modal.querySelector('[data-aksrt-badges-body]');
|
||||
var detailBox = modal.querySelector('[data-aksrt-badges-detail]');
|
||||
var countSlot = modal.querySelector('[data-aksrt-badges-count]');
|
||||
var wearSlot = modal.querySelector('[data-aksrt-badges-wear-slot]');
|
||||
|
||||
var state = { data: null, detailSlug: '' };
|
||||
var telemetry = window.AksrtModalTelemetry;
|
||||
var telemetryId = '';
|
||||
|
||||
function telemetryMark(event) {
|
||||
if (telemetry && telemetryId && typeof telemetry.mark === 'function') telemetry.mark(telemetryId, event);
|
||||
}
|
||||
|
||||
function findItem(slug) {
|
||||
var cats = (state.data && state.data.categories) || [];
|
||||
for (var i = 0; i < cats.length; i++) {
|
||||
var items = cats[i].items || [];
|
||||
for (var j = 0; j < items.length; j++) {
|
||||
if (items[j].slug === slug) return items[j];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function wornItem() {
|
||||
var wear = state.data && state.data.wear;
|
||||
return wear ? findItem(wear) : null;
|
||||
}
|
||||
|
||||
/* ----- view switching ----- */
|
||||
function showView(name) {
|
||||
modal.querySelectorAll('[data-aksrt-badges-view]').forEach(function (view) {
|
||||
view.hidden = view.getAttribute('data-aksrt-badges-view') !== name;
|
||||
});
|
||||
if (name === 'grid') state.detailSlug = '';
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
modal.hidden = false;
|
||||
document.body.classList.add('uc-modal-open');
|
||||
}
|
||||
function closeModal() {
|
||||
modal.hidden = true;
|
||||
document.body.classList.remove('uc-modal-open');
|
||||
showView('grid');
|
||||
}
|
||||
|
||||
/* ----- render: head ----- */
|
||||
function renderHead() {
|
||||
var count = state.data && typeof state.data.count === 'number' ? state.data.count : 0;
|
||||
if (countSlot) {
|
||||
countSlot.textContent = count > 0 ? '共 ' + count + ' 枚' : '';
|
||||
}
|
||||
if (!wearSlot) return;
|
||||
var worn = wornItem();
|
||||
wearSlot.innerHTML = worn
|
||||
? '<span class="uc-badges-head__wear-tag" title="' + escapeHtml('当前佩戴:' + worn.label) + '">' + iconHtml(worn) + '<span class="uc-badges-head__wear-name">' + escapeHtml(worn.label) + '</span></span>'
|
||||
: '';
|
||||
}
|
||||
|
||||
/* ----- render: grid ----- */
|
||||
function renderGrid() {
|
||||
if (!body) return;
|
||||
var cats = (state.data && state.data.categories) || [];
|
||||
if (!cats.length) {
|
||||
body.innerHTML = emptyHtml('暂无徽章', '积极参与社区互动即可获得徽章');
|
||||
return;
|
||||
}
|
||||
body.innerHTML = cats.map(function (cat) {
|
||||
var cards = (cat.items || []).map(function (item) {
|
||||
return '<button type="button" class="medal-list" data-aksrt-badge-detail="' + escapeHtml(item.slug) + '" aria-label="' + escapeHtml(item.label) + '">' +
|
||||
cardHtml(item) +
|
||||
'</button>';
|
||||
}).join('');
|
||||
return '<div class="medal-cat-box">' +
|
||||
'<div class="medal-cat-head"><b class="medal-cat-name">' + escapeHtml(cat.cat_name || '') + '</b></div>' +
|
||||
'<div class="medal-list-box">' + cards + '</div>' +
|
||||
'</div>';
|
||||
html += '<div class="medal-modal__scroll mini-scrollbar">';
|
||||
categories.forEach(function (category) {
|
||||
var items = Array.isArray(category.items) ? category.items : [];
|
||||
var cards = items.map(function (item) {
|
||||
return '<button type="button" class="medal-list" data-aksrt-badge-detail="' + escapeHtml(item.slug) + '" aria-label="' + escapeHtml(item.label) + '">' + cardHtml(item) + '</button>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/* ----- render: detail ----- */
|
||||
function renderDetail(slug) {
|
||||
var item = findItem(slug);
|
||||
if (!item || !detailBox) return;
|
||||
state.detailSlug = slug;
|
||||
|
||||
var stateLine = '';
|
||||
if (item.owned && item.time) {
|
||||
stateLine = '<div class="medal-single-state c-blue">' + escapeHtml(item.time + ' 获得') + '</div>';
|
||||
} else if (!item.owned) {
|
||||
stateLine = '<div class="medal-single-state c-yellow">' + escapeHtml('暂未获得') + '</div>';
|
||||
}
|
||||
|
||||
var action = '';
|
||||
if (canWear && item.owned && !item.wear) {
|
||||
action = '<button type="button" class="but jb-blue btn-block radius" data-aksrt-badge-wear="' + escapeHtml(item.slug) + '">' + escapeHtml('佩戴此徽章') + '</button>';
|
||||
} else if (item.wear) {
|
||||
action = '<span class="badg btn-block c-blue">' + escapeHtml('当前佩戴此徽章') + '</span>';
|
||||
}
|
||||
|
||||
detailBox.innerHTML =
|
||||
'<div class="uc-badges-detail__head">' +
|
||||
'<button type="button" class="uc-badges-back" data-aksrt-badges-back><i class="fas fa-angle-left" aria-hidden="true"></i> ' + escapeHtml('返回列表') + '</button>' +
|
||||
'</div>' +
|
||||
'<div class="medal-single-card">' +
|
||||
(medalBg ? '<div class="medal-single-bg"><img class="fit-cover" src="' + escapeHtml(medalBg) + '" alt=""></div>' : '') +
|
||||
'<div class="medal-single-main">' +
|
||||
iconHtml(item) +
|
||||
'<div class="medal-single-name">' + escapeHtml(item.label) + '</div>' +
|
||||
(item.desc ? '<div class="medal-single-desc muted-2-color">' + escapeHtml(item.desc) + '</div>' : '') +
|
||||
stateLine +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
(action ? '<div class="medal-modal__actions">' + action + '</div>' : '');
|
||||
|
||||
showView('detail');
|
||||
}
|
||||
|
||||
/* ----- header worn icon sync (profile header, next to the name) ----- */
|
||||
function syncHeaderIcon() {
|
||||
var worn = wornItem();
|
||||
if (!worn || !worn.icon_url) return;
|
||||
document.querySelectorAll('.aksrt-identity-medal.is-wear').forEach(function (node) {
|
||||
if (node.tagName === 'IMG') {
|
||||
node.src = worn.icon_url;
|
||||
node.alt = worn.label;
|
||||
node.title = worn.label;
|
||||
} else {
|
||||
var img = document.createElement('img');
|
||||
img.className = 'medal-icon img-icon ml3 aksrt-identity-medal is-wear';
|
||||
img.src = worn.icon_url;
|
||||
img.alt = worn.label;
|
||||
img.title = worn.label;
|
||||
img.setAttribute('loading', 'lazy');
|
||||
img.setAttribute('decoding', 'async');
|
||||
node.parentNode.replaceChild(img, node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ----- data ----- */
|
||||
function load() {
|
||||
if (!userId) return Promise.resolve();
|
||||
if (body) {
|
||||
body.innerHTML = '<div class="uc-badges-loading muted-2-color em09"><i class="fas fa-spinner fa-spin" aria-hidden="true"></i> ' + escapeHtml('加载中…') + '</div>';
|
||||
}
|
||||
return api('/users/' + userId + '/badges').then(function (r) {
|
||||
if (!r.ok || !r.json || !r.json.data) {
|
||||
telemetryMark('error');
|
||||
if (body) {
|
||||
body.innerHTML = '<div class="uc-badges-error">' +
|
||||
'<span class="c-red em09">' + escapeHtml('加载失败') + '</span>' +
|
||||
'<button type="button" class="but but-sm" data-aksrt-badges-retry>' + escapeHtml('重试') + '</button>' +
|
||||
'</div>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
state.data = r.json.data;
|
||||
telemetryMark('first-content');
|
||||
renderHead();
|
||||
renderGrid();
|
||||
telemetryMark('ready');
|
||||
});
|
||||
}
|
||||
|
||||
function wear(slug, btn) {
|
||||
btn.disabled = true;
|
||||
api('/users/me/badges/wear', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ slug: slug }),
|
||||
}).then(function (r) {
|
||||
if (!r.ok) {
|
||||
toast((r.json && (r.json.message || (r.json.data && r.json.data.message))) || '佩戴失败', 'error');
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
toast((r.json && r.json.message) || '已佩戴徽章', 'success');
|
||||
window.AksrtModalRuntime.invalidate(['badges', 'identity']);
|
||||
state.data = (r.json && r.json.data) || state.data;
|
||||
renderHead();
|
||||
renderGrid();
|
||||
renderDetail(slug);
|
||||
syncHeaderIcon();
|
||||
}).catch(function () {
|
||||
toast('网络错误', 'error');
|
||||
btn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
/* ----- events ----- */
|
||||
openers.forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
telemetryId = telemetry && typeof telemetry.start === 'function' ? telemetry.start('badges') : '';
|
||||
openModal();
|
||||
telemetryMark('shell');
|
||||
showView('grid');
|
||||
load();
|
||||
});
|
||||
html += '<div class="medal-cat-box"><div class="medal-cat-head"><b class="medal-cat-name">' + escapeHtml(category.cat_name || '') + '</b></div><div class="medal-list-box">' + cards + '</div></div>';
|
||||
});
|
||||
return html + '</div></div>';
|
||||
}
|
||||
|
||||
modal.querySelectorAll('[data-aksrt-badges-close]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
closeModal();
|
||||
});
|
||||
});
|
||||
function detailHtml(state) {
|
||||
var item = findItem(state, state.detailSlug);
|
||||
if (!item) return listHtml(state);
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key !== 'Escape' || modal.hidden) return;
|
||||
var detailView = modal.querySelector('[data-aksrt-badges-view="detail"]');
|
||||
// Detail view open: ESC steps back to the grid first.
|
||||
if (detailView && !detailView.hidden) {
|
||||
showView('grid');
|
||||
return;
|
||||
}
|
||||
closeModal();
|
||||
});
|
||||
var stateLine = '';
|
||||
if (item.owned && item.time) {
|
||||
stateLine = '<div class="medal-single-state c-blue">' + escapeHtml(item.time + ' 获得') + '</div>';
|
||||
} else if (!item.owned) {
|
||||
stateLine = '<div class="medal-single-state c-yellow">暂未获得</div>';
|
||||
}
|
||||
|
||||
modal.addEventListener('click', function (e) {
|
||||
var back = e.target.closest('[data-aksrt-badges-back]');
|
||||
if (back) {
|
||||
e.preventDefault();
|
||||
showView('grid');
|
||||
return;
|
||||
}
|
||||
var retry = e.target.closest('[data-aksrt-badges-retry]');
|
||||
if (retry) {
|
||||
e.preventDefault();
|
||||
load();
|
||||
return;
|
||||
}
|
||||
var wearBtn = e.target.closest('[data-aksrt-badge-wear]');
|
||||
if (wearBtn && canWear) {
|
||||
e.preventDefault();
|
||||
wear(wearBtn.getAttribute('data-aksrt-badge-wear') || '', wearBtn);
|
||||
return;
|
||||
}
|
||||
var card = e.target.closest('[data-aksrt-badge-detail]');
|
||||
if (card) {
|
||||
e.preventDefault();
|
||||
renderDetail(card.getAttribute('data-aksrt-badge-detail') || '');
|
||||
var action = '';
|
||||
if (state.canWear && item.owned && !item.wear) {
|
||||
action = '<button type="button" class="but jb-blue btn-block radius" data-aksrt-badge-wear="' + escapeHtml(item.slug) + '">佩戴此徽章</button>';
|
||||
} else if (item.wear) {
|
||||
action = '<span class="badg btn-block c-blue">当前佩戴此徽章</span>';
|
||||
}
|
||||
|
||||
return '<div class="medal-modal medal-modal--single">' +
|
||||
'<div class="uc-badges-detail__head"><button type="button" class="uc-badges-back" data-aksrt-badges-back><i class="fas fa-angle-left" aria-hidden="true"></i> 返回列表</button></div>' +
|
||||
'<div class="medal-single-card"><div class="medal-single-bg"><img class="fit-cover" src="' + escapeHtml((window.aksrtData && window.aksrtData.themeUri ? window.aksrtData.themeUri : '') + '/assets/img/medal/medal-background.svg') + '" alt=""></div><div class="medal-single-main">' +
|
||||
iconHtml(item) + '<div class="medal-single-name">' + escapeHtml(item.label) + '</div>' +
|
||||
(item.desc ? '<div class="medal-single-desc muted-2-color">' + escapeHtml(item.desc) + '</div>' : '') + stateLine +
|
||||
'</div></div>' + (action ? '<div class="medal-modal__actions">' + action + '</div>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderState(state) {
|
||||
return state.detailSlug ? detailHtml(state) : listHtml(state);
|
||||
}
|
||||
|
||||
function rehydrate(content) {
|
||||
window.AksrtFrontend.rehydrate(content, 'modal');
|
||||
}
|
||||
|
||||
function renderInto(state) {
|
||||
if (!state.session || state.session.closed || !state.session.content) return;
|
||||
state.session.content.innerHTML = renderState(state);
|
||||
rehydrate(state.session.content);
|
||||
bindContent(state);
|
||||
}
|
||||
|
||||
function syncHeaderIcon(state) {
|
||||
var worn = wornItem(state);
|
||||
if (!worn || !worn.icon_url) return;
|
||||
document.querySelectorAll('.aksrt-identity-medal.is-wear').forEach(function (node) {
|
||||
if (node.tagName === 'IMG') {
|
||||
node.src = worn.icon_url;
|
||||
node.alt = worn.label;
|
||||
node.title = worn.label;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- remote modal: global wear handler ---------- */
|
||||
document.addEventListener('click', function (e) {
|
||||
var wearBtn = e.target.closest('[data-aksrt-badge-wear]');
|
||||
if (!wearBtn) return;
|
||||
// UC modal has its own handler when canWear on modal root.
|
||||
var modalRoot = wearBtn.closest('[data-aksrt-badges-modal]');
|
||||
if (modalRoot) return;
|
||||
e.preventDefault();
|
||||
var slug = wearBtn.getAttribute('data-aksrt-badge-wear') || '';
|
||||
if (!slug) return;
|
||||
wearBtn.disabled = true;
|
||||
function wear(state, slug, button) {
|
||||
if (!state.canWear || state.wearing || !slug) return;
|
||||
state.wearing = true;
|
||||
state.wearController = window.AbortController ? new window.AbortController() : null;
|
||||
if (button) button.disabled = true;
|
||||
api('/users/me/badges/wear', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ slug: slug }),
|
||||
}).then(function (r) {
|
||||
if (!r.ok) {
|
||||
toast((r.json && (r.json.message || (r.json.data && r.json.data.message))) || '佩戴失败', 'error');
|
||||
wearBtn.disabled = false;
|
||||
signal: state.wearController ? state.wearController.signal : undefined,
|
||||
}).then(function (result) {
|
||||
if (state.session && state.session.closed) return;
|
||||
if (!result.ok) {
|
||||
toast((result.json && (result.json.message || (result.json.data && result.json.data.message))) || '佩戴失败', 'error');
|
||||
if (button) button.disabled = false;
|
||||
return;
|
||||
}
|
||||
toast((r.json && r.json.message) || '已佩戴徽章', 'success');
|
||||
window.AksrtModalRuntime.invalidate(['badges', 'identity']);
|
||||
// Re-open the refreshed single-badge modal so state stays in sync.
|
||||
var refreshed = null;
|
||||
try {
|
||||
var uid = parseInt((r.json && r.json.data && r.json.data.user_id) || '0', 10);
|
||||
var data = r.json && r.json.data;
|
||||
var isSelf = data && data.is_self;
|
||||
if (isSelf && data.wear) {
|
||||
refreshed = uid > 0 && data.wear
|
||||
? window.AksrtRequest.url('/users/' + uid + '/badges/' + encodeURIComponent(data.wear) + '/modal')
|
||||
: null;
|
||||
}
|
||||
} catch (_) {}
|
||||
if (refreshed) {
|
||||
window.AksrtModalRuntime.openRemote({
|
||||
url: refreshed,
|
||||
className: 'modal-mini',
|
||||
mobileBottom: true,
|
||||
kind: 'badges',
|
||||
cache: false,
|
||||
});
|
||||
} else {
|
||||
window.AksrtModalRuntime.close('badge-wear');
|
||||
}
|
||||
}).catch(function () {
|
||||
if (result.json && result.json.data) state.data = result.json.data;
|
||||
state.detailSlug = slug;
|
||||
window.AksrtModalRuntime.invalidate(['badges', 'identity', 'badges:user:' + state.userId]);
|
||||
toast((result.json && result.json.message) || '已佩戴徽章', 'success');
|
||||
renderInto(state);
|
||||
syncHeaderIcon(state);
|
||||
}).catch(function (error) {
|
||||
if (error && error.name === 'AbortError') return;
|
||||
toast('网络错误', 'error');
|
||||
wearBtn.disabled = false;
|
||||
if (button) button.disabled = false;
|
||||
}).finally(function () {
|
||||
state.wearing = false;
|
||||
state.wearController = null;
|
||||
});
|
||||
}
|
||||
|
||||
function bindContent(state) {
|
||||
if (!state.session || !state.session.content || state.session.content.dataset.aksrtBadgesBound === '1') return;
|
||||
state.session.content.dataset.aksrtBadgesBound = '1';
|
||||
state.session.content.addEventListener('click', function (event) {
|
||||
var back = event.target.closest('[data-aksrt-badges-back]');
|
||||
if (back) {
|
||||
event.preventDefault();
|
||||
state.detailSlug = '';
|
||||
renderInto(state);
|
||||
return;
|
||||
}
|
||||
var detail = event.target.closest('[data-aksrt-badge-detail]');
|
||||
if (detail) {
|
||||
event.preventDefault();
|
||||
state.detailSlug = detail.getAttribute('data-aksrt-badge-detail') || '';
|
||||
renderInto(state);
|
||||
return;
|
||||
}
|
||||
var wearButton = event.target.closest('[data-aksrt-badge-wear]');
|
||||
if (wearButton) {
|
||||
event.preventDefault();
|
||||
wear(state, wearButton.getAttribute('data-aksrt-badge-wear') || '', wearButton);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openBadges(trigger) {
|
||||
var runtime = window.AksrtModalRuntime;
|
||||
if (!runtime.isRolloutEnabled("badge")) return;
|
||||
var userId = parseInt(trigger.getAttribute('data-aksrt-badges-user-id') || '0', 10);
|
||||
if (!userId) return;
|
||||
if (activeState && activeState.wearController) {
|
||||
try { activeState.wearController.abort(); } catch (_) {}
|
||||
}
|
||||
|
||||
var state = {
|
||||
userId: userId,
|
||||
canWear: trigger.getAttribute('data-aksrt-badges-can-wear') === '1',
|
||||
data: null,
|
||||
detailSlug: '',
|
||||
session: null,
|
||||
wearing: false,
|
||||
wearController: null,
|
||||
};
|
||||
activeState = state;
|
||||
runtime.openRemote({
|
||||
url: '/users/' + userId + '/badges',
|
||||
kind: 'badges',
|
||||
title: '徽章',
|
||||
className: 'modal-mini full-sm aksrt-badges-modal-shell',
|
||||
mobileBottom: true,
|
||||
skeleton: '<div class="medal-modal"><div class="uc-badges-head"><span class="aksrt-modal-runtime__skeleton-line is-short"></span><span class="aksrt-modal-runtime__skeleton-line is-wide"></span></div><div class="medal-list-box aksrt-modal-runtime__skeleton"><span class="aksrt-modal-runtime__skeleton-block"></span><span class="aksrt-modal-runtime__skeleton-block"></span><span class="aksrt-modal-runtime__skeleton-block"></span></div></div>',
|
||||
cacheTtl: 12000,
|
||||
cacheTags: ['badges', 'identity', 'badges:user:' + userId],
|
||||
responseType: 'json',
|
||||
renderData: function (data, session) {
|
||||
state.session = session;
|
||||
state.data = data || {};
|
||||
state.canWear = state.canWear && state.data.can_wear === true;
|
||||
session.content.innerHTML = renderState(state);
|
||||
},
|
||||
onData: function (data, session) {
|
||||
state.session = session;
|
||||
state.data = data || state.data;
|
||||
state.canWear = state.canWear && state.data.can_wear === true;
|
||||
bindContent(state);
|
||||
syncHeaderIcon(state);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('aksrt:modal:close', function (event) {
|
||||
if (!activeState || !event.detail || event.detail.session !== activeState.session) return;
|
||||
if (activeState.wearController) {
|
||||
try { activeState.wearController.abort(); } catch (_) {}
|
||||
}
|
||||
activeState = null;
|
||||
});
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
function bindOnce() {
|
||||
if (window.__aksrtBadgesBound) return;
|
||||
window.__aksrtBadgesBound = true;
|
||||
document.addEventListener('click', function (event) {
|
||||
var trigger = event.target.closest('[data-aksrt-badges-open]');
|
||||
if (!trigger) return;
|
||||
event.preventDefault();
|
||||
openBadges(trigger);
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', bindOnce, { once: true });
|
||||
} else {
|
||||
bindOnce();
|
||||
}
|
||||
})(window, document);
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
/**
|
||||
* Canonical VIP purchase and order polling coordinator.
|
||||
*
|
||||
* Purchase requests are deduplicated by package/payment method. External
|
||||
* orders use bounded, visibility-aware polling with AbortController support;
|
||||
* terminal states invalidate the user-scoped modal cache exactly once.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
(function (window, document) {
|
||||
"use strict";
|
||||
|
||||
var ORDER_PATH = "/pay/content/order/";
|
||||
var DEFAULT_MAX_ATTEMPTS = 12;
|
||||
var DEFAULT_MAX_ELAPSED = 120000;
|
||||
var MAX_DELAY = 15000;
|
||||
var createFlights = Object.create(null);
|
||||
var polls = Object.create(null);
|
||||
var SUCCESS_STATUSES = {
|
||||
paid: true,
|
||||
delivered: true,
|
||||
completed: true,
|
||||
pending_review: true,
|
||||
};
|
||||
var FAILURE_STATUSES = {
|
||||
cancelled: true,
|
||||
expired: true,
|
||||
refunded: true,
|
||||
failed: true,
|
||||
};
|
||||
|
||||
function userScope() {
|
||||
var config = window.aksrtData || window.aksrt || {};
|
||||
var userId = parseInt(config.userId || config.user_id || 0, 10) || 0;
|
||||
if (userId > 0) return "user:" + userId;
|
||||
return config.isLoggedIn ? "auth" : "guest";
|
||||
}
|
||||
|
||||
function clearFlights() {
|
||||
createFlights = Object.create(null);
|
||||
Object.keys(polls).forEach(function (orderNumber) {
|
||||
var active = polls[orderNumber];
|
||||
if (active && active.handle && typeof active.handle.abort === "function") active.handle.abort();
|
||||
});
|
||||
polls = Object.create(null);
|
||||
}
|
||||
|
||||
["aksrt:auth:logout", "aksrt:account-switch", "aksrt:logout", "aksrt:user:logout", "aksrt:auth:changed", "wp_logout"].forEach(function (eventName) {
|
||||
document.addEventListener(eventName, clearFlights);
|
||||
});
|
||||
|
||||
function responseData(result) {
|
||||
if (!result) return {};
|
||||
if (result.data !== undefined && result.data !== null) return result.data || {};
|
||||
if (result.json && result.json.data !== undefined && result.json.data !== null) return result.json.data || {};
|
||||
return result.json || {};
|
||||
}
|
||||
|
||||
function normalizeError(error) {
|
||||
var response = error && error.response ? error.response : {};
|
||||
var json = error && error.json ? error.json : (response.json || {});
|
||||
return {
|
||||
ok: false,
|
||||
status: Number((error && error.status) || response.status || 0),
|
||||
json: json || { message: error && error.message ? error.message : "请求失败" },
|
||||
};
|
||||
}
|
||||
|
||||
function request(path, options) {
|
||||
return window.AksrtRequest.request(path, options || {}).then(function (result) {
|
||||
return {
|
||||
ok: true,
|
||||
status: Number(result.status || 200),
|
||||
json: result.json || {},
|
||||
data: responseData(result),
|
||||
};
|
||||
}).catch(normalizeError);
|
||||
}
|
||||
|
||||
function invalidate(tags) {
|
||||
window.AksrtModalRuntime.invalidate(tags || ["vip", "orders"]);
|
||||
}
|
||||
|
||||
function callback(options, name) {
|
||||
var args = Array.prototype.slice.call(arguments, 2);
|
||||
if (!options || typeof options[name] !== "function") return;
|
||||
try {
|
||||
options[name].apply(null, args);
|
||||
} catch (error) {
|
||||
if (window.console && typeof window.console.error === "function") {
|
||||
window.console.error("[AKSRT VIP] callback failed", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function purchase(options) {
|
||||
options = options || {};
|
||||
var packageId = parseInt(options.packageId || options.package_id || 0, 10);
|
||||
var method = String(options.paymentMethod || options.payment_method || "balance");
|
||||
if (!packageId) {
|
||||
return Promise.resolve({ ok: false, status: 400, json: { message: "套餐无效" } });
|
||||
}
|
||||
|
||||
var key = userScope() + ":" + String(packageId) + ":" + method;
|
||||
if (createFlights[key]) return createFlights[key];
|
||||
|
||||
var promise = request("/pay/vip", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
package_id: packageId,
|
||||
payment_method: method,
|
||||
}),
|
||||
signal: options.signal,
|
||||
});
|
||||
createFlights[key] = promise;
|
||||
promise.then(function () {
|
||||
if (createFlights[key] === promise) delete createFlights[key];
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
function exchange(options) {
|
||||
options = options || {};
|
||||
var packageId = parseInt(options.packageId || options.package_id || 0, 10);
|
||||
if (!packageId) {
|
||||
return Promise.resolve({ ok: false, status: 400, json: { message: "兑换套餐无效" } });
|
||||
}
|
||||
return request("/pay/vip/points-exchange", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ package_id: packageId }),
|
||||
signal: options.signal,
|
||||
}).then(function (response) {
|
||||
if (response.ok) invalidate(options.tags || ["vip", "orders"]);
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
function isTerminal(status, data) {
|
||||
if (data && data.paid) return true;
|
||||
return !!(SUCCESS_STATUSES[status] || FAILURE_STATUSES[status]);
|
||||
}
|
||||
|
||||
function backoff(attempt) {
|
||||
return Math.min(MAX_DELAY, 1000 * Math.pow(2, Math.max(0, attempt - 1)));
|
||||
}
|
||||
|
||||
function poll(orderNumber, options) {
|
||||
options = options || {};
|
||||
orderNumber = String(orderNumber || "");
|
||||
if (!orderNumber) {
|
||||
return {
|
||||
promise: Promise.resolve({ terminal: true, success: false, status: "invalid" }),
|
||||
abort: function () {},
|
||||
};
|
||||
}
|
||||
if (polls[orderNumber]) return polls[orderNumber].handle;
|
||||
|
||||
var maxAttempts = Math.max(1, Number(options.maxAttempts || DEFAULT_MAX_ATTEMPTS));
|
||||
var maxElapsed = Math.max(1000, Number(options.maxElapsed || DEFAULT_MAX_ELAPSED));
|
||||
var state = {
|
||||
attempt: 0,
|
||||
startedAt: Date.now(),
|
||||
timer: null,
|
||||
controller: null,
|
||||
requestTimer: null,
|
||||
waitingForVisible: false,
|
||||
done: false,
|
||||
aborted: false,
|
||||
resolve: null,
|
||||
onVisibility: null,
|
||||
};
|
||||
|
||||
state.promise = new Promise(function (resolve) {
|
||||
state.resolve = resolve;
|
||||
});
|
||||
|
||||
function finish(result) {
|
||||
if (state.done) return;
|
||||
state.done = true;
|
||||
if (state.timer) {
|
||||
window.clearTimeout(state.timer);
|
||||
state.timer = null;
|
||||
}
|
||||
if (state.requestTimer) {
|
||||
window.clearTimeout(state.requestTimer);
|
||||
state.requestTimer = null;
|
||||
}
|
||||
if (state.controller) {
|
||||
try { state.controller.abort(); } catch (error) {}
|
||||
state.controller = null;
|
||||
}
|
||||
if (state.onVisibility) document.removeEventListener("visibilitychange", state.onVisibility);
|
||||
if (polls[orderNumber] && polls[orderNumber].handle === state.handle) delete polls[orderNumber];
|
||||
state.resolve(result);
|
||||
}
|
||||
|
||||
function terminal(data, response, status) {
|
||||
var success = !!(data && data.paid) || !!SUCCESS_STATUSES[status];
|
||||
invalidate(options.tags || ["vip", "orders"]);
|
||||
callback(options, "onTerminal", {
|
||||
terminal: true,
|
||||
success: success,
|
||||
status: status,
|
||||
data: data || {},
|
||||
response: response || null,
|
||||
});
|
||||
finish({
|
||||
terminal: true,
|
||||
success: success,
|
||||
status: status,
|
||||
data: data || {},
|
||||
response: response || null,
|
||||
});
|
||||
}
|
||||
|
||||
function schedule(delay) {
|
||||
if (state.done || state.aborted) return;
|
||||
if (document.hidden) {
|
||||
state.waitingForVisible = true;
|
||||
return;
|
||||
}
|
||||
state.waitingForVisible = false;
|
||||
state.timer = window.setTimeout(run, Math.max(0, delay || 0));
|
||||
}
|
||||
|
||||
function run() {
|
||||
state.timer = null;
|
||||
if (state.done || state.aborted) return;
|
||||
if (document.hidden) {
|
||||
state.waitingForVisible = true;
|
||||
return;
|
||||
}
|
||||
if (state.attempt >= maxAttempts || Date.now() - state.startedAt >= maxElapsed) {
|
||||
callback(options, "onTimeout", { terminal: false, timedOut: true, orderNumber: orderNumber });
|
||||
finish({ terminal: false, timedOut: true, status: "timeout" });
|
||||
return;
|
||||
}
|
||||
|
||||
state.attempt += 1;
|
||||
state.controller = window.AbortController ? new window.AbortController() : null;
|
||||
var requestOptions = {};
|
||||
if (state.controller) requestOptions.signal = state.controller.signal;
|
||||
var remaining = Math.max(1000, maxElapsed - (Date.now() - state.startedAt));
|
||||
state.requestTimer = window.setTimeout(function () {
|
||||
if (state.controller) {
|
||||
try { state.controller.abort(); } catch (error) {}
|
||||
}
|
||||
}, Math.min(20000, remaining));
|
||||
request(ORDER_PATH + encodeURIComponent(orderNumber), requestOptions).then(function (response) {
|
||||
if (state.requestTimer) {
|
||||
window.clearTimeout(state.requestTimer);
|
||||
state.requestTimer = null;
|
||||
}
|
||||
state.controller = null;
|
||||
if (state.done || state.aborted) return;
|
||||
var data = response.data || response.json || {};
|
||||
var status = String(data.status || (data.paid ? "paid" : "")).toLowerCase();
|
||||
callback(options, "onStatus", { status: status, data: data, response: response });
|
||||
if (response.ok && isTerminal(status, data)) {
|
||||
terminal(data, response, status || "paid");
|
||||
return;
|
||||
}
|
||||
if (!response.ok && [401, 403, 404].indexOf(response.status) >= 0) {
|
||||
terminal(data, response, "error");
|
||||
return;
|
||||
}
|
||||
if (state.attempt >= maxAttempts || Date.now() - state.startedAt >= maxElapsed) {
|
||||
callback(options, "onTimeout", { terminal: false, timedOut: true, orderNumber: orderNumber });
|
||||
finish({ terminal: false, timedOut: true, status: "timeout", response: response });
|
||||
return;
|
||||
}
|
||||
schedule(backoff(state.attempt));
|
||||
});
|
||||
}
|
||||
|
||||
state.onVisibility = function () {
|
||||
if (document.hidden || state.done || state.aborted) return;
|
||||
if (state.waitingForVisible) {
|
||||
schedule(0);
|
||||
}
|
||||
};
|
||||
document.addEventListener("visibilitychange", state.onVisibility);
|
||||
|
||||
state.handle = {
|
||||
promise: state.promise,
|
||||
abort: function () {
|
||||
if (state.done) return;
|
||||
state.aborted = true;
|
||||
finish({ terminal: false, aborted: true, status: "aborted" });
|
||||
},
|
||||
};
|
||||
polls[orderNumber] = state;
|
||||
schedule(0);
|
||||
return state.handle;
|
||||
}
|
||||
|
||||
function start(options) {
|
||||
options = Object.assign({}, options || {});
|
||||
var activePoll = null;
|
||||
var stopped = false;
|
||||
var controller = window.AbortController ? new window.AbortController() : null;
|
||||
if (controller) options.signal = controller.signal;
|
||||
var promise = purchase(options).then(function (response) {
|
||||
if (stopped) return { ok: false, aborted: true, status: "aborted" };
|
||||
var data = response.data || response.json || {};
|
||||
if (!response.ok) {
|
||||
callback(options, "onError", response);
|
||||
return response;
|
||||
}
|
||||
if (data.redirect && (data.payment_url || data.qr_content)) {
|
||||
callback(options, "onPending", data);
|
||||
if (!data.order_number) {
|
||||
var missing = { ok: false, status: 500, json: { message: "支付订单号缺失" } };
|
||||
callback(options, "onError", missing);
|
||||
return missing;
|
||||
}
|
||||
activePoll = poll(data.order_number, options);
|
||||
activePoll.promise.then(function (result) {
|
||||
if (result.success) callback(options, "onSuccess", result);
|
||||
else if (result.terminal) callback(options, "onFailure", result);
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
pending: true,
|
||||
data: data,
|
||||
poll: activePoll,
|
||||
};
|
||||
}
|
||||
invalidate(options.tags || ["vip", "orders"]);
|
||||
callback(options, "onSuccess", { terminal: true, success: true, data: data, response: response });
|
||||
return { ok: true, pending: false, data: data, response: response };
|
||||
});
|
||||
|
||||
return {
|
||||
promise: promise,
|
||||
abort: function () {
|
||||
stopped = true;
|
||||
if (controller) {
|
||||
try { controller.abort(); } catch (error) {}
|
||||
}
|
||||
if (activePoll && typeof activePoll.abort === "function") activePoll.abort();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
window.AksrtVipPurchaseCoordinator = {
|
||||
purchase: purchase,
|
||||
exchange: exchange,
|
||||
poll: poll,
|
||||
start: start,
|
||||
abort: function (orderNumber) {
|
||||
var active = polls[String(orderNumber || "")];
|
||||
if (active && active.handle) active.handle.abort();
|
||||
},
|
||||
};
|
||||
})(window, document);
|
||||
Reference in New Issue
Block a user