/** * AKSRT CMS Download * * 功能: * - 监听购买入口(data-aksrt-pay-content / data-action="aksrt-need-purchase") * - 内容购买收银台、扫码支付与错误提示弹窗 * - 下载已改为直连:按钮为源地址锚点,不再经过本站中转请求 * * 弹窗视觉走主题 design token(见 assets/css/frontend/cms-download.css), * 付费收银台沿用参考实现的 colorful header 结构。 * * 依赖: * - window.aksrtData.{ ajaxUrl, nonce, siteUrl } * - 主题已加载 fetch (现代浏览器) */ (function () { 'use strict'; if (!window.aksrtData) return; const REST_BASE = window.aksrtData.siteUrl ? window.aksrtData.siteUrl.replace(/\/$/, '') + '/wp-json/aksrt/v1' : '/wp-json/aksrt/v1'; const PAY_CONTENT_PATH = '/pay/content/'; function escapeHtml(value) { return String(value == null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function escapeAttr(value) { return escapeHtml(value); } // ===== Focus trap state ===== let lastFocusedElement = null; function getFocusableElements(container) { if (!container) return []; return Array.prototype.slice.call(container.querySelectorAll( 'a[href], area[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]),' + ' select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' )).filter(function (el) { return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length); }); } function trapFocus(e, modal) { if (e.key !== 'Tab') return; const focusables = getFocusableElements(modal); if (!focusables.length) { e.preventDefault(); return; } const first = focusables[0]; const last = focusables[focusables.length - 1]; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } } // ===== Modal helper ===== function buildModal() { let modal = document.getElementById('aksrt-dl-modal'); if (modal) return modal; modal = document.createElement('div'); modal.id = 'aksrt-dl-modal'; modal.className = 'aksrt-dl-modal'; modal.setAttribute('role', 'dialog'); modal.setAttribute('aria-modal', 'true'); modal.setAttribute('aria-hidden', 'true'); modal.setAttribute('aria-labelledby', 'aksrt-dl-modal-title'); modal.innerHTML = '
' + ''; document.body.appendChild(modal); modal.addEventListener('click', function (e) { if (e.target.closest('[data-close]')) close(); }); modal.addEventListener('keydown', function (e) { if (e.key === 'Escape' && modal.classList.contains('is-open')) { e.preventDefault(); close(); } else if (e.key === 'Tab' && modal.classList.contains('is-open')) { trapFocus(e, modal); } }); return modal; } function open(html, returnFocusEl, variant) { // 记录调用方按钮以便关闭后回焦 lastFocusedElement = returnFocusEl || document.activeElement; const modal = buildModal(); const body = modal.querySelector('[data-body]'); modal.classList.toggle('is-cashier', variant === 'cashier'); body.innerHTML = html; const title = body.querySelector('.aksrt-dl-modal__title, .aksrt-cashier__title'); if (title) title.id = 'aksrt-dl-modal-title'; modal.classList.add('is-open'); modal.setAttribute('aria-hidden', 'false'); document.body.classList.add('aksrt-dl-modal-open'); // 进入后将焦点放在第一个可聚焦元素,否则放 panel setTimeout(function () { const focusables = getFocusableElements(modal); if (focusables.length) { focusables[0].focus(); } else { modal.querySelector('.aksrt-dl-modal__panel').focus(); } }, 0); } function close() { const modal = document.getElementById('aksrt-dl-modal'); if (!modal) return; modal.classList.remove('is-open'); modal.setAttribute('aria-hidden', 'true'); document.body.classList.remove('aksrt-dl-modal-open'); // 焦点回到触发按钮 if (lastFocusedElement && typeof lastFocusedElement.focus === 'function') { try { lastFocusedElement.focus(); } catch (_) { /* ignore */ } } lastFocusedElement = null; } // ===== 统一视图壳(下载 / 错误 / 扫码共用主题化外观) ===== // 头部与按钮统一继承运行时主题色(--focus-color),状态用图标 + 标题区分。 const DL_VIEWS = { login: { icon: 'fa-user-lock', title: '请先登录' }, purchase: { icon: 'fa-shopping-cart', title: '请先购买' }, vip: { icon: 'fa-gem', title: '会员专属' }, credits: { icon: 'fa-coins', title: '积分不足' }, quota: { icon: 'fa-hourglass-half', title: '今日免费额度已用完' }, captcha: { icon: 'fa-shield-alt', title: '验证失败' }, error: { icon: 'fa-exclamation-circle', title: '下载失败' }, pay: { icon: 'fa-qrcode', title: '扫码支付' }, }; function dlActionButton(action) { if (!action) return ''; const tag = action.kind === 'link' ? 'a' : 'button'; const cls = 'aksrt-dl-modal__btn' + (action.cls ? ' ' + action.cls : ''); const attrs = [ 'class="' + cls + '"', action.url ? 'href="' + escapeAttr(action.url) + '"' : '', action.attrs || '', ].filter(Boolean).join(' '); return '<' + tag + ' ' + attrs + '>' + '' + '' + escapeHtml(action.label) + '' + ''; } function dlView(viewKey, options) { const meta = DL_VIEWS[viewKey] || DL_VIEWS.error; options = options || {}; let html = '
'; html += '
'; html += '
'; html += '

' + (options.title || meta.title) + '

'; if (options.subtitle) { html += '

' + escapeHtml(options.subtitle) + '

'; } html += '
'; html += '
'; html += options.body || ''; if (options.actions && options.actions.length) { html += '
' + options.actions.map(dlActionButton).join('') + '
'; } html += '
'; return html; } // ===== 错误提示渲染 ===== function renderError(msg, data, returnFocusEl, code) { let viewKey = 'error'; let subtitle = ''; let action = null; data = data || {}; msg = msg || ''; if (code === 'aksrt_need_login') { viewKey = 'login'; msg = data.login_url ? '登录后即可继续下载,访客不可使用此资源。' : (msg || '无下载权限。'); if (data.login_url) action = { kind: 'link', url: data.login_url, label: '前往登录', icon: 'fa-sign-in-alt' }; } else if (code === 'aksrt_need_purchase') { viewKey = 'purchase'; msg = msg || '请先完成购买后再下载该资源。'; if (data.post_id) { action = { kind: 'button', label: '去购买', icon: 'fa-shopping-cart', attrs: 'type="button" data-aksrt-open-cashier="' + escapeAttr(data.post_id) + '"' }; } } else if (code === 'aksrt_need_vip') { viewKey = 'vip'; msg = msg || '该资源仅限会员购买/下载。'; subtitle = '开通会员即可解锁本资源'; if (data.vip_url) action = { kind: 'link', url: data.vip_url, label: '开通会员', icon: 'fa-gem' }; } else if (code === 'aksrt_points_insufficient' || code === 'aksrt_balance_insufficient') { viewKey = 'credits'; const balance = typeof data.balance === 'number' ? data.balance : 0; const cost = typeof data.cost === 'number' ? data.cost : 0; msg = '当前余额 ' + balance + ' 积分,需要 ' + cost + ' 积分。可通过签到、阅读、邀请好友等方式获取积分。'; if (data.credits_url) action = { kind: 'link', url: data.credits_url, label: '我的积分', icon: 'fa-coins' }; } else if (code === 'aksrt_download_limit') { viewKey = 'quota'; msg = msg || '您今日下载免费资源个数已超限,请明日再下载。'; if (data.allow_limit_over && data.post_id) { action = { kind: 'button', label: '立即购买', icon: 'fa-shopping-cart', attrs: 'type="button" data-aksrt-open-cashier="' + escapeAttr(data.post_id) + '" data-price-type="download_limit_over"' }; } } else if (code === 'aksrt_captcha_failed') { viewKey = 'captcha'; msg = msg || '人机验证失败,请重试。'; } else if (!action && data.login_url) { action = { kind: 'link', url: data.login_url, label: '前往登录', icon: 'fa-sign-in-alt' }; } open( dlView(viewKey, { subtitle: subtitle, body: msg ? '

' + escapeHtml(msg) + '

' : '', actions: action ? [action] : [], }), returnFocusEl ); const openCashierBtn = document.querySelector('[data-aksrt-open-cashier]'); if (openCashierBtn) { openCashierBtn.addEventListener('click', function () { const pid = parseInt(openCashierBtn.getAttribute('data-aksrt-open-cashier'), 10) || 0; const pt = openCashierBtn.getAttribute('data-price-type') || ''; close(); if (pid) openCashier(pid, returnFocusEl, pt); }); } } function cashierHeader() { return ''; } function openCashierContent(content, returnFocusEl) { open( cashierHeader() + '', returnFocusEl, 'cashier' ); } function pollOrderPaid(orderNumber, returnFocusEl) { if (!orderNumber) return; let tries = 0; const maxTries = 40; const timer = setInterval(function () { tries += 1; fetch(REST_BASE + '/pay/content/order/' + encodeURIComponent(orderNumber), { method: 'GET', credentials: 'same-origin', headers: restHeaders(), }) .then(function (resp) { return resp.json().then(function (j) { return { ok: resp.ok, json: j }; }); }) .then(function (r) { if (r.ok && r.json && r.json.paid) { clearInterval(timer); window.location.reload(); } }) .catch(function () { /* ignore */ }); if (tries >= maxTries) clearInterval(timer); }, 3000); } // ===== 收银台(内容购买) ===== function restHeaders(idempotencyKey) { const headers = { 'Content-Type': 'application/json', 'X-WP-Nonce': window.aksrtData.restNonce || '', }; if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey; return headers; } function buildPreviewUrl(postId, priceType, couponCode) { var previewUrl = REST_BASE + PAY_CONTENT_PATH + postId; var qs = []; if (priceType) qs.push('price_type=' + encodeURIComponent(priceType)); if (couponCode) qs.push('coupon_code=' + encodeURIComponent(couponCode)); if (qs.length) previewUrl += '?' + qs.join('&'); return previewUrl; } function fetchPreview(postId, priceType, couponCode) { return fetch(buildPreviewUrl(postId, priceType, couponCode), { method: 'GET', credentials: 'same-origin', headers: restHeaders(), }).then(function (resp) { return resp.json().then(function (j) { return { ok: resp.ok, status: resp.status, json: j }; }); }); } function openCashier(postId, returnFocusEl, priceType, couponCode) { if (!postId) return; priceType = priceType || ''; couponCode = couponCode || ''; openCashierContent( '

加载中…

', returnFocusEl ); fetchPreview(postId, priceType, couponCode) .then(function (r) { if (!r.ok || (r.json && r.json.code)) { const code = r.json && r.json.code ? r.json.code : ''; const data = (r.json && r.json.data) || {}; if (code === 'aksrt_need_login' || r.status === 401) { renderError(r.json.message || '', data, returnFocusEl, 'aksrt_need_login'); return; } if (code === 'aksrt_need_vip') { renderError(r.json.message || '', data, returnFocusEl, 'aksrt_need_vip'); return; } if (code === 'aksrt_already_paid') { window.location.reload(); return; } renderError((r.json && r.json.message) || '请稍后重试', data, returnFocusEl, code); return; } var prev = r.json || {}; if (priceType) prev._price_type = priceType; if (couponCode) prev._coupon_code = couponCode; renderCashier(prev, returnFocusEl); }) .catch(function () { renderError('网络错误,请稍后重试', {}, returnFocusEl); }); } function formatAssetLabel(preview, value) { const currency = escapeHtml(preview.currency || (preview.pay_modo === 'points' ? '积分' : '¥')); const amount = escapeHtml(value == null ? '' : value); return preview.pay_modo === 'points' ? amount + currency : currency + amount; } function paymentMethodLogo(methodId) { const logoNames = { balance: 'pay-balance-logo.svg', alipay: 'pay-alipay-logo.svg', epay_alipay: 'pay-alipay-logo.svg', wechatpay: 'pay-wechat-logo.svg', epay_wxpay: 'pay-wechat-logo.svg', paypal: 'pay-paypal-logo.svg', stripe: 'pay-stripe-logo.svg', }; const logo = logoNames[methodId]; if (!logo) return ''; const themeUrl = String(window.aksrtData.themeUrl || '').replace(/\/$/, ''); return ''; } function cashierPriceRow(label, value, className) { return '
' + '' + label + '' + '
' + value + '
' + '
'; } function renderCashier(preview, returnFocusEl) { if (!preview) return; if (preview.need_login && !preview.allow_guest) { renderError('', { login_url: preview.login_url }, returnFocusEl, 'aksrt_need_login'); return; } if (preview.need_vip) { renderError('', { vip_url: preview.vip_url }, returnFocusEl, 'aksrt_need_vip'); return; } if (preview.has_paid_access) { window.location.reload(); return; } // Content free: reload. if (preview.is_free) { window.location.reload(); return; } const isPoints = preview.pay_modo === 'points'; const priceType = (preview._price_type || preview.price_type || ''); let amountLabel = escapeHtml(preview.amount_label || ''); let baseLabel = escapeHtml(preview.base_label || ''); const listLabel = escapeHtml(preview.list_amount_label || preview.amount_label || amountLabel); if (priceType === 'download_limit_over' && preview.download_limit_over_label && !preview.discounts) { amountLabel = escapeHtml(preview.download_limit_over_label); baseLabel = amountLabel; preview.amount = preview.download_limit_over_price; preview.base = preview.download_limit_over_price; } const title = escapeHtml(preview.title || ''); const typeLabel = escapeHtml(preview.pay_type_label || ''); const balance = isPoints && Number.isSafeInteger(preview.balance) ? preview.balance : 0; const methods = Array.isArray(preview.payment_methods) ? preview.payment_methods : []; const couponAllowed = !isPoints && !!preview.coupon_allowed; const couponCodeVal = escapeAttr(preview.coupon_code || preview._coupon_code || ''); let priceRows = ''; if (!preview.pay_limit) { let regularPrice = '' + baseLabel + ''; if (!isPoints && Number(preview.original) > Number(preview.base)) { regularPrice = '' + formatAssetLabel(preview, preview.original) + '' + regularPrice; } priceRows += cashierPriceRow('价格', regularPrice, 'aksrt-cashier__regular-price'); } if (preview.list_amount !== preview.base) { priceRows += cashierPriceRow( '会员价VIP' + escapeHtml(preview.vip_level || '') + '', '' + listLabel + '', 'c-red' ); } else if (preview.amount !== preview.base && !preview.rebate_discount && !preview.coupon_discount) { priceRows += cashierPriceRow( '会员价VIP' + escapeHtml(preview.vip_level || '') + '', '' + amountLabel + '', 'c-red' ); } if (preview.rebate_discount > 0) { priceRows += cashierPriceRow( '推广折扣', '' + escapeHtml(preview.rebate_discount_label || ('-' + preview.rebate_discount)) + '' ); } if (preview.coupon_discount > 0) { priceRows += cashierPriceRow( '优惠码', '' + escapeHtml(preview.coupon_discount_label || ('-' + preview.coupon_discount)) + '' ); } if (preview.amount !== preview.base || preview.rebate_discount > 0 || preview.coupon_discount > 0) { priceRows += cashierPriceRow( '支付金额', '' + amountLabel + '', 'font-bold c-red' ); } let body = ''; body += '
' + '' + typeLabel + '' + title + '' + '
'; body += '
' + priceRows + '
'; if (isPoints) { body += '
' + '
' + '我的积分' + '
' + balance + '
'; if (!preview.balance_enough) { body += '
抱歉,您的积分不足,暂时无法支付
'; body += ''; openCashierContent(body, returnFocusEl); return; } body += ''; } else { if (couponAllowed) { body += '
'; body += '
使用优惠码
'; body += '
'; body += ''; body += ''; body += '
'; if (preview.coupon_desc) { body += '
' + escapeHtml(preview.coupon_desc) + '
'; } if (preview.coupon_error) { body += '
' + escapeHtml(preview.coupon_error) + '
'; } else if (preview.coupon_discount > 0) { body += '
优惠码可用
'; } body += '
'; } const walletBal = typeof preview.wallet_balance === 'string' ? preview.wallet_balance : (typeof preview.balance === 'string' ? preview.balance : '0.00'); const hasBalanceMethod = methods.some(function (m) { return (m && m.id) === 'balance'; }); let methodHtml = ''; methods.forEach(function (m, idx) { const rawId = String((m && m.id) || ''); const id = escapeAttr(rawId); const name = escapeHtml((m && m.name) || rawId); methodHtml += ''; }); if (methods.length > 1) { body += '
请选择支付方式
' + '
' + methodHtml + '
'; } if (methods.length) { body += ''; } if (hasBalanceMethod) { const balanceSelected = methods[0] && methods[0].id === 'balance'; body += '
' + '
' + '我的余额' + '
' + escapeHtml(walletBal) + '
'; if (preview.balance_enough === false) { if (preview.credits_url) { body += '' + '抱歉!余额不足,请先充值 '; } else { body += '
抱歉!余额不足,请先充值
'; } } body += '
'; } if (methods.length) { body += ''; } else { body += '
暂无可用支付方式,请联系站长配置收款接口
'; } } openCashierContent(body, returnFocusEl); const modal = document.getElementById('aksrt-dl-modal'); const checkBtn = modal && modal.querySelector('[data-aksrt-check-coupon]'); if (checkBtn) { checkBtn.addEventListener('click', function () { const input = modal.querySelector('input[name="aksrt_coupon_code"]'); const code = input ? String(input.value || '').trim() : ''; if (!code) { if (input) input.focus(); return; } checkBtn.disabled = true; const old = checkBtn.innerHTML; checkBtn.innerHTML = '检查中…'; fetchPreview(preview.post_id, priceType, code) .then(function (r) { checkBtn.disabled = false; checkBtn.innerHTML = old; if (!r.ok || (r.json && r.json.code && !r.json.amount && r.json.amount !== 0)) { renderError((r.json && r.json.message) || '请稍后重试', (r.json && r.json.data) || {}, returnFocusEl, (r.json && r.json.code) || ''); return; } var prev = r.json || {}; if (priceType) prev._price_type = priceType; prev._coupon_code = code; renderCashier(prev, returnFocusEl); }) .catch(function () { checkBtn.disabled = false; checkBtn.innerHTML = old; renderError('网络错误,请稍后重试', {}, returnFocusEl); }); }); } if (modal) { const methodCards = modal.querySelectorAll('[data-aksrt-payment-method]'); const methodInput = modal.querySelector('input[name="aksrt_pay_method"]'); const balanceBox = modal.querySelector('[data-aksrt-balance-box]'); const selectMethod = function (card) { const method = card.getAttribute('data-aksrt-payment-method') || ''; methodCards.forEach(function (item) { const active = item === card; item.classList.toggle('active', active); item.setAttribute('aria-checked', active ? 'true' : 'false'); }); if (methodInput) methodInput.value = method; if (balanceBox) balanceBox.hidden = method !== 'balance'; }; methodCards.forEach(function (card) { card.addEventListener('click', function () { selectMethod(card); }); card.addEventListener('keydown', function (event) { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); selectMethod(card); } }); }); } const submitBtn = modal && modal.querySelector('[data-aksrt-pay-submit]'); if (submitBtn) { submitBtn.addEventListener('click', function () { const mode = submitBtn.getAttribute('data-mode') || (isPoints ? 'points' : 'cash'); let paymentMethod = 'balance'; const selected = modal.querySelector('input[name="aksrt_pay_method"]'); if (selected) paymentMethod = selected.value; const input = modal.querySelector('input[name="aksrt_coupon_code"]'); const code = input ? String(input.value || '').trim() : (preview.coupon_code || preview._coupon_code || ''); submitPurchase(preview.post_id, mode, paymentMethod, submitBtn, returnFocusEl, code); }); } } function submitPurchase(postId, mode, paymentMethod, button, returnFocusEl, couponCode) { if (!postId) return; const original = button.innerHTML; button.disabled = true; button.innerHTML = '处理中…'; const payload = { mode: mode }; if (mode === 'cash') payload.payment_method = paymentMethod || 'balance'; const pt = button.getAttribute('data-price-type') || ''; if (pt) payload.price_type = pt; if (mode === 'cash' && couponCode) payload.coupon_code = couponCode; if (!button.dataset.aksrtOperationKey) { button.dataset.aksrtOperationKey = window.AksrtRequest.createIdempotencyKey(); } fetch(REST_BASE + PAY_CONTENT_PATH + postId, { method: 'POST', credentials: 'same-origin', headers: restHeaders(button.dataset.aksrtOperationKey), body: JSON.stringify(payload), }) .then(function (resp) { return resp.json().then(function (j) { return { ok: resp.ok, status: resp.status, json: j }; }); }) .then(function (r) { button.disabled = false; button.innerHTML = original; if (!r.ok || (r.json && r.json.code && !r.json.success)) { const code = r.json && r.json.code ? r.json.code : ''; const data = (r.json && r.json.data) || {}; if (code === 'aksrt_already_paid') { window.location.reload(); return; } renderError((r.json && r.json.message) || '请稍后重试', data, returnFocusEl, code); return; } const data = (r.json && r.json.data) ? r.json.data : r.json; delete button.dataset.aksrtOperationKey; if (data && data.redirect && (data.payment_url || data.qr_content)) { if (data.payment_url) { window.open(data.payment_url, '_blank'); } renderScanPay(data, returnFocusEl); if (data.order_number) pollOrderPaid(data.order_number, returnFocusEl); return; } window.location.reload(); }) .catch(function () { button.disabled = false; button.innerHTML = original; renderError('网络错误,请稍后重试', {}, returnFocusEl); }); } function renderScanPay(data, returnFocusEl) { data = data || {}; let body = ''; if (data.qr_content) { body += '
凭证内容
' + '' + escapeHtml(String(data.qr_content)) + '
'; } body += '

' + escapeHtml(data.message || '请使用手机扫码完成支付,支付后刷新页面。') + '

'; open( dlView('pay', { subtitle: '支付完成后页面会自动刷新', body: body, actions: [{ kind: 'button', label: '我已支付', icon: 'fa-sync-alt', attrs: 'type="button" data-aksrt-reload' }], }), returnFocusEl ); const reloadBtn = document.querySelector('[data-aksrt-reload]'); if (reloadBtn) reloadBtn.addEventListener('click', function () { window.location.reload(); }); } // ===== 事件绑定 ===== document.addEventListener('click', function (e) { const buyBtn = e.target.closest('[data-action="aksrt-need-purchase"], [data-aksrt-pay-content]'); if (buyBtn) { e.preventDefault(); const postId = parseInt( buyBtn.getAttribute('data-aksrt-pay-content') || buyBtn.getAttribute('data-post-id') || (buyBtn.closest('[data-post-id]') && buyBtn.closest('[data-post-id]').getAttribute('data-post-id')) || '0', 10 ); const priceType = buyBtn.getAttribute('data-price-type') || ''; if (postId) openCashier(postId, buyBtn, priceType); return; } }); // Public cashier bridge (content pay). window.aksrtOpenCashier = function (postId, returnFocusEl, priceType, couponCode) { openCashier(postId, returnFocusEl || null, priceType || '', couponCode || ''); }; })();