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:
+183
-103
@@ -266,79 +266,28 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 卡密兑换(余额 / 会员 / 积分,通用弹窗由后端自动识别类型) =====
|
||||
function initCardPass() {
|
||||
var roots = document.querySelectorAll('[data-aksrt-card-pass]');
|
||||
if (!roots || !roots.length) return;
|
||||
|
||||
var modal = document.querySelector('[data-aksrt-card-pass-modal]');
|
||||
if (modal) {
|
||||
modal.querySelectorAll('[data-aksrt-card-pass-close]').forEach(function (btn) {
|
||||
if (btn.dataset.aksrtCardPassCloseBound === '1') return;
|
||||
btn.dataset.aksrtCardPassCloseBound = '1';
|
||||
btn.addEventListener('click', function () { closeModal(modal); });
|
||||
});
|
||||
}
|
||||
document.querySelectorAll('[data-aksrt-card-pass-open]').forEach(function (btn) {
|
||||
if (btn.dataset.aksrtCardPassOpenBound === '1') return;
|
||||
btn.dataset.aksrtCardPassOpenBound = '1';
|
||||
btn.addEventListener('click', function () { openModal(modal); });
|
||||
});
|
||||
|
||||
Array.prototype.forEach.call(roots, function (root) {
|
||||
var busy = false;
|
||||
resetOperationKeyOnChange(root);
|
||||
|
||||
var btn = root.querySelector('[data-aksrt-card-redeem]');
|
||||
if (!btn) return;
|
||||
|
||||
btn.addEventListener('click', function () {
|
||||
if (busy || btn.disabled) return;
|
||||
var type = btn.getAttribute('data-type') || root.getAttribute('data-aksrt-card-pass') || '';
|
||||
var pwInput = root.querySelector('[data-aksrt-card-password]');
|
||||
var cardInput = root.querySelector('[data-aksrt-card-no]');
|
||||
var password = pwInput ? String(pwInput.value || '').trim() : '';
|
||||
var card = cardInput ? String(cardInput.value || '').trim() : '';
|
||||
// 卡号为选填:是否需要卡号由后端按卡密类型判定并返回错误提示。
|
||||
if (!password) {
|
||||
notyf('请输入卡密', 'danger');
|
||||
if (pwInput) pwInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
busy = true;
|
||||
btn.disabled = true;
|
||||
notyf('正在兑换…', 'load', '', 'aksrt-card-pass-toast');
|
||||
|
||||
var body = { password: password, type: type };
|
||||
if (card) body.card = card;
|
||||
|
||||
api('/pay/card-pass/redeem', {
|
||||
method: 'POST',
|
||||
headers: { 'Idempotency-Key': operationKey(root) },
|
||||
body: JSON.stringify(body)
|
||||
}).then(function (r) {
|
||||
busy = false;
|
||||
btn.disabled = false;
|
||||
if (!r.ok || (r.json && r.json.code && !r.json.success)) {
|
||||
notyf((r.json && r.json.message) || '兑换失败', 'danger', 5, 'aksrt-card-pass-toast');
|
||||
return;
|
||||
}
|
||||
var data = (r.json && r.json.data) ? r.json.data : r.json;
|
||||
resetOperationKey(root);
|
||||
// 通用兑换(type 为空)时以后端识别出的卡密类型为准。
|
||||
var redeemedType = (data && data.type) || type;
|
||||
if (redeemedType === 'vip' && window.AksrtModalRuntime && typeof window.AksrtModalRuntime.invalidate === 'function') {
|
||||
window.AksrtModalRuntime.invalidate(['vip', 'orders']);
|
||||
}
|
||||
notyf((data && data.message) || '兑换成功,正在刷新…', 'success', 5, 'aksrt-card-pass-toast');
|
||||
window.setTimeout(function () { window.location.reload(); }, 800);
|
||||
}).catch(function () {
|
||||
busy = false;
|
||||
btn.disabled = false;
|
||||
notyf('网络错误,请重试', 'danger', 5, 'aksrt-card-pass-toast');
|
||||
});
|
||||
});
|
||||
// ===== 卡密兑换(三个购买弹窗共用的卡密渠道提交) =====
|
||||
function redeemCardPass(type, card, password, keyOwner, toastId) {
|
||||
var body = { password: password, type: type };
|
||||
if (card) body.card = card;
|
||||
return api('/pay/card-pass/redeem', {
|
||||
method: 'POST',
|
||||
headers: { 'Idempotency-Key': operationKey(keyOwner) },
|
||||
body: JSON.stringify(body)
|
||||
}).then(function (r) {
|
||||
if (!r.ok || (r.json && r.json.code && !r.json.success)) {
|
||||
return { ok: false, message: (r.json && r.json.message) || '兑换失败' };
|
||||
}
|
||||
var data = (r.json && r.json.data) ? r.json.data : r.json;
|
||||
resetOperationKey(keyOwner);
|
||||
if (type === 'vip' && window.AksrtModalRuntime && typeof window.AksrtModalRuntime.invalidate === 'function') {
|
||||
window.AksrtModalRuntime.invalidate(['vip', 'orders']);
|
||||
}
|
||||
notyf((data && data.message) || '兑换成功,正在刷新…', 'success', 5, toastId);
|
||||
window.setTimeout(function () { window.location.reload(); }, 800);
|
||||
return { ok: true };
|
||||
}).catch(function () {
|
||||
return { ok: false, message: '网络错误,请重试' };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -471,13 +420,24 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
// 支付方式(.payment-method-radio)
|
||||
// 支付方式(.payment-method-radio);卡密渠道选中时隐藏金额选择、
|
||||
// 显示卡密输入盒(data-controller="payment_method" 契约与参考主题一致)。
|
||||
function cardPassBox() {
|
||||
return modal.querySelector('[data-controller="payment_method"][data-value="card_pass"]');
|
||||
}
|
||||
function applyMethodVisibility(value) {
|
||||
var cardBox = cardPassBox();
|
||||
if (cardBox) cardBox.style.display = value === 'card_pass' ? '' : 'none';
|
||||
var chargeBoxes = modal.querySelectorAll('.charge-box');
|
||||
chargeBoxes.forEach(function (el) { el.style.display = value === 'card_pass' ? 'none' : ''; });
|
||||
}
|
||||
function selectMethod(value) {
|
||||
modal.querySelectorAll('[data-for="payment_method"]').forEach(function (el) {
|
||||
el.classList.toggle('active', el.getAttribute('data-value') === value);
|
||||
});
|
||||
var hidden = modal.querySelector('input[name="payment_method"]');
|
||||
if (hidden) hidden.value = value;
|
||||
applyMethodVisibility(value);
|
||||
if (form) resetOperationKey(form);
|
||||
}
|
||||
modal.querySelectorAll('[data-for="payment_method"]').forEach(function (el) {
|
||||
@@ -485,11 +445,36 @@
|
||||
selectMethod(el.getAttribute('data-value'));
|
||||
});
|
||||
});
|
||||
applyMethodVisibility((modal.querySelector('input[name="payment_method"]') || {}).value || '');
|
||||
if (!form) return;
|
||||
resetOperationKeyOnChange(form);
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
if (busy) return;
|
||||
var methodEl = form.querySelector('input[name="payment_method"]');
|
||||
var method = methodEl ? methodEl.value : '';
|
||||
if (!method) {
|
||||
notyf('暂无可用支付方式', 'danger');
|
||||
return;
|
||||
}
|
||||
if (method === 'card_pass') {
|
||||
var cardEl = form.querySelector('input[name="card_pass[card]"]');
|
||||
var pwEl = form.querySelector('input[name="card_pass[password]"]');
|
||||
var password = pwEl ? String(pwEl.value || '').trim() : '';
|
||||
if (!password) {
|
||||
notyf('请输入卡密', 'danger');
|
||||
if (pwEl) pwEl.focus();
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
notyf('正在兑换…', 'load', '', 'aksrt-balance-charge-toast');
|
||||
redeemCardPass('balance', cardEl ? String(cardEl.value || '').trim() : '', password, form, 'aksrt-balance-charge-toast')
|
||||
.then(function (result) {
|
||||
busy = false;
|
||||
if (!result.ok) notyf(result.message, 'danger', 5, 'aksrt-balance-charge-toast');
|
||||
});
|
||||
return;
|
||||
}
|
||||
var pkgInput = form.querySelector('input[name="balance_product"]');
|
||||
var pkg = pkgInput ? pkgInput.value : 'custom';
|
||||
var customEl = form.querySelector('input[name="custom_price"]');
|
||||
@@ -686,40 +671,136 @@
|
||||
});
|
||||
}
|
||||
|
||||
function initPointsExchange() {
|
||||
var modal = document.querySelector('[data-aksrt-points-exchange-modal]');
|
||||
if (!modal || modal.dataset.aksrtPointsExchangeInitialized === '1') return;
|
||||
modal.dataset.aksrtPointsExchangeInitialized = '1';
|
||||
var form = modal.querySelector('[data-aksrt-points-exchange-form]');
|
||||
var amountEl = form ? form.querySelector('[data-aksrt-points-exchange-amount]') : null;
|
||||
var preview = form ? form.querySelector('[data-aksrt-points-exchange-preview]') : null;
|
||||
// ===== 购买积分(Zibll points-pay modal 1:1:商品/自定义 + 余额/卡密支付) =====
|
||||
function initPointsPay() {
|
||||
var modal = document.querySelector('[data-aksrt-points-pay-modal]');
|
||||
if (!modal || modal.dataset.aksrtPointsPayInitialized === '1') return;
|
||||
modal.dataset.aksrtPointsPayInitialized = '1';
|
||||
var form = modal.querySelector('[data-aksrt-points-pay-form]');
|
||||
var busy = false;
|
||||
document.querySelectorAll('[data-aksrt-points-exchange-open]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () { openModal(modal); });
|
||||
var backdrop = null;
|
||||
|
||||
function openPointsModal() {
|
||||
if (modal.classList.contains('in')) return;
|
||||
modal.style.display = 'flex';
|
||||
void modal.offsetWidth;
|
||||
modal.classList.add('in');
|
||||
if (!backdrop) {
|
||||
backdrop = document.createElement('div');
|
||||
backdrop.className = 'modal-backdrop fade in';
|
||||
backdrop.setAttribute('data-aksrt-points-backdrop', '1');
|
||||
document.body.appendChild(backdrop);
|
||||
}
|
||||
document.body.classList.add('modal-open');
|
||||
}
|
||||
function closePointsModal() {
|
||||
if (!modal.classList.contains('in')) return;
|
||||
modal.classList.remove('in');
|
||||
document.body.classList.remove('modal-open');
|
||||
if (backdrop && backdrop.parentNode) backdrop.parentNode.removeChild(backdrop);
|
||||
backdrop = null;
|
||||
window.setTimeout(function () {
|
||||
if (!modal.classList.contains('in')) modal.style.display = '';
|
||||
}, 300);
|
||||
}
|
||||
document.querySelectorAll('[data-aksrt-points-pay-open]').forEach(function (btn) {
|
||||
btn.addEventListener('click', openPointsModal);
|
||||
});
|
||||
modal.querySelectorAll('[data-aksrt-points-exchange-close]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () { closeModal(modal); });
|
||||
modal.addEventListener('click', function (e) {
|
||||
if (e.target === modal || (e.target.closest && e.target.closest('[data-dismiss="modal"]'))) {
|
||||
closePointsModal();
|
||||
}
|
||||
});
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && modal.classList.contains('in')) closePointsModal();
|
||||
});
|
||||
|
||||
// Zibll data-for/data-value 选中模型:积分商品档位。
|
||||
function selectProduct(value) {
|
||||
modal.querySelectorAll('[data-for="points_product"]').forEach(function (el) {
|
||||
el.classList.toggle('active', el.getAttribute('data-value') === value);
|
||||
});
|
||||
var hidden = modal.querySelector('input[name="points_product"]');
|
||||
if (hidden) hidden.value = value;
|
||||
if (form) resetOperationKey(form);
|
||||
}
|
||||
modal.querySelectorAll('[data-for="points_product"]').forEach(function (el) {
|
||||
el.addEventListener('click', function () {
|
||||
selectProduct(el.getAttribute('data-value'));
|
||||
});
|
||||
});
|
||||
var customInput = modal.querySelector('input[name="points_custom"]');
|
||||
if (customInput) {
|
||||
customInput.addEventListener('input', function () {
|
||||
if (String(customInput.value || '').trim() !== '') selectProduct('custom');
|
||||
});
|
||||
}
|
||||
// 支付方式(余额/卡密);选中卡密时隐藏商品选择、显示卡密输入盒。
|
||||
function cardPassBox() {
|
||||
return modal.querySelector('[data-controller="payment_method"][data-value="card_pass"]');
|
||||
}
|
||||
function applyMethodVisibility(value) {
|
||||
var cardBox = cardPassBox();
|
||||
if (cardBox) cardBox.style.display = value === 'card_pass' ? '' : 'none';
|
||||
modal.querySelectorAll('.charge-box').forEach(function (el) {
|
||||
el.style.display = value === 'card_pass' ? 'none' : '';
|
||||
});
|
||||
}
|
||||
function selectMethod(value) {
|
||||
modal.querySelectorAll('[data-for="payment_method"]').forEach(function (el) {
|
||||
el.classList.toggle('active', el.getAttribute('data-value') === value);
|
||||
});
|
||||
var hidden = modal.querySelector('input[name="payment_method"]');
|
||||
if (hidden) hidden.value = value;
|
||||
applyMethodVisibility(value);
|
||||
if (form) resetOperationKey(form);
|
||||
}
|
||||
modal.querySelectorAll('[data-for="payment_method"]').forEach(function (el) {
|
||||
el.addEventListener('click', function () {
|
||||
selectMethod(el.getAttribute('data-value'));
|
||||
});
|
||||
});
|
||||
applyMethodVisibility((modal.querySelector('input[name="payment_method"]') || {}).value || '');
|
||||
if (!form) return;
|
||||
resetOperationKeyOnChange(form);
|
||||
function updatePreview() {
|
||||
if (!preview || !amountEl) return;
|
||||
var amount = balanceMinor(amountEl.value);
|
||||
var rateRaw = amountEl.getAttribute('data-aksrt-points-exchange-rate') || '';
|
||||
var rate = /^[1-9]\d*$/.test(rateRaw) ? BigInt(rateRaw) : 0n;
|
||||
var scaled = amount !== null ? amount * rate : 0n;
|
||||
preview.textContent = amount !== null && amount > 0n && rate > 0n && scaled % 100n === 0n
|
||||
? '预计获得 ' + String(scaled / 100n) + ' 积分'
|
||||
: '';
|
||||
}
|
||||
amountEl.addEventListener('input', updatePreview);
|
||||
updatePreview();
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
if (busy) return;
|
||||
var amount = amountEl ? String(amountEl.value || '').trim() : '';
|
||||
if (!/^(0|[1-9]\d*)(?:\.\d{1,2})?$/.test(amount) || /^0(?:\.0{1,2})?$/.test(amount)) {
|
||||
notyf('请输入最多两位小数的正数余额', 'danger');
|
||||
var methodEl = form.querySelector('input[name="payment_method"]');
|
||||
var method = methodEl ? methodEl.value : '';
|
||||
if (!method) {
|
||||
notyf('暂无可用支付方式', 'danger');
|
||||
return;
|
||||
}
|
||||
if (method === 'card_pass') {
|
||||
var cardEl = form.querySelector('input[name="card_pass[card]"]');
|
||||
var pwEl = form.querySelector('input[name="card_pass[password]"]');
|
||||
var password = pwEl ? String(pwEl.value || '').trim() : '';
|
||||
if (!password) {
|
||||
notyf('请输入卡密', 'danger');
|
||||
if (pwEl) pwEl.focus();
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
notyf('正在兑换…', 'load', '', 'aksrt-balance-points-toast');
|
||||
redeemCardPass('credits', cardEl ? String(cardEl.value || '').trim() : '', password, form, 'aksrt-balance-points-toast')
|
||||
.then(function (result) {
|
||||
busy = false;
|
||||
if (!result.ok) notyf(result.message, 'danger', 5, 'aksrt-balance-points-toast');
|
||||
});
|
||||
return;
|
||||
}
|
||||
var pkgInput = form.querySelector('input[name="points_product"]');
|
||||
var pkg = pkgInput ? pkgInput.value : 'custom';
|
||||
var customEl = form.querySelector('input[name="points_custom"]');
|
||||
var custom = customEl ? String(customEl.value || '').trim() : '';
|
||||
var validCustom = /^[1-9]\d{0,8}$/.test(custom);
|
||||
if (validCustom) {
|
||||
pkg = 'custom';
|
||||
selectProduct('custom');
|
||||
}
|
||||
if ((!pkg || pkg === 'custom') && !validCustom) {
|
||||
notyf('请选择或输入需购买的积分', 'danger');
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
@@ -727,7 +808,7 @@
|
||||
api('/pay/balance/points-exchange', {
|
||||
method: 'POST',
|
||||
headers: { 'Idempotency-Key': operationKey(form) },
|
||||
body: JSON.stringify({ amount: amount })
|
||||
body: JSON.stringify({ product: pkg, custom: validCustom ? custom : '0' })
|
||||
}).then(function (r) {
|
||||
busy = false;
|
||||
if (!r.ok || (r.json && r.json.code && !r.json.success)) {
|
||||
@@ -1054,10 +1135,9 @@
|
||||
initAffiliateRevenue();
|
||||
initVipShop();
|
||||
initVipPointsExchange();
|
||||
initCardPass();
|
||||
initBalanceCharge();
|
||||
initTransfer();
|
||||
initPointsExchange();
|
||||
initPointsPay();
|
||||
initWithdraw();
|
||||
initWithdrawHistory();
|
||||
initAssetTabs();
|
||||
|
||||
Reference in New Issue
Block a user