补充主题静态资源与归档模板
This commit is contained in:
1 parent
1268a55dd0
commit
3247db7ed1
265 files changed
+59174
No files matched your search
@@ -0,0 +1,344 @@
|
||||
/**
|
||||
* 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, '<')
|
||||
.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__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);
|
||||
})();
|
||||
Reference in new issue
Block a user