/** * Jio Credits Frontend * * 功能: * - 签到 panel:跳转至全局签到按钮 * - revenue panel:拉取 /revenue/me,复制推广链接 * * 仅在 page-profile 页面 + 启用积分时加载。 */ (function () { 'use strict'; var jio = window.jioData; if (!jio) { return; } var activePoll = null; // 自定义货币名称(后台「积分配置 → 自定义货币名称」)由服务端注入。 // 转账标题/金额标签与购买提示都在客户端拼装,必须跟随它。 function currencyName() { return (jio && jio.currencyName) || '积分'; } function operationKey(owner) { if (!owner.dataset.jioOperationKey) { owner.dataset.jioOperationKey = window.JioRequest.createIdempotencyKey(); } return owner.dataset.jioOperationKey; } function resetOperationKey(owner) { delete owner.dataset.jioOperationKey; } function resetOperationKeyOnChange(owner) { owner.addEventListener('input', function () { resetOperationKey(owner); }); owner.addEventListener('change', function () { resetOperationKey(owner); }); } function balanceMinor(value) { var match = /^(0|[1-9]\d*)(?:\.(\d{1,2}))?$/.exec(String(value || '').trim()); if (!match) return null; return (BigInt(match[1]) * 100n) + BigInt((match[2] || '').padEnd(2, '0') || '0'); } function formatCutCount(value) { var number = Number(value); if (!Number.isFinite(number)) return String(value); if (number > 999999) return String(Math.round(number / 10000)) + 'W+'; if (number > 9999) return String(Math.round(number / 1000) / 10) + 'W+'; return String(number); } function api(path, options) { return window.JioRequest.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 }), }; }); } function applyAssetBalances(data) { data = data || {}; var wallet = data.wallet_balance !== undefined ? data.wallet_balance : data.wallet; var points = data.credit_balance !== undefined ? data.credit_balance : data.points_balance; if (wallet !== undefined && wallet !== null && wallet !== '') { var walletNumber = Number(wallet); var walletText = Number.isFinite(walletNumber) ? walletNumber.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : String(wallet); document.querySelectorAll('[data-jio-wallet-value]').forEach(function (element) { var text = element.dataset.jioCountFormat === 'cut-count' ? formatCutCount(wallet) : walletText; element.textContent = text; element.title = text; }); } if (points !== undefined && points !== null && points !== '') { var pointsNumber = Number(points); var pointsText = Number.isFinite(pointsNumber) ? pointsNumber.toLocaleString() : String(points); document.querySelectorAll('[data-jio-points-value]').forEach(function (element) { var text = element.dataset.jioCountFormat === 'cut-count' ? formatCutCount(points) : pointsText; element.textContent = text; element.title = text; }); } } function refreshAssetBalances(snapshot) { if (snapshot && (snapshot.wallet_balance !== undefined || snapshot.wallet !== undefined)) { applyAssetBalances(snapshot); } return api('/pay/assets').then(function (response) { if (!response.ok) return null; var data = response.data || (response.json && response.json.data) || response.json || {}; applyAssetBalances(data); return data; }); } // Task-list "去签到" → scroll + click the real asset-card sign-in button. function initSigninGoto() { document.querySelectorAll('[data-jio-signin-goto]').forEach(function (el) { if (el.dataset.jioSigninGotoInitialized === '1') return; el.dataset.jioSigninGotoInitialized = '1'; el.addEventListener('click', function (e) { e.preventDefault(); var root = document.querySelector('[data-jio-signin]'); var btn = root ? root.querySelector('[data-signin-btn]') : null; if (!btn) return; try { if (root && typeof root.scrollIntoView === 'function') { root.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } catch (_) {} if (!btn.disabled) { btn.click(); } else { btn.focus(); } }); }); } // ===== VIP 开通 ===== function initVipShop() { var root = document.querySelector('[data-jio-vip-shop]'); if (!root) return; var tip = root.querySelector('[data-jio-vip-tip]'); var busy = false; var coordinator = window.JioVipPurchaseCoordinator; function setTip(text, isError) { if (!tip) return; tip.textContent = text || ''; tip.classList.toggle('is-error', !!isError); } function selectedMethod() { var fixed = root.querySelector('[data-jio-vip-method-fixed]'); if (fixed && fixed.value) return fixed.value; var checked = root.querySelector('input[name="jio_vip_pay_method"]:checked'); if (checked) return checked.value; var first = root.querySelector('input[name="jio_vip_pay_method"]'); return first ? first.value : 'balance'; } root.addEventListener('click', function (e) { var btn = e.target.closest('[data-jio-vip-buy]'); if (!btn || busy) return; e.preventDefault(); var packageId = parseInt(btn.getAttribute('data-package-id') || '0', 10); if (!packageId) return; busy = true; btn.disabled = true; btn.classList.add('is-loading'); setTip('正在创建订单…'); coordinator.start({ packageId: packageId, paymentMethod: selectedMethod(), tags: ['vip', 'orders'], onPending: function (data) { busy = false; btn.disabled = false; btn.classList.remove('is-loading'); if (window.JioPayCashier) window.JioPayCashier.handle(data, {}); setTip(data.message || '请完成支付,支付成功后将自动刷新。'); }, 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); } }); }); } // ===== 卡密兑换(三个购买弹窗共用的卡密渠道提交) ===== 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.JioModalRuntime && typeof window.JioModalRuntime.invalidate === 'function') { window.JioModalRuntime.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: '网络错误,请重试' }; }); } // ===== 余额充值 / 转账 / 余额兑积分 ===== function openModal(el) { if (!el) return; el.hidden = false; el.classList.add('is-open'); document.body.classList.add('uc-modal-open'); } function closeModal(el) { if (!el) return; if (activePoll && typeof activePoll.abort === 'function') { activePoll.abort(); notyf('已停止查询支付状态', 'warning', 3, 'jio-balance-charge-payment-toast'); } 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, tags) { var coordinator = window.JioVipPurchaseCoordinator; if (!orderNumber) return; notyf('等待支付结果…', 'load', '', 'jio-balance-charge-payment-toast'); var handle = coordinator.poll(orderNumber, { tags: tags || ['credits', 'orders'], onTerminal: function (result) { activePoll = null; if (window.JioPayCashier) window.JioPayCashier.close(); if (result.success) { notyf('支付成功,正在刷新…', 'success', 5, 'jio-balance-charge-payment-toast'); refreshAssetBalances(); window.setTimeout(function () { window.location.reload(); }, 800); return; } var message = result.data && result.data.message ? result.data.message : '支付未完成,请重试'; notyf(message, 'danger', 5, 'jio-balance-charge-payment-toast'); }, onTimeout: function () { activePoll = null; notyf('支付状态暂未确认,请稍后在订单中心查看', 'danger', 5, 'jio-balance-charge-payment-toast'); } }); return handle; } function initBalanceCharge() { var modal = document.querySelector('[data-jio-charge-modal]'); if (!modal || modal.dataset.jioChargeInitialized === '1') return; modal.dataset.jioChargeInitialized = '1'; var form = modal.querySelector('[data-jio-charge-form]'); var busy = false; var chargeBackdrop = null; // 开关自包含:不复依赖 jQuery/Bootstrap modal 运行时,行为与视觉 // 复刻 Bootstrap 3(.in 显隐 + .modal-backdrop + body.modal-open + ESC)。 function openChargeModal() { if (modal.classList.contains('in')) return; // 兼容旧缓存标记(uc-modal 带 hidden 属性) modal.removeAttribute('hidden'); // 打开态用 flex 而非 block:壳的 .flex.jc 负责双轴居中, // .modal.flex .modal-dialog{margin:0} 在 block 下会把对话框顶到左上角。 modal.style.display = 'flex'; void modal.offsetWidth; // 强制 reflow,让 .fade 过渡从初始态开始 modal.classList.add('in'); if (!chargeBackdrop) { chargeBackdrop = document.createElement('div'); chargeBackdrop.className = 'modal-backdrop fade in'; chargeBackdrop.setAttribute('data-jio-charge-backdrop', '1'); document.body.appendChild(chargeBackdrop); } document.body.classList.add('modal-open'); } function closeChargeModal() { if (!modal.classList.contains('in')) return; if (activePoll && typeof activePoll.abort === 'function') { activePoll.abort(); notyf('已停止查询支付状态', 'warning', 3, 'jio-balance-charge-payment-toast'); } activePoll = null; modal.classList.remove('in'); document.body.classList.remove('modal-open'); if (chargeBackdrop && chargeBackdrop.parentNode) { chargeBackdrop.parentNode.removeChild(chargeBackdrop); } chargeBackdrop = null; // 等过渡结束再撤 display,复刻 Bootstrap 的 fade-out 收尾 window.setTimeout(function () { if (!modal.classList.contains('in')) { modal.style.display = ''; } }, 300); } document.querySelectorAll('[data-jio-charge-open]').forEach(function (btn) { btn.addEventListener('click', openChargeModal); }); modal.addEventListener('click', function (e) { // 点遮罩层(.modal 自身)或关闭按钮(data-dismiss="modal") if (e.target === modal || (e.target.closest && e.target.closest('[data-dismiss="modal"]'))) { closeChargeModal(); } }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && modal.classList.contains('in')) { closeChargeModal(); } }); // data-for/data-value 选中模型:余额档位(.vip-product.product-box) // 选中变化不产生 change 事件(隐藏 input 由程序赋值),需显式重置幂等键。 var customInput = modal.querySelector('input[name="custom_price"]'); function selectProduct(value) { modal.querySelectorAll('[data-for="balance_product"]').forEach(function (el) { el.classList.toggle('active', el.getAttribute('data-value') === value); }); var hidden = modal.querySelector('input[name="balance_product"]'); if (hidden) hidden.value = value; // 选中预设档位即代表放弃自定义金额:清空输入框,否则框里残留的数字会在 // 提交时按「合法自定义金额」把 package 顶回 custom,静默覆盖刚点的档位。 if (value !== 'custom' && customInput) { customInput.value = ''; } if (form) resetOperationKey(form); } modal.querySelectorAll('[data-for="balance_product"]').forEach(function (el) { el.addEventListener('click', function () { selectProduct(el.getAttribute('data-value')); }); }); if (customInput) { customInput.addEventListener('input', function () { if (String(customInput.value || '').trim() !== '') { selectProduct('custom'); } }); } // 支付方式(.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) { el.addEventListener('click', function () { 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', '', 'jio-balance-charge-toast'); redeemCardPass('balance', cardEl ? String(cardEl.value || '').trim() : '', password, form, 'jio-balance-charge-toast') .then(function (result) { busy = false; if (!result.ok) notyf(result.message, 'danger', 5, 'jio-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"]'); var custom = customEl ? String(customEl.value || '').trim() : ''; var validCustom = /^(0|[1-9]\d*)(?:\.\d{1,2})?$/.test(custom) && custom !== '0' && custom !== '0.0' && custom !== '0.00'; if (validCustom) { pkg = 'custom'; selectProduct('custom'); } if ((!pkg || pkg === 'custom') && !validCustom) { notyf('请选择或输入充值金额', 'danger'); return; } busy = true; notyf('正在创建充值订单…', 'load', '', 'jio-balance-charge-toast'); api('/pay/balance/charge', { method: 'POST', headers: { 'Idempotency-Key': operationKey(form) }, body: JSON.stringify({ package: pkg, // 预设档位下该字段为空串:服务端仅在 package=custom 时消费它, // 空串表示「未使用自定义金额」,不是非法值。 custom_price: custom, payment_method: method }) }).then(function (r) { busy = false; if (!r.ok || (r.json && r.json.code && !r.json.success)) { notyf((r.json && r.json.message) || '充值失败', 'danger', 5, 'jio-balance-charge-toast'); return; } var data = (r.json && r.json.data) ? r.json.data : r.json; resetOperationKey(form); if (data && data.mode) { if (window.JioModalRuntime) window.JioModalRuntime.invalidate(['orders']); if (window.JioPayCashier) { window.JioPayCashier.handle(data, { onClose: function () { if (activePoll && typeof activePoll.abort === 'function') activePoll.abort(); activePoll = null; notyf('已停止查询支付状态', 'warning', 3, 'jio-balance-charge-payment-toast'); } }); } notyf(data.message || '请完成支付,支付成功后将自动刷新。', 'info', 5, 'jio-balance-charge-toast'); if (data.order_number) activePoll = pollPaidOrder(data.order_number, ['credits', 'orders']); return; } notyf((data && data.message) || '充值成功,正在刷新…', 'success', 5, 'jio-balance-charge-toast'); refreshAssetBalances(data); window.setTimeout(function () { window.location.reload(); }, 800); }).catch(function () { busy = false; notyf('网络错误,请重试', 'danger', 5, 'jio-balance-charge-toast'); }); }); } function initTransfer() { var modal = document.querySelector('[data-jio-transfer-modal]'); if (!modal || modal.dataset.jioTransferInitialized === '1') return; modal.dataset.jioTransferInitialized = '1'; var form = modal.querySelector('[data-jio-transfer-form]'); var results = modal.querySelector('[data-jio-transfer-results]'); var selected = modal.querySelector('[data-jio-transfer-selected]'); var recipientInput = modal.querySelector('[data-jio-transfer-recipient]'); var typeInput = modal.querySelector('[data-jio-transfer-type]'); var title = modal.querySelector('[data-jio-transfer-title]'); var amountLabel = modal.querySelector('[data-jio-transfer-amount-label]'); var balanceInfo = modal.querySelector('[data-jio-transfer-balance-info]'); var pointsInfo = modal.querySelector('[data-jio-transfer-points-info]'); var pointsTransferMin = 1; var searchTimer = null; var busy = false; document.querySelectorAll('[data-jio-transfer-open]').forEach(function (btn) { btn.addEventListener('click', function () { var type = btn.getAttribute('data-jio-transfer-open') || 'balance'; pointsTransferMin = type === 'points' ? Math.max(1, parseInt(btn.getAttribute('data-jio-transfer-min') || '1', 10) || 1) : 1; if (typeInput) typeInput.value = type; if (title) title.textContent = type === 'points' ? currencyName() + '转账' : '余额转账'; var amountEl = form ? form.querySelector('[data-jio-transfer-amount]') : null; if (amountEl) { amountEl.min = type === 'points' ? String(pointsTransferMin) : '0.01'; amountEl.step = type === 'points' ? '1' : '0.01'; amountEl.inputMode = type === 'points' ? 'numeric' : 'decimal'; amountEl.value = ''; } if (amountLabel) amountLabel.textContent = type === 'points' ? currencyName() : '金额'; if (balanceInfo) balanceInfo.hidden = type === 'points'; if (pointsInfo) pointsInfo.hidden = type !== 'points'; if (results) results.innerHTML = ''; if (selected) selected.textContent = ''; if (recipientInput) recipientInput.value = ''; openModal(modal); }); }); modal.querySelectorAll('[data-jio-transfer-close]').forEach(function (btn) { btn.addEventListener('click', function () { closeModal(modal); }); }); var searchEl = modal.querySelector('[data-jio-transfer-search]'); if (searchEl) { searchEl.addEventListener('input', function () { var q = String(searchEl.value || '').trim(); if (searchTimer) clearTimeout(searchTimer); if (q.length < 1) { if (results) results.innerHTML = ''; return; } searchTimer = setTimeout(function () { api('/pay/transfer/search?s=' + encodeURIComponent(q)).then(function (r) { if (!results) return; results.innerHTML = ''; if (!r.ok) { notyf((r.json && r.json.message) || '用户搜索失败', 'danger'); return; } var data = (r.json && r.json.data) ? r.json.data : r.json; var users = (data && data.users) || []; if (!users.length) { results.innerHTML = '