Files
kate522andClaude Fable 5 38bbd917a9 fix(skeleton): align the homepage tab and sort loaders with the reference 1:1
首页「更多栏目」tab 与「排序方式」切换的骨架占位与参考主题 zibll 有实质差异,
根因不是几何宽度,而是三处结构/契约错位:

1. 骨架块自身结构与参考主题不同。card 分支用了 `<posts>` 元素 + `.post-graphic`
   包裹层(列表变体,带 margin-right:20px),而参考主题 card 骨架是
   `<div class="posts-item card">` 且 `.item-thumbnail` 是直接子元素。
   → card 骨架右侧多出一条真实卡片没有的 20px 沟槽。

2. 骨架块被错误地打上 `ajax-item`。参考主题的占位块没有该类。它会让
   `find('.ajax-item')` / ajax-list 的 `inject()` 把骨架当成真实条目。

3. 排序切换的骨架被预渲染节点顶替。aksrt_render_pagination() 在 .ajax-pag 内
   渲染了一个写死 2 条、写死 list 形状的 .post_ajax_loader;runtime post_ajax()
   会先在容器内 find('.post_ajax_loader'),找到就复用并丢弃调用方按视口动态
   拼出的骨架 → 切排序永远是 2 条 list 骨架,card 布局下也是、与视口宽度无关。
   参考主题 zib_paging() 根本没有 loader 兄弟节点。

改动:
- inc/helpers/posts-renderer/markup.php:aksrt_render_placeholder() 的
  card/list 分支改为与 zib_get_post_placeholder() 逐字等价的结构;去掉
  ajax-item 类、card 分支的 .post-graphic 包裹层,元素改用 <div>。
- inc/helpers/template-tags/pagination-permissions.php:删除预渲染 loader,
  与参考主题 zib_paging() 一致 —— 只渲染翻页控件。
- assets/js/core/pagination.js:找不到骨架节点时构造与 runtime/main.js 同款
  的文章骨架(卡片 8/6/4 条、列表 4/3 条,按视口与参考主题同阈值),
  不再退化为「转圈 + 文案」这个第二套加载语言;仅评论等非文章面保留该兜底。
- assets/js/core/pagination.js:为排序菜单(.ajax-option)与 tab 触发器
  (.post_ajax_trigger)增加避让 —— 它们归 legacy runtime 管线,
  否则一次点击发两个请求(与 ajax-list.js 的既有避让对称)。
- assets/js/runtime/main.js:post_ajax 的显隐由 fadeIn()/hide() 改为
  .is-loading 类切换。前者写 inline display,优先级高于类规则,一旦残留
  骨架永远打不开,正是骨架契约明令禁止的失败模式。

归档/分类筛选面原本另有一套独立加载语言(spinner 遮罩 + opacity 淡出),
本次一并统一到同一条 .post_ajax_loader + .is-loading 契约:
- inc/helpers/aksrt-archive.php:删除 show_loading 标记位与遮罩 DOM,
  改为在分页之后渲染文章骨架。
- assets/js/core/archive.js:showLoading/hideLoading 只切 .is-loading;
  fetchPosts() 清空内容时保留骨架与分页节点(与参考主题 success 只 hide
  不删的生命周期一致),原先会连带删掉骨架。
- assets/css/frontend/layout.css:删除 .archive__ajax-wrap.is-loading
  的 opacity 淡出与 spinner 遮罩规则。
- category.php / tag.php:删除已失效的 show_loading 实参。

验证:php -l 全部通过;php tools/audit-admin-contracts.php errors=0;
契约测试 post-visual(22) / asset-ledger(16) / message-center(8) /
lazyload(7) / frontend-lifecycle(13) / zibll-modern-widgets(10) /
page-display(18) / a11y 全绿;已重跑 build:public 刷新产物。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-04 03:04:02 +08:00

