feat(pay): 卡密兑换入口收敛到三个购买弹窗并对齐参考主题

- 移除余额卡片独立「卡密兑换」通用弹窗,兑换入口分布到充值/积分/会员三个弹窗的支付方式中
- 充值弹窗补齐卡密 logo 与仅卡密时的金额选择隐藏逻辑
- 积分购买弹窗 1:1 复刻参考主题 points-pay modal(商品档位/自定义/支付方式/立即支付),REST 改为 product+custom 契约
- 会员弹窗注入卡密渠道:视图模型透传 card_pass,前端渲染卡密盒并直兑 /pay/card-pass/redeem type=vip
- 后台卡密兑换拆分为余额/积分/会员三个独立开关,VIP 段旧会员卡密组清理
- REST type 改为必填(balance/vip/credits),三类型独立闸门
- 测试重写为 card-pass-modal-redeem 契约,321 项全部通过
This commit is contained in:
kate522 committed 2026-09-18 12:25:18 +08:00
1 parent 74e43aaa76
commit b88eab0bfe
18 files changed
+746 -472

No files matched your search

+112 -16
View File
@@ -102,7 +102,7 @@
append(parent, wrap);
}
function renderMethods(parent, methods) {
function renderMethods(parent, methods, cardPass) {
methods = Array.isArray(methods) ? methods : [];
if (!methods.length) {
append(parent, node("div", "muted-2-color text-center padding-10", "暂无可用支付方式"));
@@ -115,23 +115,60 @@
one.value = text(methods[0].id || "balance");
one.setAttribute("data-aksrt-vip-method-fixed", "");
append(parent, one);
return;
} else {
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);
}
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);
// 卡密渠道输入盒(契约与参考主题一致:data-controller="payment_method")。
if (cardPass) {
var box = node("div", "uc-vip-shop__card-pass mb10");
box.setAttribute("data-controller", "payment_method");
box.setAttribute("data-condition", "==");
box.setAttribute("data-value", "card_pass");
box.style.display = "none";
if (cardPass.desc) append(box, node("div", "muted-box muted-2-color padding-10 mb10 em09", cardPass.desc));
append(box, node("div", "muted-2-color em09 mb6", cardPass.only_password ? "请输入卡密" : "请输入卡号和密码"));
if (!cardPass.only_password) {
var cardInput = node("input", "form-control mb6");
cardInput.type = "text";
cardInput.name = "card_pass[card]";
cardInput.placeholder = "卡号";
cardInput.autocomplete = "off";
append(box, cardInput);
}
var pwInput = node("input", "form-control");
pwInput.type = "text";
pwInput.name = "card_pass[password]";
pwInput.placeholder = "卡密";
pwInput.autocomplete = "off";
append(box, pwInput);
append(parent, box);
}
}
// 卡密渠道显隐:选中 card_pass 时隐藏套餐(.charge-box)、显示卡密输入盒。
function applyVipMethodVisibility(root) {
var pane = root.closest ? root.closest(".aksrt-vip-pane") : null;
pane = pane || root;
var value = selectedMethod(pane);
var cardBox = pane.querySelector('[data-controller="payment_method"][data-value="card_pass"]');
if (cardBox) cardBox.style.display = value === "card_pass" ? "" : "none";
pane.querySelectorAll(".charge-box").forEach(function (el) {
el.style.display = value === "card_pass" ? "none" : "";
});
append(parent, wrap);
}
function renderPointsModal(body, view) {
@@ -248,6 +285,7 @@
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 cardPass = view.card_pass && view.card_pass.enabled ? view.card_pass : null;
var allowed = view.allowed_actions || {};
var reasons = view.action_reasons || {};
var modal = node("div", "box-body payvip-modal");
@@ -354,6 +392,11 @@
append(pointsWrap, pointsLink);
append(modal, pointsWrap);
}
// 卡密渠道初始显隐(选中 card_pass 时隐藏套餐、显示卡密输入盒)。
modal.querySelectorAll(".aksrt-vip-pane").forEach(function (pane) {
applyVipMethodVisibility(pane);
});
append(body, modal);
}
@@ -444,9 +487,62 @@
});
}
// 卡密兑换会员(type=vip):直兑,不创建支付订单。
function redeemVipCardPass(pane, button) {
var pwEl = pane.querySelector('input[name="card_pass[password]"]');
var cardEl = pane.querySelector('input[name="card_pass[card]"]');
var password = pwEl ? String(pwEl.value || "").trim() : "";
if (!password) {
notyf("请输入卡密", "danger");
if (pwEl) pwEl.focus();
return;
}
if (!button || button.disabled) return;
var request = window.AksrtRequest;
if (!request || typeof request.request !== "function") {
notyf("会员兑换暂不可用", "danger");
return;
}
button.disabled = true;
button.classList.add("is-loading");
notyf("正在兑换会员…", "load", "", "aksrt-vip-purchase-toast");
var key = typeof request.createIdempotencyKey === "function" ? request.createIdempotencyKey() : "";
request.request("/pay/card-pass/redeem", {
method: "POST",
headers: key ? { "Idempotency-Key": key } : {},
body: JSON.stringify({
type: "vip",
card: cardEl ? String(cardEl.value || "").trim() : "",
password: password,
}),
}).then(function (response) {
button.disabled = false;
button.classList.remove("is-loading");
if (!response.ok || (response.json && response.json.code && !response.json.success)) {
notyf((response.json && response.json.message) || "兑换失败", "danger", 5, "aksrt-vip-purchase-toast");
return;
}
var data = response.data || (response.json && response.json.data) || {};
if (window.AksrtModalRuntime && typeof window.AksrtModalRuntime.invalidate === "function") {
window.AksrtModalRuntime.invalidate(["vip", "orders"]);
}
notyf(data.message || "兑换成功,正在刷新…", "success", 5, "aksrt-vip-purchase-toast");
window.setTimeout(function () { window.location.reload(); }, 800);
}).catch(function () {
button.disabled = false;
button.classList.remove("is-loading");
notyf("网络错误,请重试", "danger", 5, "aksrt-vip-purchase-toast");
});
}
function bindModalUi(root) {
if (!root || root.dataset.aksrtVipBound === "1") return;
root.dataset.aksrtVipBound = "1";
root.addEventListener("change", function (event) {
var input = event.target.closest ? event.target.closest('input[name="aksrt_vip_pay_method"]') : null;
if (!input) return;
applyVipMethodVisibility(input);
});
root.addEventListener("click", function (event) {
var tab = event.target.closest("[data-vip-tab]");
if (tab) {