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:
1 parent
74e43aaa76
commit
b88eab0bfe
18 files changed
+746
-472
No files matched your search
@@ -25,6 +25,12 @@ export const rechargeSchema = {
|
||||
pay_balance_pass_charge_s: false,
|
||||
pay_balance_pass_charge_desc: '',
|
||||
pay_balance_pass_charge_only_password: false,
|
||||
points_pass_exchange_s: false,
|
||||
points_pass_exchange_desc: '',
|
||||
points_pass_exchange_only_password: false,
|
||||
pay_vip_pass_charge_s: false,
|
||||
pay_vip_pass_charge_desc: '',
|
||||
pay_vip_pass_charge_only_password: false,
|
||||
pay_balance_points_exchange_s: false,
|
||||
pay_balance_points_exchange_rate: 100,
|
||||
pay_balance_points_exchange_min_points: 100,
|
||||
@@ -98,9 +104,15 @@ export const rechargeSchema = {
|
||||
title: '卡密兑换',
|
||||
icon: 'fa fas fa-ticket-alt',
|
||||
fields: [
|
||||
{ key: 'pay_balance_pass_charge_s', label: '启用卡密兑换功能', type: 'switch', default: false, help: '开启后用户中心余额卡片展示通用「卡密兑换」入口,支持余额 / 会员 / 积分卡密,类型按卡密自动识别' },
|
||||
{ key: 'pay_balance_pass_charge_desc', label: '卡密兑换说明', type: 'textarea', default: '', rows: 2, dependsOn: { key: 'pay_balance_pass_charge_s', value: true }, help: '用户使用卡密兑换时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' },
|
||||
{ key: 'pay_balance_pass_charge_only_password', label: '单密码模式', type: 'switch', default: false, dependsOn: { key: 'pay_balance_pass_charge_s', value: true }, help: '余额卡密仅需输入密码,不需要卡号' },
|
||||
{ key: 'pay_balance_pass_charge_s', label: '启用卡密兑换余额功能', type: 'switch', default: false, help: '开启后用户可在余额充值弹窗选择「卡密」渠道,使用卡密直接兑换余额' },
|
||||
{ key: 'pay_balance_pass_charge_desc', label: '卡密兑换余额说明', type: 'textarea', default: '', rows: 2, dependsOn: { key: 'pay_balance_pass_charge_s', value: true }, help: '用户使用卡密兑换余额时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' },
|
||||
{ key: 'pay_balance_pass_charge_only_password', label: '余额卡密单密码模式', type: 'switch', default: false, dependsOn: { key: 'pay_balance_pass_charge_s', value: true }, help: '余额卡密仅需输入密码,不需要卡号' },
|
||||
{ key: 'points_pass_exchange_s', label: '启用卡密兑换积分功能', type: 'switch', default: false, help: '开启后用户可在购买积分弹窗选择「卡密」渠道,使用卡密直接兑换积分' },
|
||||
{ key: 'points_pass_exchange_desc', label: '卡密兑换积分说明', type: 'textarea', default: '', rows: 2, dependsOn: { key: 'points_pass_exchange_s', value: true }, help: '用户使用卡密兑换积分时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' },
|
||||
{ key: 'points_pass_exchange_only_password', label: '积分卡密单密码模式', type: 'switch', default: false, dependsOn: { key: 'points_pass_exchange_s', value: true }, help: '积分卡密仅需输入密码,不需要卡号' },
|
||||
{ key: 'pay_vip_pass_charge_s', label: '启用卡密兑换会员功能', type: 'switch', default: false, help: '开启后用户可在会员购买/续费弹窗选择「卡密」渠道,使用会员卡密直接兑换会员' },
|
||||
{ key: 'pay_vip_pass_charge_desc', label: '卡密兑换会员说明', type: 'textarea', default: '', rows: 2, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '用户使用卡密兑换会员时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' },
|
||||
{ key: 'pay_vip_pass_charge_only_password', label: '会员卡密单密码模式', type: 'switch', default: true, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '会员卡密仅需输入密码,不需要卡号' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -40,7 +40,6 @@ export const vipSchema = {
|
||||
vip_upgrade: true,
|
||||
vip_upgrade_desc: '',
|
||||
vip_upgrade_product: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级',
|
||||
pay_vip_pass_charge_only_password: true,
|
||||
pay_vip_points_exchange_s: false,
|
||||
payment_vip_points_packages: '1|500|30|积分月卡\n1|5000|0|积分永久',
|
||||
pay_vip_points_exchange_desc: '',
|
||||
@@ -201,14 +200,6 @@ export const vipSchema = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'pass-charge',
|
||||
title: '会员卡密',
|
||||
icon: 'fa fas fa-ticket-alt',
|
||||
fields: [
|
||||
{ key: 'pay_vip_pass_charge_only_password', label: '单密码模式', type: 'switch', default: true, help: '会员卡密核销是否仅需卡密;关闭后,带卡号的会员卡必须同时输入卡号。会员/积分卡密无独立开关,统一受「余额充值 → 卡密兑换 → 启用卡密兑换功能」控制' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'points-exchange',
|
||||
title: '积分兑换会员',
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" version="1.1">
|
||||
<path d="M 855.34776,170.36629 H 178.33999 c -64.95102,0 -117.564884,52.26845 -117.564884,116.79333 v 55.01654 H 972.91264 v -55.01654 c 0,-64.36964 -52.66906,-116.79333 -117.56488,-116.79333 z" fill="#646bee" />
|
||||
<path d="m 60.719782,754.99249 c 0,64.52488 52.669088,116.79333 117.564888,116.79333 h 677.00776 c 64.95226,0 117.56489,-52.26845 117.56489,-116.79333 V 405.87661 H 60.719782 Z M 736.7317,653.69832 H 889.98099 L 834.15842,763.29168 H 680.90913 Z m -110.428,0 h 51.94985 l -55.76726,109.59336 h -51.94984 z m -116.62436,0 h 51.94984 l -55.76724,109.59336 h -51.94986 z" fill="#585fe4" />
|
||||
<path d="m 60.775106,405.87661 v 349.11588 c 0,64.52488 52.665714,116.79333 117.564884,116.79333 H 560.79931 C 628.68799,846.31039 691.75943,809.63009 747.35404,763.29168 H 680.96446 L 736.7317,653.69832 H 851.30906 C 906.37282,580.10783 944.79505,495.57497 963.95005,405.87661 Z M 505.80661,763.29168 h -51.94985 l 55.76725,-109.59336 h 51.94985 z m 116.62437,0 h -51.94986 l 55.76725,-109.59336 h 51.94985 z" fill="#646bee" />
|
||||
<path d="m 60.775106,678.3211 c 31.369079,4.67174 63.457374,7.09004 96.154254,7.09004 221.2986,0 416.53929,-110.74756 532.61042,-279.53453 H 60.775106 Z M 789.2348,170.36629 H 178.33999 c -64.951,0 -117.564884,52.26845 -117.564884,116.79333 v 55.01654 H 728.10106 c 28.10492,-53.31272 48.96232,-111.02238 61.13374,-171.80987 z" fill="#6b72f2" />
|
||||
<path d="M 251.47919,405.87661 H 60.775106 v 49.52037 C 127.93923,448.63671 192.00518,431.54366 251.47919,405.87661 Z M 533.30296,170.36629 H 178.33999 c -64.95113,0 -117.564884,52.26845 -117.564884,116.79333 v 55.01654 H 365.66927 C 431.84559,295.79524 488.70183,237.52257 533.30296,170.36629 Z" fill="#8c91f2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
+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();
|
||||
|
||||
+112
-16
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user