diff --git a/assets/css/frontend/vip-modal.css b/assets/css/frontend/vip-modal.css index 51ef29a4..c9cb0f0c 100644 --- a/assets/css/frontend/vip-modal.css +++ b/assets/css/frontend/vip-modal.css @@ -274,7 +274,6 @@ cursor: wait; } -.aksrt-vip-modal-tip.is-error, .uc-vip-shop__tip.is-error { color: #e74c3c; } diff --git a/assets/js/pay-vip.js b/assets/js/pay-vip.js index 662220b0..9dd08f53 100644 --- a/assets/js/pay-vip.js +++ b/assets/js/pay-vip.js @@ -189,7 +189,6 @@ submit.setAttribute("data-aksrt-vip-points-submit", ""); submit.disabled = !view.enabled || !exchange.enabled || (view.allowed_actions && view.allowed_actions.points_exchange === false) || !packages.some(function (item) { return item && Number(member.points || 0) >= Number(item.points || 0); }); append(modal, submit); - append(modal, node("p", "aksrt-vip-points-modal__tip aksrt-vip-modal-tip", "")); if (exchange.desc) append(modal, node("div", "aksrt-vip-points-modal__note", exchange.desc)); append(body, modal); } @@ -201,25 +200,33 @@ function exchangeFromModal(root, button) { var packageId = pointsPackage(root); - if (!packageId || !button || button.disabled) return; + if (!button || button.disabled) return; + if (!packageId) { + notyf("请选择兑换套餐", "danger"); + return; + } var coordinator = window.AksrtVipPurchaseCoordinator; - if (!coordinator || typeof coordinator.exchange !== "function") return; + if (!coordinator || typeof coordinator.exchange !== "function") { + notyf("会员兑换暂不可用", "danger"); + return; + } button.disabled = true; button.classList.add("is-loading"); - setTip(root, "正在兑换…"); + notyf("正在兑换会员…", "load", "", "aksrt-vip-points-toast"); coordinator.exchange({ packageId: packageId, tags: ["vip", "orders"] }).then(function (response) { button.classList.remove("is-loading"); if (response && response.ok) { - setTip(root, "兑换成功,正在刷新…"); - window.setTimeout(function () { window.location.reload(); }, 400); + var data = response.data || response.json || {}; + notyf(data.message || "兑换成功,正在刷新…", "success", 5, "aksrt-vip-points-toast"); + window.setTimeout(function () { window.location.reload(); }, 800); return; } button.disabled = false; - setTip(root, response && response.json && response.json.message ? response.json.message : "兑换失败,请重试。", true); + notyf(response && response.json && response.json.message ? response.json.message : "兑换失败,请重试。", "danger", 5, "aksrt-vip-points-toast"); }).catch(function () { button.disabled = false; button.classList.remove("is-loading"); - setTip(root, "网络异常,请稍后重试。", true); + notyf("网络异常,请稍后重试。", "danger", 5, "aksrt-vip-points-toast"); }); } function renderModalView(body, view) { @@ -326,7 +333,6 @@ 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); @@ -361,14 +367,8 @@ return checked ? parseInt(checked.value, 10) : 0; } - function setTip(root, message, isError) { - var tip = root.querySelector("[data-aksrt-vip-tip]") || root.querySelector(".aksrt-vip-modal-tip"); - if (!tip) return; - tip.textContent = message || ""; - tip.classList.toggle("is-error", !!isError); - } - var activePurchase = null; + var activePurchaseToastId = ""; function restoreButton(button) { if (!button) return; @@ -379,39 +379,51 @@ function purchaseFromPane(pane, button) { var packageId = selectedPackage(pane); if (!packageId) { - setTip(pane, "请选择套餐", true); + notyf("请选择套餐", "danger"); return; } if (!button || button.disabled) return; var coordinator = window.AksrtVipPurchaseCoordinator; + if (!coordinator || typeof coordinator.start !== "function") { + notyf("会员购买暂不可用", "danger"); + return; + } + var waitingPayment = false; button.disabled = true; button.classList.add("is-loading"); - setTip(pane, "正在创建订单…"); + activePurchaseToastId = "aksrt-vip-purchase-toast"; + notyf("正在创建会员订单…", "load", "", activePurchaseToastId); activePurchase = coordinator.start({ packageId: packageId, paymentMethod: selectedMethod(pane), tags: ["vip", "orders"], onPending: function (data) { - restoreButton(button); + waitingPayment = true; if (data.payment_url) window.open(data.payment_url, "_blank"); - setTip(pane, data.message || "请完成支付,支付成功后将自动刷新。"); + notyf(data.message || "请完成支付,支付成功后将自动刷新。", "info", 5, activePurchaseToastId); + activePurchaseToastId = "aksrt-vip-payment-toast"; + notyf("等待支付结果…", "load", "", activePurchaseToastId); }, onSuccess: function (result) { activePurchase = null; restoreButton(button); - setTip(pane, (result.data && result.data.message) || "开通成功,正在刷新…"); - window.setTimeout(function () { window.location.reload(); }, 400); + var toastId = waitingPayment ? "aksrt-vip-payment-toast" : "aksrt-vip-purchase-toast"; + notyf((result.data && result.data.message) || "开通成功,正在刷新…", "success", 5, toastId); + activePurchaseToastId = ""; + window.setTimeout(function () { window.location.reload(); }, 800); }, onFailure: function (result) { activePurchase = null; restoreButton(button); - setTip(pane, result.data && result.data.message ? result.data.message : "支付未完成,请重试。", true); + notyf(result.data && result.data.message ? result.data.message : "支付未完成,请重试。", "danger", 5, "aksrt-vip-payment-toast"); + activePurchaseToastId = ""; }, onTimeout: function () { activePurchase = null; restoreButton(button); - setTip(pane, "支付状态暂未确认,请稍后在订单中心查看。", true); + notyf("支付状态暂未确认,请稍后在订单中心查看。", "danger", 5, "aksrt-vip-payment-toast"); + activePurchaseToastId = ""; }, onError: function (response) { activePurchase = null; @@ -422,7 +434,8 @@ var login = document.querySelector(".signin-loader"); if (login) login.click(); } - setTip(pane, message, true); + notyf(message, "danger", 5, activePurchaseToastId || "aksrt-vip-purchase-toast"); + activePurchaseToastId = ""; }, }); } @@ -460,7 +473,10 @@ } function vipPointsPay() { - if (!window.AksrtModalRuntime.isRolloutEnabled("vip")) return; + if (!window.AksrtModalRuntime.isRolloutEnabled("vip")) { + notyf("会员兑换暂不可用", "danger"); + return; + } window.AksrtModalRuntime.openRemote({ url: "/pay/vip/modal", query: { vip_level: 1, mode: "points" }, @@ -477,16 +493,22 @@ 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(); + var unauthorized = error && error.code === "unauthorized"; + notyf(unauthorized ? "请先登录" : ((error && error.message) || "会员兑换信息加载失败,请重试"), "danger"); + if (unauthorized) { + window.AksrtModalRuntime.close("unauthorized"); + var login = document.querySelector(".signin-loader"); + if (login) login.click(); + } }, }); } function vipPay(trigger) { - if (!window.AksrtModalRuntime.isRolloutEnabled("vip")) return; + if (!window.AksrtModalRuntime.isRolloutEnabled("vip")) { + notyf("会员购买暂不可用", "danger"); + return; + } var level = trigger ? parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1 : 1; @@ -506,10 +528,13 @@ 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(); + var unauthorized = error && error.code === "unauthorized"; + notyf(unauthorized ? "请先登录" : ((error && error.message) || "会员套餐加载失败,请重试"), "danger"); + if (unauthorized) { + window.AksrtModalRuntime.close("unauthorized"); + var login = document.querySelector(".signin-loader"); + if (login) login.click(); + } }, }); } @@ -539,6 +564,10 @@ if (options.kind !== "vip" || !activePurchase) return; if (typeof activePurchase.abort === "function") activePurchase.abort(); activePurchase = null; + if (activePurchaseToastId) { + notyf("已取消会员购买", "warning", 3, activePurchaseToastId); + activePurchaseToastId = ""; + } }); function bindOnce() { diff --git a/src/Core/ScriptsPart1b.php b/src/Core/ScriptsPart1b.php index 160f0440..4ffbe962 100644 --- a/src/Core/ScriptsPart1b.php +++ b/src/Core/ScriptsPart1b.php @@ -256,7 +256,7 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js'; wp_enqueue_script( 'aksrt-pay-vip', $base_uri . '/assets/js/pay-vip.js', - ['aksrt-script', 'aksrt-core-request', 'aksrt-core-modal-runtime', 'aksrt-vip-coordinator'], + ['aksrt-script', 'aksrt-core-request', 'aksrt-core-modal-runtime', 'aksrt-vip-coordinator', 'aksrt-notyf'], (string) filemtime($vip_js), true ); diff --git a/tests/widget-system/toast-contract.test.cjs b/tests/widget-system/toast-contract.test.cjs index c8ea7581..16c559a8 100644 --- a/tests/widget-system/toast-contract.test.cjs +++ b/tests/widget-system/toast-contract.test.cjs @@ -236,6 +236,29 @@ test('balance center actions use direct fixed-id notyf flows', () => { assert.doesNotMatch(css, /\.uc-(?:modal|balance-card)__tip/); }); +test('VIP purchase and points exchange use direct fixed-id notyf flows', () => { + const vip = read('assets/js/pay-vip.js'); + const css = read('assets/css/frontend/vip-modal.css'); + const scripts = read('src/Core/ScriptsPart1b.php'); + + assert.match(vip, /notyf\("正在兑换会员…", "load", "", "aksrt-vip-points-toast"\)/); + assert.match(vip, /notyf\([^;]+?"success"[^;]+?"aksrt-vip-points-toast"\)/); + assert.match(vip, /notyf\([^;]+?"danger"[^;]+?"aksrt-vip-points-toast"\)/); + + assert.match(vip, /activePurchaseToastId = "aksrt-vip-purchase-toast";[\s\S]*?notyf\("正在创建会员订单…", "load"/); + assert.match(vip, /onPending:[\s\S]*?"info"[\s\S]*?activePurchaseToastId = "aksrt-vip-payment-toast";[\s\S]*?notyf\("等待支付结果…", "load"/); + assert.match(vip, /onSuccess:[\s\S]*?notyf\([^;]+?"success"/); + assert.match(vip, /onFailure:[\s\S]*?notyf\([^;]+?"danger"[^;]+?"aksrt-vip-payment-toast"/); + assert.match(vip, /onTimeout:[\s\S]*?notyf\([^;]+?"danger"[^;]+?"aksrt-vip-payment-toast"/); + assert.match(vip, /已取消会员购买", "warning", 3, activePurchaseToastId/); + assert.match(vip, /会员套餐加载失败,请重试[\s\S]*?"danger"/); + assert.match(vip, /会员兑换信息加载失败,请重试[\s\S]*?"danger"/); + + assert.doesNotMatch(vip, /setTip|aksrt-vip-modal-tip/); + assert.doesNotMatch(css, /\.aksrt-vip-modal-tip/); + assert.match(scripts, /'aksrt-pay-vip',[\s\S]*?\/assets\/js\/pay-vip\.js',[\s\S]*?\[[^\]]*'aksrt-notyf'[^\]]*\]/); +}); + test('legacy frontend toast APIs and selectors are fully removed', () => { const files = [ 'assets/js',