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

241 lines
11 KiB
PHP
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.
<?php
/**
* legacy 9.0 article-list DOM contract.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
if (!function_exists('aksrt_renderer_list')) {
function aksrt_renderer_list(array $data, array $cfg): string {
$classes = ['posts-item', 'list', 'ajax-item'];
if ((string) ($cfg['style'] ?? '') !== '') {
$classes[] = (string) $cfg['style'];
}
if (!empty($data['is_mult_thumb'])) {
$classes[] = 'mult-thumb';
}
if (!empty($data['is_no_thumb'])) {
$classes[] = 'no-thumb';
}
if (!empty($cfg['no_margin'])) {
$classes[] = 'no_margin';
}
$classes[] = ($cfg['style'] ?? '') === 'style2' ? 'flex xx' : 'flex';
$title = '<h2 class="item-heading' . (!empty($cfg['heading_bold']) ? ' font-bold' : '') . '"><a' . (!empty($cfg['target_blank']) ? ' target="_blank"' : '') . ' href="' . esc_url((string) $data['permalink']) . '">' . aksrt_renderer_title_markup($data, $cfg) . '</a></h2>';
$excerpt_class = ($cfg['style'] ?? '') === 'style2' ? 'text-ellipsis-2' : 'text-ellipsis';
$excerpt = '<div class="item-excerpt muted-color ' . $excerpt_class . ' mb6">' . aksrt_renderer_excerpt_markup($data, $cfg) . '</div>';
$thumb = aksrt_renderer_thumb($data, $cfg);
$mult = aksrt_renderer_mult_thumb($data, $cfg);
$graphic = '';
if ($thumb !== '' && empty($data['is_mult_thumb'])) {
$graphic = '<div class="post-graphic' . (($cfg['img_position'] ?? 'left') === 'right' ? ' order1' : '') . '">' . $thumb . '</div>';
}
$badges = aksrt_renderer_badges($data, $cfg);
$meta = aksrt_renderer_meta($data, $cfg, false);
$html = '<posts class="' . esc_attr(implode(' ', $classes)) . '">';
if (($cfg['style'] ?? '') === 'style2') {
$html .= $title . '<div class="flex">' . $graphic . '<div class="item-body flex xx flex1 jsb">';
$html .= $mult !== '' ? $mult : $excerpt;
$html .= $badges . $meta . '</div></div>';
} else {
$html .= $graphic . '<div class="item-body flex xx flex1 jsb">' . $title;
$html .= $mult !== '' ? $mult : $excerpt;
$html .= '<div>' . $badges . $meta . '</div></div>';
}
return $html . '</posts>';
}
}
if (!function_exists('aksrt_renderer_card')) {
function aksrt_renderer_card(array $data, array $cfg): string {
$classes = ['posts-item', 'card', 'ajax-item'];
if ((string) ($cfg['style'] ?? '') !== '') {
$classes[] = (string) $cfg['style'];
}
$title = '<h2 class="item-heading' . (!empty($cfg['heading_bold']) ? ' font-bold' : '') . '"><a' . (!empty($cfg['target_blank']) ? ' target="_blank"' : '') . ' href="' . esc_url((string) $data['permalink']) . '">' . aksrt_renderer_title_markup($data, $cfg) . '</a></h2>';
$html = '<posts class="' . esc_attr(implode(' ', $classes)) . '">';
$html .= aksrt_renderer_thumb($data, $cfg);
$html .= '<div class="item-body">' . $title;
$html .= aksrt_renderer_badges($data, $cfg);
$html .= aksrt_renderer_meta($data, $cfg, true);
return $html . '</div></posts>';
}
}
if (!function_exists('aksrt_renderer_mini')) {
function aksrt_renderer_mini(array $data, array $cfg = []): string {
$target = !empty($cfg['target_blank']) ? ' target="_blank"' : '';
$thumb = '';
$show_thumb = !array_key_exists('show_thumb', $cfg) || !empty($cfg['show_thumb']);
$show_meta = !array_key_exists('show_meta', $cfg) || !empty($cfg['show_meta']);
if ($show_thumb && (string) $data['primary'] !== '') {
$default_attr = empty($data['has_real']) ? ' data-thumb="default"' : '';
$thumb = '<div class="mr10"><div class="item-thumbnail"><a' . $target . ' href="' . esc_url((string) $data['permalink']) . '"><img' . $default_attr . aksrt_get_lazy_attr('lazy_posts_thumb', (string) $data['primary'], 'fit-cover radius8') . ' alt="' . esc_attr((string) $data['title']) . '"></a></div></div>';
}
$number = '';
if (!empty($cfg['show_number'])) {
$number_value = max(1, (int) ($cfg['number_offset'] ?? 1));
$colors = ['c-red', 'c-yellow', 'c-purple', 'c-blue', 'c-green'];
$number_class = $colors[$number_value - 1] ?? '';
$number = '<span class="badg badg-sm mr3 ' . esc_attr($number_class) . '">' . $number_value . '</span>';
}
$title_class = $show_thumb ? ' text-ellipsis-2' : ' text-ellipsis' . (!$show_meta && empty($cfg['show_number']) ? ' icon-circle' : '');
$title = esc_html((string) $data['title']);
if ((string) ($data['subtitle'] ?? '') !== '') {
$title .= '<span class="focus-color">' . esc_html((string) $data['subtitle']) . '</span>';
}
$meta = '';
if ($show_meta) {
$show_time = !empty($cfg['show_time']);
if (!empty($cfg['show_author'])) {
$author_value = $show_time ? (string) $data['time_ago'] : (string) $data['author_name'];
$meta_left = '<item class="meta-author flex ac"><img class="avatar-mini" src="' . esc_url((string) $data['avatar_url']) . '" alt="" width="20" height="20"><span class="ml6">' . esc_html($author_value) . '</span></item>';
} else {
$meta_left = $show_time ? '<item title="' . esc_attr((string) $data['time_title']) . '" class="icon-circle mln3">' . esc_html((string) $data['time_ago']) . '</item>' : '';
}
$pay = aksrt_renderer_pay_badge($data, true);
if ($meta_left === '') {
$meta_left = $pay;
$pay = '';
}
if ($meta_left === '') {
$meta_left = '<item></item>';
}
$meta_right = $pay . '<item class="meta-view">' . aksrt_renderer_svg_icon('view') . esc_html(aksrt_renderer_meta_number((int) $data['views'])) . '</item>';
$meta = '<div class="item-meta muted-2-color flex jsb ac' . (!$show_thumb ? ' mt6' : '') . '">' . $meta_left . '<div class="meta-right">' . $meta_right . '</div></div>';
}
$class = trim('posts-mini ' . (string) ($cfg['class'] ?? ''));
return '<div class="' . esc_attr($class) . '">' . $thumb . '<div class="posts-mini-con flex xx flex1 jsb"><h2 class="item-heading' . esc_attr($title_class) . '">' . $number . '<a' . $target . ' href="' . esc_url((string) $data['permalink']) . '">' . $title . '</a></h2>' . $meta . '</div></div>';
}
}
if (!function_exists('aksrt_render_post')) {
function aksrt_render_post(array $args = []): string {
$cfg = aksrt_renderer_config($args);
$data = aksrt_renderer_collect_post_data($cfg);
if (!$data) {
return '';
}
return ($cfg['mode'] ?? 'list') === 'card'
? aksrt_renderer_card($data, $cfg)
: aksrt_renderer_list($data, $cfg);
}
}
if (!function_exists('aksrt_render_post_list')) {
/**
* Render the canonical article stream.
*
* @param array<string,mixed> $args legacy-shaped list arguments.
* @param WP_Query|null $query Optional custom query.
*/
function aksrt_render_post_list(array $args = [], ?WP_Query $query = null, bool $echo = true): string {
$cfg = aksrt_renderer_config($args);
$has_posts = $query instanceof WP_Query ? $query->have_posts() : have_posts();
if (!$has_posts) {
$html = aksrt_get_null(__('暂无内容', 'aksrt'), 100, 'null-post.svg');
if ($echo) {
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return '';
}
return $html;
}
$classes = ['posts-row', 'ajaxpager'];
foreach (preg_split('/\s+/', trim((string) ($cfg['wrapper_class'] ?? ''))) ?: [] as $class) {
$class = sanitize_html_class($class);
if ($class !== '') {
$classes[] = $class;
}
}
// legacy 契约:分页等控件与列表同处一个 .ajaxpager 内,供 runtime post_ajax 提取。
$append_inner_html = (string) ($cfg['append_inner_html'] ?? '');
$html = empty($cfg['skip_wrapper'])
? '<div class="' . esc_attr(implode(' ', array_unique($classes))) . '" data-view="' . esc_attr((string) $cfg['mode']) . '">'
: '';
if ($query instanceof WP_Query) {
while ($query->have_posts()) {
$query->the_post();
$html .= aksrt_render_post($cfg);
}
wp_reset_postdata();
} else {
while (have_posts()) {
the_post();
$html .= aksrt_render_post($cfg);
}
}
$html .= $append_inner_html;
if (empty($cfg['skip_wrapper'])) {
$html .= '</div>';
}
if ($echo) {
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return '';
}
return $html;
}
}
if (!function_exists('aksrt_render_mini_list')) {
function aksrt_render_mini_list(WP_Query $query, array $args = [], bool $echo = true): string {
$cfg = aksrt_renderer_config($args);
$html = '';
$number = max(0, (int) ($args['number_offset'] ?? 0));
while ($query->have_posts()) {
$query->the_post();
$data = aksrt_renderer_collect_post_data($cfg);
if (!$data) {
continue;
}
$number++;
$html .= aksrt_renderer_mini($data, array_merge($cfg, ['number_offset' => $number]));
}
wp_reset_postdata();
if ($echo) {
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return '';
}
return $html;
}
}
if (!function_exists('aksrt_render_placeholder')) {
/**
* 骨架屏占位,1:1 对齐参考主题 `zib_get_post_placeholder()`。
*
* 骨架块复用真实卡片的 posts-item / post-graphic / item-body 结构与尺寸,
* 只是把内容换成 .placeholder。列表模式四行(标题 t1、摘要 k1、k2、两枚 s1),
* 卡片模式三行(标题 t1、摘要 k2、两枚 s1)。
*
* 刻意与真实条目不同的两点(均与参考主题一致):
* - **不写 `ajax-item`**:否则 `find('.ajax-item')` / ajax-list 的 `inject()`
* 会把骨架当成真实条目删除或定位。
* - **元素是 `<div>` 而非 `<posts>`**,且 card 分支的缩略图是
* `.item-thumbnail` 直接子元素(不经 `.post-graphic`,那个带 20px 右边距,
* 是列表变体的 wrapper)。
*
* @param string $mode list|card|mini|mini_thumb。
* @param int $count 重复数量,1–12。
*/
function aksrt_render_placeholder(string $mode = 'list', int $count = 3): string {
$count = max(1, min(12, $count));
if ($mode === 'mini') {
$item = '<div class="posts-mini"><div class="placeholder k1"></div></div>';
} elseif ($mode === 'mini_thumb') {
$item = '<div class="posts-mini "><div class="mr10"><div class="item-thumbnail placeholder"></div></div><div class="posts-mini-con flex xx flex1 jsb"><div class="placeholder t1"></div><div class="placeholder s1"></div></div></div>';
} elseif ($mode === '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>';
} else {
$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>';
}
return str_repeat($item, $count);
}
}