902 lines
35 KiB
JavaScript
902 lines
35 KiB
JavaScript
/**
|
||
* 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, '<')
|
||
.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 =
|
||
'<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 captchaScope(sourceEl) {
|
||
const box = sourceEl ? sourceEl.closest('[data-aksrt-download-box]') : null;
|
||
return box ? (box.querySelector('[data-aksrt-download-captcha]') || box) : sourceEl;
|
||
}
|
||
|
||
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 captcha = window.AKSRTCaptcha;
|
||
if (!captcha) {
|
||
if (window.console && console.warn) console.warn('[AKSRT] download captcha not ready');
|
||
alert('验证码组件加载中,请稍候再试');
|
||
return;
|
||
}
|
||
captcha.run('download', captchaScope(sourceEl)).then(function (payload) {
|
||
if (payload !== null) callback(payload);
|
||
});
|
||
}
|
||
|
||
function performDownloadRequest(button, captchaPayload) {
|
||
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 || '';
|
||
|
||
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);
|
||
return;
|
||
}
|
||
renderSuccess(r.json, button);
|
||
})
|
||
.catch(function () {
|
||
button.disabled = false;
|
||
button.innerHTML = original;
|
||
renderError('网络错误,请稍后重试', {}, button);
|
||
})
|
||
.finally(function () {
|
||
if (window.AKSRTCaptcha) window.AKSRTCaptcha.reset('download', captchaScope(button));
|
||
});
|
||
}
|
||
|
||
function requestDownload(button) {
|
||
ensureCaptchaThen(button, function (captchaPayload) {
|
||
performDownloadRequest(button, captchaPayload);
|
||
});
|
||
}
|
||
|
||
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 fas 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 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 || '');
|
||
};
|
||
|
||
})();
|