Files
wordpress-theme-aksrt/assets/js/cms-download.js
T

345 lines
15 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
* - 弹窗显示下载链接(含提取码)
*
* 依赖:
* - 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';
function tt(key, zh) { return zh; }
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__panel" role="document" tabindex="-1">' +
' <button type="button" class="aksrt-dl-modal__close" aria-label="' + tt('close', '关闭') + '" data-close>×</button>' +
' <div class="aksrt-dl-modal__body" data-body></div>' +
'</div>';
document.body.appendChild(modal);
modal.addEventListener('click', function (e) {
if (e.target.matches('[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) {
// 记录调用方按钮以便关闭后回焦
lastFocusedElement = returnFocusEl || document.activeElement;
const modal = buildModal();
const body = modal.querySelector('[data-body]');
// 给标题加 id,与 aria-labelledby 联动
body.innerHTML = html.replace(/aksrt-dl-modal__title/, 'aksrt-dl-modal__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;
}
// ===== 错误提示渲染 =====
function renderError(msg, data, returnFocusEl, code) {
let title = tt('failed', '下载失败');
let extra = '';
if (code === 'aksrt_need_login' && data && data.login_url) {
title = tt('need_login', '请先登录');
msg = tt('login_hint', '登录后即可继续下载,访客不可使用此资源。');
extra = '<a class="btn-primary btn-sm" href="' + data.login_url + '">' + tt('go_login', '前往登录') + '</a>';
} else if (code === 'aksrt_credits_insufficient') {
title = tt('credit_low', '积分不够');
const balance = typeof (data && data.balance) === 'number' ? data.balance : 0;
const cost = typeof (data && data.cost) === 'number' ? data.cost : 0;
msg = tt('credit_hint',
'当前余额 ' + balance + ' 积分,需要 ' + cost + ' 积分。可通过签到、阅读、邀请好友等方式获取积分。'
);
} else if (data && data.login_url) {
extra = '<a class="btn-primary btn-sm" href="' + data.login_url + '">' + tt('go_login', '前往登录') + '</a>';
}
open(
'<h3 class="aksrt-dl-modal__title">' + title + '</h3>' +
'<p class="aksrt-dl-modal__msg">' + (msg || '') + '</p>' +
(extra ? '<div class="aksrt-dl-modal__actions">' + extra + '</div>' : ''),
returnFocusEl
);
}
// ===== 成功渲染 =====
function renderSuccess(payload, returnFocusEl) {
const code = String(payload.extract_code || '').trim();
const has = !!payload.has_extract || code !== '';
const safeCode = escapeHtml(code);
const safeCodeAttr = escapeAttr(code);
const dlUrl = escapeAttr(payload.dl_url || '#');
const extractHtml = code !== ''
? '<div class="aksrt-dl-modal__row"><span class="aksrt-dl-modal__label">' + tt('extract', '提取码') + '</span><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">' + tt('copy', '复制') + '</span></button></div>'
: (has ? '<p class="aksrt-dl-modal__msg aksrt-dl-modal__msg--warn">' + tt('extract_missing', '下载源标记了提取码,但接口未返回具体内容,请检查资源下载源的 extract_code 字段。') + '</p>' : '');
open(
'<h3 class="aksrt-dl-modal__title">' + tt('ready', '下载已就绪') + '</h3>' +
(extractHtml || '<p class="aksrt-dl-modal__msg">' + tt('hint', '点击下方按钮开始下载,链接 10 分钟内有效。') + '</p>') +
'<div class="aksrt-dl-modal__actions">' +
' <a class="btn-primary aksrt-dl-modal__download" href="' + dlUrl + '" target="_blank" rel="noopener noreferrer">' + tt('go', '立即下载') + '</a>' +
(typeof payload.balance === 'number' ? ' <span class="aksrt-dl-modal__balance">' + tt('balance', '余额:') + payload.balance + '</span>' : '') +
'</div>',
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 = tt('copied', '已复制');
setTimeout(function () { label.textContent = tt('copy', '复制'); }, 2000);
});
}
});
});
}
// ===== 主流程 =====
function collectCaptchaPayload(sourceEl) {
// 优先从触发按钮所属的下载卡片中查找验证码容器(支持双下载框场景:侧栏+移动端)
var container = null;
if (sourceEl) {
var box = sourceEl.closest('.resource-download-box');
if (box) container = box.querySelector('[data-aksrt-download-captcha]');
}
// 回退到全局查找
if (!container) container = document.querySelector('[data-aksrt-download-captcha]');
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() {
const card = document.querySelector('.resource-download-box');
if (!card) return false;
return card.getAttribute('data-captcha') === '1';
}
function ensureCaptchaThen(callback) {
if (!isCaptchaActive()) { callback(); return; }
const state = window.AKSRTCaptchaState;
const scene = state && state.scenes && state.scenes.download;
if (!scene || typeof scene.verify !== 'function') {
// captcha 组件还未就绪,直接发,由后端兜底报错
callback();
return;
}
scene.verify(function () { callback(); });
}
function performDownloadRequest(button) {
const card = button.closest('.resource-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="fas fa-spinner fa-spin" aria-hidden="true"></i> ' + tt('processing', '处理中…');
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 || tt('default_err', '请稍后重试'), 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(tt('network_err', '网络错误,请稍后重试'), {}, button);
});
}
function requestDownload(button) {
ensureCaptchaThen(function () {
performDownloadRequest(button);
});
}
// ===== 积分不足按钮(前置拦截,不发请求) =====
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_credits_insufficient');
}
// ===== 事件绑定 =====
document.addEventListener('click', function (e) {
const lowBtn = e.target.closest('[data-action="aksrt-need-credits"]');
if (lowBtn) {
e.preventDefault();
showLowCreditModal(lowBtn);
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);
});
// ===== 注入 modal 基础样式(避免必须额外加载 CSS)=====
const style = document.createElement('style');
style.textContent =
'.aksrt-dl-modal{position:fixed;inset:0;z-index:10000;display:none;}' +
'.aksrt-dl-modal.is-open{display:flex;align-items:center;justify-content:center;}' +
'.aksrt-dl-modal__backdrop{position:absolute;inset:0;background:rgba(0,0,0,.55);}' +
'.aksrt-dl-modal__panel{position:relative;background:#fff;border-radius:14px;padding:26px 28px 24px;width:min(420px,calc(100vw - 32px));box-shadow:0 24px 56px rgba(15,23,42,.24);color:#111827;}' +
'.aksrt-dl-modal__close{position:absolute;top:10px;right:12px;background:transparent;border:0;font-size:24px;line-height:1;cursor:pointer;color:#6b7280;}' +
'.aksrt-dl-modal__title{font:700 20px/1.3 inherit;margin:0 28px 18px 0;color:#111827;}' +
'.aksrt-dl-modal__msg{margin:0 0 18px;color:#4b5563;font-size:14px;line-height:1.65;}' +
'.aksrt-dl-modal__msg--warn{color:#92400e;background:transparent;border:0;border-top:1px solid #fde68a;border-bottom:1px solid #fde68a;padding:12px 0;}' +
'.aksrt-dl-modal__row{display:grid;grid-template-columns:64px minmax(0,1fr) 72px;align-items:center;gap:12px;margin:0 0 18px;padding:12px 0;border-top:1px solid #e5e7eb;border-bottom:1px solid #e5e7eb;}' +
'.aksrt-dl-modal__label{font-size:13px;font-weight:600;color:#6b7280;white-space:nowrap;}' +
'.aksrt-dl-modal__code{display:block;min-width:0;color:#111827;font:800 18px/1.25 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;letter-spacing:.06em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}' +
'.aksrt-dl-modal__copy{appearance:none!important;display:inline-flex!important;align-items:center!important;justify-content:center!important;width:72px!important;height:32px!important;background:#eff6ff!important;border:1px solid #bfdbfe!important;border-radius:8px!important;padding:0!important;cursor:pointer!important;color:#2563eb!important;-webkit-text-fill-color:#2563eb!important;font-size:0!important;font-weight:700!important;line-height:1!important;font-family:inherit!important;text-indent:0!important;opacity:1!important;visibility:visible!important;white-space:nowrap!important;overflow:visible!important;}' +
'.aksrt-dl-modal__copy::before,.aksrt-dl-modal__copy::after{content:none!important;display:none!important;}' +
'.aksrt-dl-modal__copy:hover{background:#dbeafe!important;border-color:#93c5fd!important;color:#1d4ed8!important;-webkit-text-fill-color:#1d4ed8!important;}' +
'.aksrt-dl-modal__copy-text{display:inline-flex!important;align-items:center!important;justify-content:center!important;position:relative!important;z-index:2!important;color:#2563eb!important;-webkit-text-fill-color:#2563eb!important;font-size:13px!important;font-weight:700!important;line-height:1!important;font-family:inherit!important;text-indent:0!important;opacity:1!important;visibility:visible!important;white-space:nowrap!important;}' +
'.aksrt-dl-modal__copy:hover .aksrt-dl-modal__copy-text{color:#1d4ed8!important;-webkit-text-fill-color:#1d4ed8!important;}' +
'.aksrt-dl-modal__actions{display:flex;align-items:center;gap:12px;padding-top:4px;}' +
'.aksrt-dl-modal__download{min-height:40px;padding:0 18px;border-radius:9px;display:inline-flex;align-items:center;justify-content:center;text-decoration:none;}' +
'.aksrt-dl-modal__balance{font-size:13px;color:#6b7280;margin-left:auto;white-space:nowrap;}' +
'@media(max-width:480px){.aksrt-dl-modal__panel{padding:24px 20px 22px;}.aksrt-dl-modal__row{grid-template-columns:1fr 72px;gap:8px 12px;}.aksrt-dl-modal__label{grid-column:1 / -1;}.aksrt-dl-modal__code{font-size:17px;}.aksrt-dl-modal__actions{align-items:flex-start;flex-direction:column;}.aksrt-dl-modal__download{width:100%;}.aksrt-dl-modal__balance{margin-left:0;}}' +
'body.aksrt-dl-modal-open{overflow:hidden;}';
document.head.appendChild(style);
})();