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:
@@ -7,7 +7,7 @@ use AKSRT\WidgetSystem\RendererHelpers;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** Render article discovery and paid-content widgets. */
|
||||
/** Render article discovery and paid-content widgets (reference parity). */
|
||||
final class ContentRenderer {
|
||||
public static function render(string $slug, array $instance, array $context = []): string {
|
||||
switch ($slug) {
|
||||
@@ -15,110 +15,71 @@ final class ContentRenderer {
|
||||
case 'term_aggregation': return self::term_aggregation($instance);
|
||||
case 'post_pay': return self::post_pay($instance);
|
||||
case 'tab_posts': return self::tab_posts($instance, $context);
|
||||
case 'posts':
|
||||
case 'hot_posts':
|
||||
case 'oneline_posts': return self::posts($slug, $instance, $context);
|
||||
case 'posts': return self::posts($instance, $context, 'posts');
|
||||
case 'hot_posts': return function_exists('aksrt_widget_hot_posts') ? aksrt_widget_hot_posts($instance) : '';
|
||||
case 'oneline_posts': return self::posts($instance, $context, 'oneline_posts');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private static function terms(array $instance): array {
|
||||
$args = [
|
||||
'taxonomy' => (string) $instance['taxonomy'],
|
||||
'number' => (int) $instance['count'],
|
||||
'hide_empty' => true,
|
||||
'orderby' => 'count',
|
||||
'order' => 'DESC',
|
||||
];
|
||||
$ids = RendererHelpers::ids((string) $instance['term_ids']);
|
||||
if ($ids) {
|
||||
$args['include'] = $ids;
|
||||
$args['orderby'] = 'include';
|
||||
}
|
||||
$terms = get_terms($args);
|
||||
return is_wp_error($terms) ? [] : (array) $terms;
|
||||
}
|
||||
|
||||
private static function term_cards(array $instance): string {
|
||||
$terms = self::terms($instance);
|
||||
if (!$terms) {
|
||||
return RendererHelpers::empty_state(__('暂无分类', 'aksrt'));
|
||||
}
|
||||
$html = '<div class="aksrt-widget-grid aksrt-term-cards" style="--aksrt-cols:' . (int) $instance['pc_row'] . ';--aksrt-cols-m:' . (int) $instance['m_row'] . '">';
|
||||
foreach ($terms as $term) {
|
||||
$link = get_term_link($term);
|
||||
if (is_wp_error($link)) {
|
||||
continue;
|
||||
}
|
||||
$image = (string) get_term_meta((int) $term->term_id, 'aksrt_cover_url', true);
|
||||
if ($image === '') {
|
||||
$image = (string) get_term_meta((int) $term->term_id, 'cover_url', true);
|
||||
}
|
||||
$html .= '<a class="aksrt-term-card" href="' . esc_url($link) . '"><span class="aksrt-term-card__media">' . RendererHelpers::image($image, $term->name) . '</span><span class="aksrt-term-card__body"><strong>' . esc_html($term->name) . '</strong><small>' . esc_html(wp_trim_words((string) $term->description, 18)) . '</small><em>' . sprintf(esc_html__('%s 篇内容', 'aksrt'), number_format_i18n((int) $term->count)) . '</em></span></a>';
|
||||
}
|
||||
return $html . '</div>';
|
||||
}
|
||||
|
||||
private static function term_aggregation(array $instance): string {
|
||||
$terms = self::terms($instance);
|
||||
if (!$terms) {
|
||||
return RendererHelpers::empty_state(__('暂无聚合内容', 'aksrt'));
|
||||
}
|
||||
$taxonomy = (string) $instance['taxonomy'];
|
||||
$term_ids = array_map(static fn($term): int => (int) $term->term_id, $terms);
|
||||
$query = new \WP_Query([
|
||||
'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => min(100, count($terms) * 6),
|
||||
'ignore_sticky_posts' => true, 'no_found_rows' => true,
|
||||
'tax_query' => [['taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $term_ids]],
|
||||
]);
|
||||
$by_term = [];
|
||||
foreach ($query->posts as $post) {
|
||||
foreach (wp_get_post_terms((int) $post->ID, $taxonomy, ['fields' => 'ids']) as $term_id) {
|
||||
if (in_array((int) $term_id, $term_ids, true) && count($by_term[$term_id] ?? []) < 4) {
|
||||
$by_term[$term_id][] = $post;
|
||||
}
|
||||
}
|
||||
}
|
||||
$html = '<div class="aksrt-term-aggregation">';
|
||||
foreach ($terms as $term) {
|
||||
$link = get_term_link($term);
|
||||
if (is_wp_error($link)) {
|
||||
continue;
|
||||
}
|
||||
$html .= '<section class="aksrt-term-aggregation__card"><header><a href="' . esc_url($link) . '"><strong>' . esc_html($term->name) . '</strong><span>' . esc_html__('查看全部', 'aksrt') . '</span></a></header><ul>';
|
||||
foreach ($by_term[$term->term_id] ?? [] as $post) {
|
||||
$html .= '<li><a href="' . esc_url(get_permalink($post)) . '">' . get_the_post_thumbnail($post, 'thumbnail') . '<span>' . esc_html(get_the_title($post)) . '</span></a></li>';
|
||||
}
|
||||
$html .= '</ul></section>';
|
||||
}
|
||||
return $html . '</div>';
|
||||
}
|
||||
|
||||
private static function posts(string $slug, array $instance, array $context): string {
|
||||
private static function posts(array $instance, array $context, string $slug): string {
|
||||
$widget_id = (string) ($context['widget_id'] ?? '');
|
||||
if (($instance['load_mode'] ?? 'sync') === 'ajax') {
|
||||
return '<div class="aksrt-widget-items is-loading" data-widget-items data-widget="' . esc_attr($slug) . '" data-instance="' . esc_attr($widget_id) . '" data-page="1" aria-live="polite" aria-busy="true"><div class="aksrt-widget-skeleton" role="status" aria-label="' . esc_attr__('正在加载', 'aksrt') . '"></div></div>';
|
||||
$style = in_array(($instance['style'] ?? 'list'), ['list', 'card', 'mini'], true) ? (string) $instance['style'] : 'list';
|
||||
if ($slug === 'oneline_posts') {
|
||||
// Reference parity: the swiper row is fully rendered server-side.
|
||||
$query = \AKSRT\WidgetSystem\PostQuery::build(array_merge($instance, ['paginate' => '']), 1, []);
|
||||
return \AKSRT\WidgetSystem\PostRenderer::items($query, $instance, 'oneline_posts');
|
||||
}
|
||||
$payload = PostRenderer::payload($instance, $slug, 1);
|
||||
return '<div class="aksrt-widget-items" data-widget-items data-widget="' . esc_attr($slug) . '" data-instance="' . esc_attr($widget_id) . '" data-page="1" aria-live="polite" aria-busy="false">' . $payload['html'] . $payload['paginate_html'] . '</div>';
|
||||
$inner = self::ajaxpager($instance, $slug, $widget_id, 1, '');
|
||||
$html = '<div class="widget-main-post mb20 style-' . esc_attr($style) . '">';
|
||||
$html .= $style === 'mini' ? '<div class="zib-widget posts-mini-lists">' : '';
|
||||
$html .= $inner;
|
||||
$html .= $style === 'mini' ? '</div>' : '';
|
||||
return $html . '</div>';
|
||||
}
|
||||
|
||||
/** The .widget-ajaxpager container contract shared by posts and tab panes. */
|
||||
private static function ajaxpager(array $instance, string $slug, string $widget_id, int $page, string $segment): string {
|
||||
$attrs = ' data-widget-items data-widget="' . esc_attr($slug) . '" data-instance="' . esc_attr($widget_id) . '" data-page="' . (int) $page . '"';
|
||||
if ($segment !== '') {
|
||||
$attrs .= ' data-segment="' . esc_attr($segment) . '"';
|
||||
}
|
||||
if (($instance['load_mode'] ?? '') === 'ajax') {
|
||||
// The runtime (lazysizes + lazyload-action) reveals this container
|
||||
// and clicks the single .ias-btn trigger; frontend.js then loads
|
||||
// the first page through the REST client.
|
||||
$trigger = '<span class="hide aksrt-ias-trigger"><a class="ias-btn" no-scroll="true" href="javascript:void(0)" data-widget-ias="1" data-next-page="1" aria-hidden="true" tabindex="-1"></a></span>';
|
||||
return '<div class="widget-ajaxpager lazyload" lazyload-action="ias"' . $attrs . ' aria-live="polite" aria-busy="true">' . $trigger . '<div class="post_ajax_loader" role="status" aria-label="' . esc_attr__('正在加载', 'aksrt') . '">' . self::placeholder($instance) . '</div></div>';
|
||||
}
|
||||
$payload = PostRenderer::payload($instance, $slug, $page, $segment);
|
||||
return '<div class="widget-ajaxpager"' . $attrs . ' aria-live="polite" aria-busy="false">' . $payload['html'] . $payload['paginate_html'] . '</div>';
|
||||
}
|
||||
|
||||
private static function placeholder(array $instance): string {
|
||||
$style = (string) ($instance['style'] ?? 'list');
|
||||
$count = max(1, (int) ($instance['count'] ?? 6));
|
||||
if ($style === 'mini') {
|
||||
$mini_opt = (array) ($instance['mini_opt'] ?? []);
|
||||
if (in_array('show_thumb', $mini_opt, true)) {
|
||||
$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>';
|
||||
return str_repeat($item, $count);
|
||||
}
|
||||
return str_repeat('<div class="posts-mini"><div class="placeholder k1"></div></div>', $count);
|
||||
}
|
||||
return aksrt_render_placeholder($style === 'card' ? 'card' : 'lists', $count);
|
||||
}
|
||||
|
||||
private static function tab_posts(array $instance, array $context): string {
|
||||
$style = in_array(($instance['style'] ?? 'mini'), ['list', 'card', 'mini'], true) ? (string) $instance['style'] : 'mini';
|
||||
$tabs = array_values((array) ($instance['tabs'] ?? []));
|
||||
if (!$tabs) {
|
||||
return RendererHelpers::empty_state(__('请配置文章栏目', 'aksrt'));
|
||||
return '';
|
||||
}
|
||||
$widget_id = (string) ($context['widget_id'] ?? 'preview');
|
||||
$buttons = '';
|
||||
$key = sanitize_html_class((string) ($context['widget_id'] ?? 'preview')) ?: 'preview';
|
||||
$panel_id = 'aksrt-tab-panel-' . $key;
|
||||
$ajax_url = rest_url('aksrt/v1/widgets/tab_posts/items');
|
||||
$first_tab_id = 'aksrt-tab-' . $key . '-0';
|
||||
$placeholder_type = $style === 'card' ? 'card' : 'lists';
|
||||
$placeholder = $style === 'mini'
|
||||
? str_repeat('<div class="posts-mini"><div class="placeholder k1"></div></div>', max(1, (int) ($instance['count'] ?? 6)))
|
||||
: aksrt_render_placeholder($placeholder_type, max(1, (int) ($instance['count'] ?? 6)));
|
||||
$panels = '';
|
||||
$key = sanitize_html_class($widget_id) ?: 'preview';
|
||||
$panel_id = 'aksrt-tab-panel-' . $key;
|
||||
$visible_index = 0;
|
||||
foreach ($tabs as $index => $tab) {
|
||||
if (!is_array($tab) || trim((string) ($tab['title'] ?? '')) === '') continue;
|
||||
@@ -126,16 +87,117 @@ final class ContentRenderer {
|
||||
$selected = $visible_index === 0;
|
||||
$buttons .= '<li class="' . ($selected ? 'active' : '') . '"><a' . ($selected ? '' : ' data-ajax') . ' data-toggle="tab" href="#' . esc_attr($panel_id . '-' . $visible_index) . '" role="tab" id="' . esc_attr($tab_id) . '" aria-controls="' . esc_attr($panel_id . '-' . $visible_index) . '" aria-selected="' . ($selected ? 'true' : 'false') . '" data-segment="' . (int) $index . '">' . wp_kses_post((string) $tab['title']) . '</a></li>';
|
||||
if ($selected) {
|
||||
$payload = PostRenderer::payload($instance, 'tab_posts', 1, (string) $index);
|
||||
$body = $payload['html'] . $payload['paginate_html'] . '<div class="post_ajax_loader" style="display:none;">' . $placeholder . '</div>';
|
||||
$body = self::ajaxpager($instance, 'tab_posts', $widget_id, 1, (string) $index);
|
||||
} else {
|
||||
$body = '<span class="post_ajax_trigger hide"><a ajaxpager-target=".widget-ajaxpager" class="ajax_load ajax-next ajax-open" data-aksrt-load="1" no-scroll="true" href="' . esc_url(add_query_arg(['widget' => 'tab_posts', 'instance' => (string) ($context['widget_id'] ?? ''), 'page' => 1, 'segment' => $index], $ajax_url)) . '"></a></span><div class="post_ajax_loader" style="display:none;">' . $placeholder . '</div>';
|
||||
// Inactive panes load lazily through the REST client on switch.
|
||||
$body = '<div class="widget-ajaxpager" data-widget-items data-widget="tab_posts" data-instance="' . esc_attr($widget_id) . '" data-page="1" data-segment="' . (int) $index . '" aria-live="polite" aria-busy="false"></div>';
|
||||
}
|
||||
$panels .= '<div class="tab-pane fade' . ($selected ? ' active in' : '') . '" id="' . esc_attr($panel_id . '-' . $visible_index) . '" role="tabpanel" aria-labelledby="' . esc_attr($tab_id) . '"><div class="widget-ajaxpager" data-widget-items data-widget="tab_posts" data-instance="' . esc_attr((string) ($context['widget_id'] ?? '')) . '" data-segment="' . (int) $index . '" aria-live="polite" aria-busy="false">' . $body . '</div></div>';
|
||||
$body .= '<div class="post_ajax_loader" style="display:none;">' . self::placeholder($instance) . '</div>';
|
||||
$panels .= '<div class="tab-pane fade' . ($selected ? ' active in' : '') . '" id="' . esc_attr($panel_id . '-' . $visible_index) . '" role="tabpanel" aria-labelledby="' . esc_attr($tab_id) . '">' . $body . '</div>';
|
||||
$visible_index++;
|
||||
}
|
||||
if ($buttons === '') return RendererHelpers::empty_state(__('请配置文章栏目', 'aksrt'));
|
||||
return '<div class="widget-tab-post style-' . esc_attr($style) . ($style === 'mini' ? ' posts-mini-lists zib-widget' : ' index-tab') . '" data-widget="tab_posts" data-instance="' . esc_attr((string) ($context['widget_id'] ?? '')) . '"><div class="relative' . ($style === 'card' ? ' mb20' : '') . '"><ul class="list-inline scroll-x no-scrollbar' . ($style === 'mini' ? ' tab-nav-theme' : '') . '" role="tablist">' . $buttons . '</ul></div><div class="tab-content">' . $panels . '</div></div>';
|
||||
if ($buttons === '') return '';
|
||||
return '<div class="widget-tab-post style-' . esc_attr($style) . ($style === 'mini' ? ' posts-mini-lists zib-widget' : ' index-tab relative-h') . '" data-widget="tab_posts" data-instance="' . esc_attr($widget_id) . '"><div class="relative' . ($style === 'card' ? ' mb20' : '') . '"><ul class="list-inline scroll-x no-scrollbar' . ($style === 'mini' ? ' tab-nav-theme' : '') . '" role="tablist">' . $buttons . '</ul></div><div class="tab-content">' . $panels . '</div></div>';
|
||||
}
|
||||
|
||||
private static function term_cards(array $instance): string {
|
||||
$term_ids = array_values(array_filter(array_map('absint', (array) ($instance['term_id'] ?? []))));
|
||||
if (!$term_ids) {
|
||||
return '';
|
||||
}
|
||||
$terms = get_terms(['taxonomy' => 'category', 'include' => $term_ids, 'orderby' => 'include']);
|
||||
if (is_wp_error($terms) || !$terms) {
|
||||
return '';
|
||||
}
|
||||
$pc_row = max(1, (int) ($instance['pc_row'] ?? 2));
|
||||
$m_row = max(1, (int) ($instance['m_row'] ?? 2));
|
||||
$row_class = 'col-sm-' . (int) (12 / $pc_row);
|
||||
$row_class .= $m_row > 1 ? ' col-xs-' . (int) (12 / $m_row) : '';
|
||||
$is_row = count($terms) > 1;
|
||||
$target_blank = !empty($instance['target_blank']) ? '_blank' : '';
|
||||
$animation = (string) ($instance['obs_animation'] ?? '');
|
||||
$html = '';
|
||||
foreach ($terms as $term) {
|
||||
$count = (int) $term->count;
|
||||
$name = aksrt_widget_str_cut((string) $term->name, 16);
|
||||
$description = aksrt_widget_str_cut((string) $term->description, 24);
|
||||
$href = get_term_link($term);
|
||||
if (is_wp_error($href)) {
|
||||
continue;
|
||||
}
|
||||
$icon = '<i class="fa fa-folder-open-o mr6" aria-hidden="true"></i>';
|
||||
$card = [
|
||||
'type' => (string) ($instance['type'] ?? 'style-1'),
|
||||
'class' => 'mb10',
|
||||
'img' => aksrt_widget_term_cover_url((int) $term->term_id),
|
||||
'alt' => $name . '-' . $description,
|
||||
'link' => ['url' => esc_url((string) $href), 'target' => $target_blank],
|
||||
'text1' => $name,
|
||||
'text2' => $description,
|
||||
'text3' => $icon . $count . __('篇文章', 'aksrt'),
|
||||
'height_scale' => (int) ($instance['height_scale'] ?? 60),
|
||||
'mask_opacity' => (int) ($instance['mask_opacity'] ?? 10),
|
||||
];
|
||||
$type = (string) $card['type'];
|
||||
if ($type === 'style-2' || $type === 'style-4') {
|
||||
$card['text3'] = '<item data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%d篇文章', 'aksrt'), $count)) . '">' . $icon . $count . '</item>';
|
||||
} elseif ($type === 'style-3') {
|
||||
$card['text1'] = $icon . $name;
|
||||
$card['text3'] = '<i class="fa mr6 fa-file-text-o" aria-hidden="true"></i>' . $count . __('篇文章', 'aksrt');
|
||||
}
|
||||
|
||||
$attr = '';
|
||||
$col_class = '';
|
||||
if ($animation !== '') {
|
||||
$col_class .= ' obs-animate ani-' . sanitize_html_class($animation);
|
||||
$attr .= ' data-animation="' . esc_attr($animation) . '"';
|
||||
if (!empty($instance['animation_repeat'])) {
|
||||
$attr .= ' data-animation-repeat="true"';
|
||||
}
|
||||
}
|
||||
if ($is_row) {
|
||||
$col_class .= ' ' . $row_class;
|
||||
}
|
||||
$html .= '<div class="' . esc_attr(trim($col_class)) . '"' . $attr . '>' . aksrt_graphic_card($card) . '</div>';
|
||||
}
|
||||
return '<div class="' . ($is_row ? 'mb10' : 'mb20') . '">' . ($is_row ? '<div class="row gutters-5">' : '') . $html . ($is_row ? '</div>' : '') . '</div>';
|
||||
}
|
||||
|
||||
private static function term_aggregation(array $instance): string {
|
||||
$term_ids = array_values(array_filter(array_map('absint', (array) ($instance['term_id'] ?? []))));
|
||||
if (!$term_ids) {
|
||||
return '';
|
||||
}
|
||||
$pc_row = max(1, min(2, (int) ($instance['pc_row'] ?? 2)));
|
||||
$row_class = 'col-sm-' . (int) (12 / $pc_row) . ' col-xs-12';
|
||||
$is_row = count($term_ids) > 1;
|
||||
$animation = (string) ($instance['obs_animation'] ?? '');
|
||||
$html = '';
|
||||
foreach ($term_ids as $key => $term_id) {
|
||||
$attr = '';
|
||||
$col_class = '';
|
||||
if ($animation !== '') {
|
||||
$col_class .= ' obs-animate ani-' . sanitize_html_class($animation);
|
||||
$attr .= ' data-animation="' . esc_attr($animation) . '"';
|
||||
$attr .= ' style=" --delay: ' . esc_attr((string) ($key * 0.15)) . 's; "';
|
||||
if (!empty($instance['animation_repeat'])) {
|
||||
$attr .= ' data-animation-repeat="true"';
|
||||
}
|
||||
}
|
||||
if ($is_row) {
|
||||
$col_class .= ' ' . $row_class;
|
||||
}
|
||||
$html .= '<div class="' . esc_attr(trim($col_class)) . '"' . $attr . '>';
|
||||
$html .= aksrt_widget_term_aggregation([
|
||||
'term_id' => (int) $term_id,
|
||||
'target_blank' => !empty($instance['target_blank']),
|
||||
'orderby' => (string) ($instance['orderby'] ?? 'modified'),
|
||||
'order' => (($instance['order'] ?? 'desc') === 'asc') ? 'ASC' : 'DESC',
|
||||
'count' => (int) ($instance['count'] ?? 4),
|
||||
]);
|
||||
$html .= '</div>';
|
||||
}
|
||||
return '<div class="clearfix mb6">' . ($is_row ? '<div class="row gutters-5">' : '') . $html . ($is_row ? '</div>' : '') . '</div>';
|
||||
}
|
||||
|
||||
private static function post_pay(array $instance): string {
|
||||
|
||||
Reference in New Issue
Block a user