feat(widgets): replicate 12 reference widgets for full Zibll parity

- rewrite schemas for posts, tab_posts, oneline_posts, hot_posts,
  term_cards, term_aggregation, media_cards, combo_info, partners,
  recent_comments, posts_toc and slider_media_combo to mirror the
  modern reference modules field by field (options, defaults,
  dependencies, min/max/step, subheadings)
- port reference renderers: hot ranking with TOP badges, category
  aggregation card, graphic cover cards, partners CSS marquee,
  combo info cards with auto band palette, recent comment list
- rename 分类与标签聚合卡片 to 分类聚合卡片 and restrict it to
  categories (no tags/topics branch in this project)
- drop topics filter fields and the topics query branch; align
  orderby value domain with supported post meta
- add searchable sortable terms admin field type and spinner unit
  suffix rendering
- unify widget pagination and lazy loading on the REST client with
  the runtime-owned ias reveal trigger; remove the legacy
  post_ajax_trigger dual path
- align section-navs TOC with the reference contract (wznav ids,
  3-heading minimum, zib-widget box)
This commit is contained in:
2026-09-05 11:28:00 +08:00
parent 9dc278bd83
commit b639d57c5b
25 changed files with 1388 additions and 446 deletions
+32 -28
View File
@@ -5,7 +5,7 @@ namespace AKSRT\WidgetSystem;
defined('ABSPATH') || exit;
/**
* Shared list/card/mini markup and pagination payloads.
* Shared list/card/mini markup and pagination payloads (reference parity).
*/
final class PostRenderer {
public static function payload(array $instance, string $slug, int $page = 1, string $segment = ''): array {
@@ -26,35 +26,32 @@ final class PostRenderer {
public static function items(\WP_Query $query, array $instance, string $slug, int $page = 1): string {
if (!$query->have_posts()) {
return RendererHelpers::empty_state(__('暂无内容', 'aksrt'));
return function_exists('aksrt_widget_ajax_null') ? aksrt_widget_ajax_null() : RendererHelpers::empty_state(__('暂无内容', 'aksrt'));
}
$style = (string) ($instance['style'] ?? 'card');
$mode = in_array($style, ['list', 'card', 'mini'], true) ? $style : 'card';
$mini_opt = (array) ($instance['mini_opt'] ?? []);
$config = [
'type' => $mode === 'card' ? 'card' : 'list',
'show_number' => $slug === 'hot_posts' || in_array('show_number', (array) ($instance['mini_opt'] ?? []), true),
'show_thumb' => $slug === 'tab_posts'
? in_array('show_thumb', (array) ($instance['mini_opt'] ?? []), true)
: ($slug !== 'hot_posts' || !array_key_exists('show_thumbnail', $instance) || !empty($instance['show_thumbnail'])),
'show_meta' => $slug !== 'tab_posts' || in_array('show_meta', (array) ($instance['mini_opt'] ?? []), true),
'show_number' => in_array('show_number', $mini_opt, true),
'show_thumb' => in_array('show_thumb', $mini_opt, true),
'show_meta' => in_array('show_meta', $mini_opt, true),
'number_offset' => ($page - 1) * max(1, (int) ($instance['count'] ?? 8)),
'wrapper_class' => 'aksrt-widget-posts aksrt-widget-posts--' . $slug,
'widget_style' => $mode,
];
if ($slug === 'oneline_posts') {
return self::oneline($query, $config, !empty($instance['autoplay']));
return self::oneline($query, $config, $mode);
}
if ($mode === 'mini') {
return aksrt_render_mini_list($query, $config, false);
return aksrt_render_mini_list($query, array_merge($config, ['class' => 'ajax-item']), false);
}
return aksrt_render_post_list($config, $query, false);
// The .widget-ajaxpager container rendered by the caller is the ajax
// target, so item markup goes in without the page-level list wrapper.
return aksrt_render_post_list(array_merge($config, ['skip_wrapper' => true]), $query, false);
}
private static function oneline(\WP_Query $query, array $config, bool $autoplay): string {
private static function oneline(\WP_Query $query, array $config, string $style): string {
$items = '';
$style = in_array(($config['widget_style'] ?? 'card'), ['list', 'card', 'mini'], true)
? (string) $config['widget_style']
: 'card';
while ($query->have_posts()) {
$query->the_post();
if ($style === 'mini') {
@@ -74,30 +71,37 @@ final class PostRenderer {
}
}
wp_reset_postdata();
$autoplay_attr = $autoplay ? ' data-autoplay="true"' : '';
return '<div class="widget-main-post mb20 style-' . esc_attr($style) . '"><div class="swiper-container swiper-scroll" data-slideClass="posts-item"' . $autoplay_attr . '><div class="swiper-wrapper">' . $items . '</div><div class="swiper-button-prev"></div><div class="swiper-button-next"></div></div></div>';
return '<div class="widget-main-post mb20 style-' . esc_attr($style) . '"><div class="swiper-container swiper-scroll" data-slideClass="posts-item"><div class="swiper-wrapper">' . $items . '</div><div class="swiper-button-prev"></div><div class="swiper-button-next"></div></div></div>';
}
/**
* Pagination contract: '' keeps the hidden stub, ajax renders the load
* more trigger, number renders numbered page links; all targets carry the
* `.widget-ajaxpager` reference attribute. Interactive controls stay clear
* of the runtime's legacy `.ajax-next` HTML pipeline — the widget system
* owns the REST client in frontend.js.
*/
private static function pagination(array $instance, int $page, int $max_pages): string {
$mode = (string) ($instance['paginate'] ?? '');
if ($mode === '' || $mode === 'none') {
return '<div class="ajax-pag hide"><div class="next-page"><a href="#"></a></div></div>';
}
if ($max_pages <= 1) {
return '';
}
if ($mode === '' || $mode === 'none') {
return '<div class="ajax-pag hide"><div class="next-page ajax-next"><a href="#"></a></div></div>';
}
if ($mode === 'number' || $mode === 'numbered') {
$links = '';
$start = max(1, min($page - 2, $max_pages - 4));
$end = min($max_pages, max(5, $page + 2));
$html = '<nav class="aksrt-widget-pagination" aria-label="' . esc_attr__('文章分页', 'aksrt') . '">';
$end = min($max_pages, max($start + 4, $page + 2));
for ($number = $start; $number <= $end; $number++) {
$html .= '<button type="button" data-widget-page="' . $number . '"' . ($number === $page ? ' aria-current="page"' : '') . '>' . $number . '</button>';
if ($number === $page) {
$links .= '<span class="page-numbers current">' . $number . '</span>';
continue;
}
$links .= '<a class="next-page page-numbers" ajaxpager-target=".widget-ajaxpager" ajax-replace="true" data-widget-page="' . $number . '" role="button" tabindex="0" href="#page-' . $number . '">' . $number . '</a>';
}
return $html . '</nav>';
return '<div class="pagenav ajax-pag" role="navigation" aria-label="' . esc_attr__('文章分页', 'aksrt') . '">' . $links . '</div>';
}
$label = __('加载更多', 'aksrt');
$class = 'aksrt-widget-load-more';
$action = '';
return '<button class="' . $class . '"' . $action . ' type="button" data-next-page="' . (int) ($page + 1) . '" data-mode="' . esc_attr($mode) . '">' . esc_html($label) . '</button>';
return '<div class="text-center theme-pagination ajax-pag"><div no-scroll="true" ajaxpager-target=".widget-ajaxpager" class="next-page"><a role="button" tabindex="0" href="#next" data-next-page="' . (int) ($page + 1) . '">' . esc_html__('加载更多', 'aksrt') . '</a></div></div>';
}
}