Files
wordpress-theme-aksrt/assets/js/cms-download.js
T
2026-08-27 02:29:40 +08:00

929 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AKSRT CMS Download
*
* 功能:
* - 监听 [data-action="aksrt-download"] 按钮点击
* - 调 POST /aksrt/v1/download/request
* - 弹窗显示下载链接(含提取码)
*
* 弹窗视觉走主题 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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
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 =
'<div class="aksrt-dl-modal__backdrop" data-close></div>' +
'<div class="aksrt-dl-modal__dialog modal-mini">' +
' <div class="aksrt-dl-modal__panel modal-content" role="document" tabindex="-1">' +
' <button type="button" class="aksrt-dl-modal__close" aria-label="关闭" data-close>×</button>' +
' <div class="aksrt-dl-modal__body" data-body></div>' +
' </div>' +
'</div>';
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 = {
success: { icon: 'fa-cloud-download-alt', title: '下载已就绪' },
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 + '>' +
'<i class="fa fas ' + (action.icon || 'fa-arrow-circle-right') + '" aria-hidden="true"></i>' +
'<span>' + escapeHtml(action.label) + '</span>' +
'</' + tag + '>';
}
function dlView(viewKey, options) {
const meta = DL_VIEWS[viewKey] || DL_VIEWS.error;
options = options || {};
let html = '<div class="aksrt-dl-view">';
html += '<div class="aksrt-dl-modal__hero">';
html += '<div class="aksrt-dl-modal__hero-icon"><i class="fa fas ' + meta.icon + '" aria-hidden="true"></i></div>';
html += '<h3 class="aksrt-dl-modal__title">' + (options.title || meta.title) + '</h3>';
if (options.subtitle) {
html += '<p class="aksrt-dl-modal__subtitle">' + escapeHtml(options.subtitle) + '</p>';
}
html += '</div>';
html += '<div class="aksrt-dl-modal__content">';
html += options.body || '';
if (options.actions && options.actions.length) {
html += '<div class="aksrt-dl-modal__actions">' +
options.actions.map(dlActionButton).join('') +
'</div>';
}
html += '</div></div>';
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 ? '<p class="aksrt-dl-modal__msg">' + escapeHtml(msg) + '</p>' : '',
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 renderSuccess(payload, returnFocusEl) {
payload = payload || {};
const code = String(payload.extract_code || '').trim();
const has = !!payload.has_extract || code !== '';
const safeCode = escapeHtml(code);
const safeCodeAttr = escapeAttr(code);
const expiresIn = typeof payload.expires_in === 'number' && payload.expires_in > 0
? Math.max(1, Math.round(payload.expires_in / 60))
: 10;
const hintMsg = '点击下方按钮开始下载,链接 ' + expiresIn + ' 分钟内有效。';
let body = '';
if (code !== '') {
body +=
'<div class="aksrt-dl-modal__code-card">' +
' <div class="aksrt-dl-modal__code-caption">网盘提取码</div>' +
' <div class="aksrt-dl-modal__code-row">' +
' <code class="aksrt-dl-modal__code">' + safeCode + '</code>' +
' <button type="button" class="aksrt-dl-modal__copy" data-copy="' + safeCodeAttr + '"><span class="aksrt-dl-modal__copy-text">复制</span></button>' +
' </div>' +
'</div>' +
'<p class="aksrt-dl-modal__msg">' + escapeHtml(hintMsg) + '</p>';
} else if (has) {
body += '<p class="aksrt-dl-modal__alert aksrt-dl-modal__alert--warn">下载源标记了提取码,但接口未返回具体内容,请检查资源下载源的 extract_code 字段。</p>';
} else {
body += '<p class="aksrt-dl-modal__msg">' + escapeHtml(hintMsg) + '</p>';
}
const chips = [];
const sourceLabel = String(payload.source_label || '').trim();
if (sourceLabel !== '') {
chips.push('<span class="aksrt-dl-modal__chip"><i class="fa fas fa-box" aria-hidden="true"></i>' + escapeHtml(sourceLabel) + '</span>');
}
if (chips.length) {
body += '<div class="aksrt-dl-modal__meta">' + chips.join('') + '</div>';
}
open(
dlView('success', {
subtitle: '下载链接已生成,请及时将文件保存到本地。',
body: body,
actions: [{
kind: 'link',
url: payload.dl_url || '#',
label: '立即下载',
icon: 'fa-download',
cls: 'aksrt-dl-modal__download',
attrs: 'target="_blank" rel="noopener noreferrer"',
}],
}),
returnFocusEl
);
// 复制按钮
document.querySelectorAll('[data-copy]').forEach(function (btn) {
btn.addEventListener('click', function () {
const text = btn.getAttribute('data-copy') || '';
const label = btn.querySelector('.aksrt-dl-modal__copy-text') || btn;
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(function () {
label.textContent = '已复制';
btn.classList.add('is-done');
setTimeout(function () {
label.textContent = '复制';
btn.classList.remove('is-done');
}, 2000);
});
}
});
});
}
// ===== 主流程 =====
function collectCaptchaPayload(sourceEl) {
const box = sourceEl ? sourceEl.closest('[data-aksrt-download-box]') : null;
const container = box ? box.querySelector('[data-aksrt-download-captcha]') : null;
if (!container) return {};
const out = {};
const keys = [
'captcha_verified',
'cf_turnstile_response',
'tencent_ticket', 'tencent_randstr', 'tencent_sid',
'geetest_lot_number', 'geetest_captcha_output', 'geetest_pass_token', 'geetest_gen_time',
'aliyun_captcha_verify_param',
];
keys.forEach(function (k) {
const el = container.querySelector('input[name="' + k + '"]');
if (el && el.value !== '') out[k] = el.value;
});
return out;
}
function isCaptchaActive(sourceEl) {
const card = sourceEl ? sourceEl.closest('[data-aksrt-download-box]') : null;
if (!card) return false;
return card.getAttribute('data-captcha') === '1';
}
function cashierHeader() {
return '<div class="modal-colorful-header colorful-bg jb-blue">' +
'<button type="button" class="close aksrt-cashier__close" aria-label="关闭" data-close>×</button>' +
'<div class="colorful-make"></div>' +
'<div class="text-center">' +
' <div class="em2x"><i class="fa fas fa-cart-plus" aria-hidden="true"></i></div>' +
' <div class="aksrt-cashier__title mt10 em12 padding-w10">确认购买</div>' +
'</div>' +
'</div>';
}
function openCashierContent(content, returnFocusEl) {
open(
cashierHeader() + '<div class="modal-pay-body box-body">' + content + '</div>',
returnFocusEl,
'cashier'
);
}
function ensureCaptchaThen(sourceEl, callback) {
if (!isCaptchaActive(sourceEl)) { callback(); return; }
const state = window.AKSRTCaptchaState;
const scene = state && state.scenes && state.scenes.download;
if (!scene || typeof scene.verify !== 'function') {
// captcha 组件未就绪:不直发,避免绕过服务端门槛
if (window.console && console.warn) {
console.warn('[AKSRT] download captcha not ready');
}
alert('验证码组件加载中,请稍候再试');
return;
}
scene.verify(function () { callback(); });
}
function performDownloadRequest(button) {
const card = button.closest('[data-aksrt-download-box]');
if (!card) return;
const postId = parseInt(card.getAttribute('data-post-id'), 10);
const sourceIndex = parseInt(button.getAttribute('data-source-index'), 10);
if (!postId || isNaN(sourceIndex)) return;
button.disabled = true;
const original = button.innerHTML;
button.innerHTML = '<i class="fa fas fa-spinner fa-spin" aria-hidden="true"></i> 处理中…';
const restNonce = window.aksrtData.restNonce || '';
const captchaPayload = collectCaptchaPayload(button);
fetch(REST_BASE + '/download/request', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': restNonce,
},
body: JSON.stringify({
post_id: postId,
source_index: sourceIndex,
captcha: captchaPayload,
}),
})
.then(function (resp) { return resp.json().then(function (j) { return { ok: resp.ok, json: j }; }); })
.then(function (r) {
button.disabled = false;
button.innerHTML = original;
if (!r.ok || (r.json && r.json.code)) {
renderError(r.json.message || '请稍后重试', r.json.data || {}, button, r.json.code);
// 验证码已被消费,重置以便用户重试
try {
const scene = window.AKSRTCaptchaState && window.AKSRTCaptchaState.scenes && window.AKSRTCaptchaState.scenes.download;
if (scene && typeof scene.reset === 'function') scene.reset();
} catch (_) { /* ignore */ }
return;
}
renderSuccess(r.json, button);
})
.catch(function () {
button.disabled = false;
button.innerHTML = original;
renderError('网络错误,请稍后重试', {}, button);
});
}
function requestDownload(button) {
ensureCaptchaThen(button, function () {
performDownloadRequest(button);
});
}
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 showLowCreditModal(button) {
const balance = parseInt(button.getAttribute('data-balance'), 10) || 0;
const cost = parseInt(button.getAttribute('data-cost'), 10) || 0;
renderError('', { balance: balance, cost: cost }, button, 'aksrt_points_insufficient');
}
// ===== 收银台(内容购买) =====
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(
'<p class="aksrt-cashier__loading muted-2-color">加载中…</p>',
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 '<img src="' + escapeAttr(themeUrl + '/assets/img/' + logo) + '" alt="">';
}
function cashierPriceRow(label, value, className) {
return '<div class="flex jsb ab aksrt-cashier__price-row">' +
'<span class="muted-2-color">' + label + '</span>' +
'<div class="' + (className || '') + '">' + value + '</div>' +
'</div>';
}
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 = '<span class="em14">' + baseLabel + '</span>';
if (!isPoints && Number(preview.original) > Number(preview.base)) {
regularPrice = '<span class="original-price">' + formatAssetLabel(preview, preview.original) + '</span>' + regularPrice;
}
priceRows += cashierPriceRow('价格', regularPrice, 'aksrt-cashier__regular-price');
}
if (preview.list_amount !== preview.base) {
priceRows += cashierPriceRow(
'会员价<span class="aksrt-cashier__vip-mark">VIP' + escapeHtml(preview.vip_level || '') + '</span>',
'<span class="em14">' + listLabel + '</span>',
'c-red'
);
} else if (preview.amount !== preview.base && !preview.rebate_discount && !preview.coupon_discount) {
priceRows += cashierPriceRow(
'会员价<span class="aksrt-cashier__vip-mark">VIP' + escapeHtml(preview.vip_level || '') + '</span>',
'<span class="em14">' + amountLabel + '</span>',
'c-red'
);
}
if (preview.rebate_discount > 0) {
priceRows += cashierPriceRow(
'推广折扣',
'<span class="em14">' + escapeHtml(preview.rebate_discount_label || ('-' + preview.rebate_discount)) + '</span>'
);
}
if (preview.coupon_discount > 0) {
priceRows += cashierPriceRow(
'优惠码',
'<span class="em14">' + escapeHtml(preview.coupon_discount_label || ('-' + preview.coupon_discount)) + '</span>'
);
}
if (preview.amount !== preview.base || preview.rebate_discount > 0 || preview.coupon_discount > 0) {
priceRows += cashierPriceRow(
'支付金额',
'<span class="em14">' + amountLabel + '</span>',
'font-bold c-red'
);
}
let body = '';
body += '<div class="mb10 order-type-' + escapeAttr(preview.pay_type || '') + '">' +
'<span class="pay-tag badg badg-sm mr6">' + typeLabel + '</span><span>' + title + '</span>' +
'</div>';
body += '<div class="mb10 muted-box padding-h6 line-16">' + priceRows + '</div>';
if (isPoints) {
body += '<div class="mb10 muted-box padding-h10">' +
'<div class="flex jsb ac"><span class="muted-2-color">' +
'<i class="fa fas fa-database aksrt-cashier__asset-icon" aria-hidden="true"></i>我的积分' +
'</span><div class="c-green"><span class="em14">' + balance + '</span></div></div></div>';
if (!preview.balance_enough) {
body += '<div class="badg c-red btn-block mb20 aksrt-cashier__warn">抱歉,您的积分不足,暂时无法支付</div>';
body += '<div class="modal-buts but-average">';
if (preview.credits_url) {
body += '<a class="but padding-lg" href="' + escapeAttr(preview.credits_url) + '">我的积分</a>';
}
body += '</div>';
openCashierContent(body, returnFocusEl);
return;
}
body += '<button type="button" class="mt6 but jb-red initiate-pay btn-block radius" data-aksrt-pay-submit data-mode="points" data-price-type="' + escapeAttr(priceType) + '">' +
'立即支付<span class="pay-price-text ml10">' + amountLabel + '</span></button>';
} else {
if (couponAllowed) {
body += '<div class="mb10 coupon-input-box">';
body += '<div class="muted-2-color em09 mb6">使用优惠码</div>';
body += '<div class="relative">';
body += '<input type="text" class="form-control coupon-input" name="aksrt_coupon_code" value="' + couponCodeVal + '" placeholder="请输入优惠码" autocomplete="off">';
body += '<span class="abs-right"><button type="button" class="but c-blue coupon-submit" data-aksrt-check-coupon>检查优惠码</button></span>';
body += '</div>';
if (preview.coupon_desc) {
body += '<div class="muted-3-color px12 mt6 aksrt-cashier__coupon-desc">' + escapeHtml(preview.coupon_desc) + '</div>';
}
if (preview.coupon_error) {
body += '<div class="aksrt-cashier__warn">' + escapeHtml(preview.coupon_error) + '</div>';
} else if (preview.coupon_discount > 0) {
body += '<div class="aksrt-cashier__coupon-ok">优惠码可用</div>';
}
body += '</div>';
}
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 += '<div class="flex jc hh payment-method-radio hollow-radio flex-auto pointer' + (idx === 0 ? ' active' : '') + '"' +
' role="radio" aria-checked="' + (idx === 0 ? 'true' : 'false') + '" tabindex="0" data-aksrt-payment-method="' + id + '">' +
paymentMethodLogo(rawId) + '<div>' + name + '</div></div>';
});
if (methods.length > 1) {
body += '<div class="muted-2-color em09 mb6">请选择支付方式</div>' +
'<div class="flex mb10 aksrt-cashier__methods" role="radiogroup">' + methodHtml + '</div>';
}
if (methods.length) {
body += '<input type="hidden" name="aksrt_pay_method" value="' + escapeAttr(methods[0].id || '') + '">';
}
if (hasBalanceMethod) {
const balanceSelected = methods[0] && methods[0].id === 'balance';
body += '<div class="mb10 muted-box padding-h10 aksrt-cashier__balance" data-aksrt-balance-box' + (balanceSelected ? '' : ' hidden') + '>' +
'<div class="flex jsb ac"><span class="muted-2-color">' +
'<i class="fa fas fa-wallet aksrt-cashier__asset-icon" aria-hidden="true"></i>我的余额' +
'</span><div class="c-blue-2"><span class="em14">¥' + escapeHtml(walletBal) + '</span></div></div>';
if (preview.balance_enough === false) {
if (preview.credits_url) {
body += '<a class="but c-red block mt6 aksrt-cashier__recharge" href="' + escapeAttr(preview.credits_url) + '">' +
'抱歉!余额不足,请先充值 <i class="ml6 fa fa-angle-right em12" aria-hidden="true"></i></a>';
} else {
body += '<div class="c-red mt6 aksrt-cashier__recharge">抱歉!余额不足,请先充值</div>';
}
}
body += '</div>';
}
if (methods.length) {
body += '<button type="button" class="mt6 but jb-red initiate-pay btn-block radius" data-aksrt-pay-submit data-mode="cash" data-price-type="' + escapeAttr(priceType) + '">' +
'立即支付<span class="pay-price-text ml10">' + amountLabel + '</span></button>';
} else {
body += '<div class="badg c-red btn-block aksrt-cashier__warn">暂无可用支付方式,请联系站长配置收款接口</div>';
}
}
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 += '<div class="aksrt-dl-modal__code-card"><div class="aksrt-dl-modal__code-caption">凭证内容</div>' +
'<code class="aksrt-dl-modal__code aksrt-dl-modal__code--wrap">' + escapeHtml(String(data.qr_content)) + '</code></div>';
}
body += '<p class="aksrt-dl-modal__msg">' + escapeHtml(data.message || '请使用手机扫码完成支付,支付后刷新页面。') + '</p>';
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 lowBtn = e.target.closest('[data-action="aksrt-need-credits"]');
if (lowBtn) {
e.preventDefault();
showLowCreditModal(lowBtn);
return;
}
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;
}
const cashierLink = e.target.closest('.cashier-link');
if (cashierLink && cashierLink.hasAttribute('data-aksrt-pay-content')) {
e.preventDefault();
const postId = parseInt(cashierLink.getAttribute('data-aksrt-pay-content') || '0', 10);
if (postId) openCashier(postId, cashierLink);
return;
}
const btn = e.target.closest('[data-action="aksrt-download"]');
if (!btn) return;
if (btn.disabled || btn.getAttribute('aria-disabled') === 'true') return;
e.preventDefault();
requestDownload(btn);
});
// Public cashier bridge (content pay).
window.aksrtOpenCashier = function (postId, returnFocusEl, priceType, couponCode) {
openCashier(postId, returnFocusEl || null, priceType || '', couponCode || '');
};
})();