首页「更多栏目」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>
241 lines
11 KiB
PHP
241 lines
11 KiB
PHP
<?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);
|
||
}
|
||
}
|