357 lines
15 KiB
JavaScript
Raw Permalink 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 AJAX Pagination & Lazy Load
*/
(function () {
"use strict";
function hasAjaxConfig() {
return !!(window.aksrtData && aksrtData.ajaxUrl && aksrtData.nonce);
}
function initAjaxPagination(root) {
var containers = (root || document).querySelectorAll("[data-aksrt-pagination], .aksrt-comment-pagination");
if (!containers.length) return;
var triggerText = (window.aksrtData && aksrtData.ajaxTrigger) || "加载更多";
var nomoreText = (window.aksrtData && aksrtData.ajaxNomore) || "没有更多了";
var errorText = (window.aksrtData && aksrtData.ajaxLoadError) || "加载失败,请重试";
containers.forEach(function (container) {
if (container.dataset.aksrtPaginationBound === "1") return;
container.dataset.aksrtPaginationBound = "1";
var mode = container.dataset.mode || "load_more";
var nextPage = parseInt(container.dataset.nextPage, 10) || 2;
var maxPages = parseInt(container.dataset.maxPages, 10) || 1;
var isComment = container.classList.contains("aksrt-comment-pagination");
var postId = container.dataset.postId || "";
var commentOrder = container.dataset.commentOrder || "newest";
var loading = false;
var failed = false;
var clickTarget = container.querySelector(".next-page.ajax-next, .ias-btn") || container;
var loadMoreBtn = container.querySelector("[data-aksrt-load-more]") || clickTarget;
var successfulAutoLoads = 0;
var iasMax = isComment
? ((window.aksrtData && typeof aksrtData.commentIasMax !== "undefined") ? parseInt(aksrtData.commentIasMax, 10) : 0)
: ((window.aksrtData && typeof aksrtData.iasMax !== "undefined") ? parseInt(aksrtData.iasMax, 10) : 0);
// legacy 契约:分页容器与列表同处一个 .ajaxpager 内(首页 tab pane、归档流),
// 此时追加目标就是列表容器本身,新卡片插到分页控件之前。
var listContainer = container.closest(".ajaxpager, .posts-row");
var target = container.previousElementSibling;
if (listContainer && container.parentNode === listContainer) {
target = listContainer;
}
if (isComment) {
var commentList = container.closest("#comment-list, .commentlist, ol.commentlist") ||
document.getElementById("comment-list");
if (commentList) target = commentList;
}
if (!target && container.parentElement) {
target = container.parentElement.querySelector(".post-grid, .comment-list, .commentlist, .content-stream__list");
}
// 文章骨架:与 runtime/main.js 的 .ajax-next 处理器、aksrt_render_placeholder()
// 同源同款(1:1 对齐参考主题 zib_get_post_placeholder)。刻意不写 ajax-item:
// 否则会被 inject() / find('.ajax-item') 当成真实条目删除或定位。
var CARD_ITEM =
'<div class="posts-item card"><div class="item-thumbnail">' +
'<div class="radius8 item-thumbnail placeholder"></div> </div>' +
'<div class="item-body "> <h2 class="item-excerpt placeholder t1 item-heading"></h2>' +
' <p class="mt10 placeholder k2"></p><i class="flex jsb"><i class="placeholder s1"></i>' +
'<i class="placeholder s1 ml10"></i></i></div></div>';
var LIST_ITEM =
'<div class="posts-item list flex"><div class="post-graphic">' +
'<div class="radius8 item-thumbnail placeholder"></div> </div>' +
'<div class="item-body flex xx flex1 jsb"> <p class="placeholder t1"></p>' +
' <h4 class="item-excerpt placeholder k1"></h4><p class="placeholder k2"></p>' +
'<i><i class="placeholder s1"></i><i class="placeholder s1 ml10"></i></i></div></div>';
// 视口阈值与参考主题 .ajax-next 处理器一致:宽 + 无侧栏 → 8,宽 + 有侧栏 → 6,
// 窄屏 → 4;list 模式宽屏 4、窄屏 3。
function skeletonCount(style) {
var wide = window.innerWidth > 766;
if (style === "card") {
if (!wide) return 4;
return window.innerWidth > 992 && !document.querySelector(".sidebar") ? 8 : 6;
}
return wide ? 4 : 3;
}
// 从列表首张真实卡片判断形状;无卡片时退化为 list。
function postSkeleton() {
var first = target && target.querySelector
? target.querySelector(".posts-item.card, .posts-item.list")
: null;
var isCard = !!first && /(^|\s)card(\s|$)/.test(first.className || "");
var tpl = isCard ? CARD_ITEM : LIST_ITEM;
var out = "";
for (var i = 0, n = skeletonCount(isCard ? "card" : "list"); i < n; i++) {
out += tpl;
}
return '<div class="mb20">' + out + '</div>';
}
// 兜底(非文章列表面,例如评论区没有 .posts-item 时):转圈 + 文案,
// 绝不用按钮自身文字来表达加载态。
function fallbackLoader() {
var text = (window.aksrtData && aksrtData.ajaxLoading) || "加载中...";
return (
'<div class="mb20"><div class="text-center muted-2-color mt20">' +
'<i class="loading mr10"></i>' + text +
'</div></div>'
);
}
function showSkeleton() {
if (!target) return;
var loader = target.querySelector(".post_ajax_loader");
if (!loader) {
loader = document.createElement("div");
loader.className = "post_ajax_loader";
loader.innerHTML = postSkeleton() || fallbackLoader();
}
// 参考主题 beforeSend:骨架每次翻页都挪到列表末尾(分页控件之前),
// 紧贴最后一张卡片、位于「加载更多」按钮上方。
target.insertBefore(
loader,
container.parentNode === target ? container : null
);
loader.classList.add("is-loading");
}
function hideSkeleton() {
if (!target) return;
var loader = target.querySelector(".post_ajax_loader");
if (loader) {
loader.classList.remove("is-loading");
}
}
function stopInfinite(markNomore) {
container.classList.remove("ias-pending");
if (markNomore && loadMoreBtn) {
loadMoreBtn.innerHTML = nomoreText;
loadMoreBtn.classList.add("ajax-nomore");
var nextWrap = loadMoreBtn.closest(".next-page");
if (nextWrap) nextWrap.classList.add("no-more");
loadMoreBtn.style.pointerEvents = "none";
} else if (loadMoreBtn && !loadMoreBtn.classList.contains("ajax-nomore")) {
if (failed) {
loadMoreBtn.textContent = errorText;
} else {
loadMoreBtn.innerHTML = triggerText;
}
loadMoreBtn.style.pointerEvents = "";
}
}
function markFailed(message) {
failed = true;
stopInfinite(false);
if (loadMoreBtn) {
loadMoreBtn.textContent = message || errorText;
loadMoreBtn.classList.remove("ajax-nomore");
loadMoreBtn.style.pointerEvents = "";
}
}
function loadNextPage(fromAuto) {
if (loading || failed || nextPage > maxPages || !target) return Promise.resolve(false);
if (!hasAjaxConfig()) {
markFailed(errorText);
return Promise.resolve(false);
}
loading = true;
// 加载态的呈现完全交给列表末尾的骨架(参考主题 post_ajax beforeSend
// 只操作 .post_ajax_loader,不触碰「加载更多」按钮自身的文字)。
showSkeleton();
var action = isComment ? "aksrt_load_more_comments" : "aksrt_load_more_posts";
var queryContext = container.dataset.queryContext || "";
var queryVars = container.dataset.queryVars || "";
var renderConfig = container.dataset.renderConfig || "";
var body = new URLSearchParams();
body.set("action", action);
body.set("nonce", aksrtData.nonce);
body.set("page", String(nextPage));
if (queryContext) {
body.set("query_context", queryContext);
}
if (!isComment && queryVars) {
body.set("query_vars", queryVars);
}
if (!isComment && renderConfig) {
body.set("render_config", renderConfig);
}
if (isComment && postId) {
body.set("post_id", postId);
body.set("comment_order", commentOrder);
var authorOnly = container.dataset.authorOnly || "0";
if (authorOnly === "1") body.set("author_only", "1");
}
return fetch(aksrtData.ajaxUrl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
credentials: "same-origin",
body: body.toString(),
})
.then(function (r) {
if (!r.ok) {
throw new Error("HTTP " + r.status);
}
return r.text().then(function (text) {
try {
return JSON.parse(text);
} catch (err) {
throw new Error("Invalid JSON");
}
});
})
.then(function (res) {
if (!(res && res.success && res.data && typeof res.data.html === "string")) {
var msg = res && res.data && res.data.message ? res.data.message : errorText;
markFailed(msg);
return false;
}
var temp = document.createElement("div");
temp.innerHTML = res.data.html;
var newItems = temp.children;
while (newItems.length > 0) {
if (container.parentNode === target) {
target.insertBefore(newItems[0], container);
} else {
target.appendChild(newItems[0]);
}
}
// legacy 事件顺序:先 post_ajax.ed,再全局 auto_fun 重扫
target.dispatchEvent(new CustomEvent("post_ajax.ed", { bubbles: true }));
if (typeof window.auto_fun === "function") window.auto_fun();
nextPage++;
container.dataset.nextPage = String(nextPage);
if (fromAuto) {
successfulAutoLoads++;
}
if (nextPage > maxPages || !res.data.has_more) {
stopInfinite(true);
if (!loadMoreBtn) {
container.innerHTML = '<span class="ajax-nomore">' + nomoreText + '</span>';
}
}
return true;
})
.catch(function () {
markFailed(errorText);
return false;
})
.finally(function () {
loading = false;
hideSkeleton();
if (loadMoreBtn && !loadMoreBtn.classList.contains("ajax-nomore") && !failed) {
loadMoreBtn.innerHTML = triggerText;
if (mode === "infinite") {
var lazyTrigger = clickTarget.closest(".next-page") || clickTarget;
if (iasMax > 0 && successfulAutoLoads >= iasMax) {
container.classList.remove("ias-pending");
lazyTrigger.classList.remove("lazyload", "lazyloaded", "lazyloading");
lazyTrigger.removeAttribute("lazyload-action");
} else {
lazyTrigger.classList.remove("lazyloaded", "lazyloading");
lazyTrigger.classList.add("lazyload");
if (window.lazySizes && lazySizes.loader) {
lazySizes.loader.checkElems();
}
}
}
}
});
}
if (loadMoreBtn) {
clickTarget.addEventListener("click", function(e) {
// 排序菜单(.ajax-option)与 tab pane 触发器(.post_ajax_trigger)归 legacy
// runtime post_ajax() 管线;本引擎避开,否则一次点击发两个请求。
if (e.target.closest && e.target.closest(".ajax-option, .post_ajax_trigger")) {
return;
}
e.preventDefault();
if (loadMoreBtn.dataset.jumpComments === "1") {
var jumpInput = container.querySelector(".jump-input");
if (!jumpInput) return;
var targetPage = parseInt(jumpInput.value, 10) || 1;
var max = parseInt(jumpInput.dataset.maxPages || String(maxPages), 10) || maxPages;
targetPage = Math.max(1, Math.min(max, targetPage));
// 评论跳页:本地 DOM 刷新,地址栏不携带 ? 参数。
if (window.AksrtComments && typeof window.AksrtComments.loadPage === "function") {
e.preventDefault();
window.AksrtComments.loadPage(targetPage);
return;
}
window.location.hash = "comments";
window.location.reload();
return;
}
if (loadMoreBtn.dataset.jumpArchive === "1") {
var archiveJumpInput = container.querySelector(".jump-input");
if (!archiveJumpInput) return;
var archiveTargetPage = parseInt(archiveJumpInput.value, 10) || 1;
var archiveMax = parseInt(archiveJumpInput.dataset.maxPages || String(maxPages), 10) || maxPages;
archiveTargetPage = Math.max(1, Math.min(archiveMax, archiveTargetPage));
var archiveUrl = new URL(window.location.href);
archiveUrl.searchParams.set(loadMoreBtn.dataset.pageVar || "paged", String(archiveTargetPage));
window.location.href = archiveUrl.toString();
return;
}
var fromAuto = clickTarget.dataset.lazyAuto === "1";
delete clickTarget.dataset.lazyAuto;
if (fromAuto && iasMax > 0 && successfulAutoLoads >= iasMax) {
container.classList.remove("ias-pending");
return;
}
// Manual click can recover from a previous auto-load failure.
failed = false;
loadNextPage(fromAuto);
});
}
// The runtime lazySizes dispatches lazybeforeunveil natively but follows up
// with a jQuery-simulated click, which native listeners never receive.
// Route the unveil trigger through the same click pipeline.
if (container.classList.contains("lazyload") || clickTarget.classList.contains("lazyload")) {
container.addEventListener("lazybeforeunveil", function (e) {
if (e.target !== container && e.target !== clickTarget) return;
clickTarget.dataset.lazyAuto = "1";
clickTarget.click();
});
}
if (mode === "infinite") {
if ((isComment && window.aksrtData && aksrtData.commentIasAuto === false) || !hasAjaxConfig()) {
var autoTrigger = clickTarget.closest(".next-page") || clickTarget;
autoTrigger.classList.remove("lazyload");
autoTrigger.removeAttribute("lazyload-action");
return;
}
container.classList.add("ias-pending");
}
});
}
function init(root) {
initAjaxPagination(root);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () { init(); });
} else {
init();
}
// AJAX 注入新内容后由 auto_fun 重扫(container 级 data 标记保证幂等)。
if (window.jQuery) {
window.jQuery(document).on("auto_fun", function () { init(); });
}
})();