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:
kate522 committed 2026-09-05 11:28:00 +08:00
1 parent 9dc278bd83
commit b639d57c5b
25 files changed
+1388 -446

No files matched your search

+158 -96
View File
@@ -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 {
+293 -31
View File
@@ -25,7 +25,12 @@ final class GeneralRenderer {
if (!is_singular('post')) {
return '';
}
return '<nav class="aksrt-widget-toc' . (!empty($i['collapsed']) ? ' is-collapsed' : '') . '" data-min-headings="' . (int) $i['min_headings'] . '" data-scroll-offset="' . (int) $i['offset'] . '" aria-label="' . esc_attr__('文章目录', 'aksrt') . '"><button class="aksrt-widget-toc__toggle" type="button" aria-expanded="' . (empty($i['collapsed']) ? 'true' : 'false') . '"><span>' . esc_html__('文章目录', 'aksrt') . '</span><i class="fa fas fa-chevron-down" aria-hidden="true"></i></button><ol class="aksrt-widget-toc__list"></ol><p class="aksrt-widget-toc__empty">' . esc_html__('正在生成目录…', 'aksrt') . '</p></nav>';
$title = esc_html((string) ($i['title'] ?? '文章目录'));
if (!empty($i['mini_title'])) {
$title .= '<small class="ml10">' . esc_html((string) $i['mini_title']) . '</small>';
}
$affix = !empty($i['in_affix']) ? ' data-affix="true"' : '';
return '<div class="widget-container"><div' . $affix . ' class="posts-nav-box" data-title="' . esc_attr(wp_kses_post($title)) . '"></div></div>';
}
private static function render_quote(array $i): string {
@@ -112,44 +117,305 @@ final class GeneralRenderer {
}
private static function render_media_cards(array $i): string {
return self::media_grid((array) $i['cards']);
}
private static function media_grid(array $items, string $class = ''): string {
$html = '<div class="aksrt-widget-grid aksrt-widget-media-cards ' . esc_attr($class) . '">';
foreach ($items as $item) {
if (is_array($item)) {
$html .= RendererHelpers::card_media($item);
$pc_row = max(1, (int) ($i['pc_row'] ?? 4));
$m_row = max(1, (int) ($i['m_row'] ?? 2));
$row_class = 'col-sm-' . (int) (12 / $pc_row);
$row_class .= $m_row > 1 ? ' col-xs-' . (int) (12 / $m_row) : '';
$covers = (array) ($i['covers'] ?? []);
$is_row = count($covers) > 1;
$is_mobile = function_exists('wp_is_mobile') && wp_is_mobile();
$style = '';
$font_pc = (int) ($i['font_size_pc'] ?? 18);
$font_m = (int) ($i['font_size_m'] ?? 14);
if ($font_pc > 0 && $font_pc !== 14) $style .= '--font-size:' . $font_pc . 'px;';
if ($font_m > 0 && $font_m !== 14) $style .= '--font-size-sm:' . $font_m . 'px;';
if (!empty($i['font_bold'])) $style .= '--font-weight:bold;--font-weight-sm:bold;';
$font_color = (string) ($i['font_color'] ?? '');
if ($font_color !== '') $style .= '--color:' . esc_attr($font_color) . ';--color-sm:' . esc_attr($font_color) . ';';
$animation = (string) ($i['obs_animation'] ?? '');
$html = '';
foreach ($covers as $cover) {
if (!is_array($cover)) {
continue;
}
$hide = (string) ($cover['hide'] ?? '');
if (($is_mobile && $hide === 'm') || (!$is_mobile && $hide === 'pc')) {
continue;
}
$title = (string) ($cover['title'] ?? '');
$link = RendererHelpers::link((array) ($cover['link'] ?? []));
$card = aksrt_graphic_card([
'class' => 'noshadow mb10',
'img' => (string) ($cover['image'] ?? ''),
'video' => (string) ($cover['video'] ?? ''),
'alt' => wp_strip_all_tags($title),
'link' => ['url' => $link['url'], 'target' => $link['target']],
'more' => trim($title) !== '' ? '<div class="abs-center text-center graphic-text this-font">' . wp_kses_post($title) . '</div>' : '',
'height_scale' => (int) ($i['height_scale'] ?? 30),
'mask_opacity' => (int) ($i['mask_opacity'] ?? 10),
]);
if ($card === '') {
continue;
}
$attr = '';
$col_class = '';
if ($animation !== '') {
$col_class .= ' obs-animate ani-' . sanitize_html_class($animation);
$attr .= ' data-animation="' . esc_attr($animation) . '"';
if (!empty($i['animation_repeat'])) {
$attr .= ' data-animation-repeat="true"';
}
}
if ($is_row) {
$col_class .= ' ' . $row_class;
}
$html .= '<div class="' . esc_attr(trim($col_class)) . '"' . $attr . '>' . $card . '</div>';
}
return $html . '</div>';
if ($html === '') {
return '';
}
return '<div class="widget-graphic-cover ' . ($is_row ? 'mb10' : 'mb20') . '"><div' . ($style !== '' ? ' style="' . esc_attr($style) . '"' : '') . '>' . ($is_row ? '<div class="row gutters-5">' : '') . $html . ($is_row ? '</div>' : '') . '</div></div>';
}
private static function render_partners(array $i): string {
$items = '';
foreach ((array) $i['items'] as $item) {
if (is_array($item)) {
$items .= RendererHelpers::card_media($item, 'aksrt-partner');
$defaults = [
'rows' => 1, 'm_rows' => 1, 'style_type' => 'card', 'card_width' => 220, 'card_height' => 90,
'speed' => 3, 'direction' => 'alternate', 'pause_hover' => true, 'show_widget_bg' => true,
'card_bg' => true, 'card_radius' => 10, 'partners' => [], 'link_type' => 'link',
'link_orderby' => 'name', 'link_order' => 'ASC', 'link_cats' => [], 'link_limit' => 50,
'go_link' => true, 'end_fade' => true,
];
$i = array_merge($defaults, $i);
$is_mobile = function_exists('wp_is_mobile') && wp_is_mobile();
$rows = max(1, (int) ($is_mobile ? $i['m_rows'] : $i['rows']));
if ((string) $i['link_type'] === 'link') {
$args = [
'orderby' => (string) ($i['link_orderby'] ?: 'name'),
'order' => (string) ($i['page_links_order'] ?: 'ASC'),
'limit' => (int) $i['link_limit'] > 0 ? (int) $i['link_limit'] : -1,
];
$cats = array_values(array_filter(array_map('absint', (array) $i['link_cats'])));
if ($cats) {
$args['category'] = $cats;
}
$partners = [];
foreach ((array) get_bookmarks($args) as $bookmark) {
$partners[] = [
'title' => (string) $bookmark->link_name,
'desc' => (string) $bookmark->link_description,
'image' => (string) $bookmark->link_image,
'link' => ['url' => (string) $bookmark->link_url, 'target' => (string) $bookmark->link_target],
];
}
$i['partners'] = $partners;
}
return '<div class="aksrt-widget-partners" style="--aksrt-partner-rows:' . (int) $i['rows_pc'] . ';--aksrt-partner-rows-m:' . (int) $i['rows_mobile'] . ';--aksrt-partner-speed:' . (int) $i['speed'] . 's"><div class="aksrt-widget-partners__track">' . $items . $items . '</div></div>';
$partners = array_values(array_filter((array) $i['partners'], 'is_array'));
if (!$partners) {
return function_exists('is_super_admin') && is_super_admin()
? '<div class="c-red muted-box mb20"><b>' . esc_html__('[合作伙伴]横向滚动合作伙伴模块:', 'aksrt') . '</b>' . esc_html__('当前配置下没有可显示的内容,请正确配置或移出该模块', 'aksrt') . '</div>'
: '';
}
$rows_data = array_fill(0, $rows, []);
foreach ($partners as $index => $partner) {
$rows_data[$index % $rows][] = $partner;
}
$rows_data = array_values(array_filter($rows_data, static fn(array $row): bool => $row !== []));
$style_vars = [
'--zps-card-w:' . (int) $i['card_width'] . 'px',
'--zps-card-h:' . (int) $i['card_height'] . 'px',
'--zps-card-radius:' . (int) $i['card_radius'] . 'px',
'--zps-speed:' . max(0.1, (float) $i['speed']),
];
$style_attr = ' style="' . esc_attr(implode(';', $style_vars)) . '"';
$wrap_class = 'zib-partners-scroll';
$wrap_class .= ' zps-style-' . sanitize_html_class((string) $i['style_type']);
$wrap_class .= !empty($i['pause_hover']) ? ' zps-pause-hover' : '';
$wrap_class .= !empty($i['card_bg']) ? ' zps-card-bg' : '';
$wrap_class .= !empty($i['end_fade']) ? ' zps-end-fade' : '';
$html = '';
foreach ($rows_data as $row_index => $row_partners) {
$direction = (string) $i['direction'];
if ($direction === 'left' || $direction === 'right') {
$dir = $direction;
} else {
$dir = $row_index % 2 === 0 ? 'left' : 'right';
}
$html .= '<div class="zps-row zps-dir-' . $dir . '" data-zps-row><div class="zps-track" data-zps-track><div class="zps-group flex shrink0">';
foreach ($row_partners as $partner) {
$html .= self::partners_card($partner, (string) $i['style_type'], !empty($i['go_link']));
}
$html .= '</div></div></div>';
}
if (trim($html) === '') {
return '';
}
return '<div class="' . (!empty($i['show_widget_bg']) ? 'zib-widget' : 'mb20') . '"><div class="' . esc_attr($wrap_class) . '"' . $style_attr . '>' . $html . '</div></div>';
}
private static function partners_card(array $partner, string $style_type, bool $go_link): string {
$partner = wp_parse_args($partner, ['image' => '', 'link' => [], 'title' => '', 'desc' => '']);
$title = trim((string) $partner['title']);
$desc = trim((string) $partner['desc']);
$image = esc_url((string) $partner['image']);
if ($image === '' && $title === '') {
return '';
}
$img_tag = $image !== '' ? '<img class="zps-img" src="' . $image . '" alt="' . esc_attr($title !== '' ? $title : __('合作伙伴', 'aksrt')) . '">' : '';
$inner = '';
if ($style_type === 'logo') {
if ($image === '') {
return '';
}
$inner .= $img_tag;
} elseif ($style_type === 'chip') {
if ($img_tag !== '') {
$inner .= '<span class="zps-chip-icon flex jc shrink0 mr6">' . $img_tag . '</span>';
}
$inner .= '<span class="zps-chip-title text-ellipsis">' . esc_html($title) . '</span>';
} else {
if ($img_tag !== '') {
$inner .= '<div class="zps-card-icon flex jc shrink0 mr10">' . $img_tag . '</div>';
}
$inner .= '<div class="zps-card-text flex xx flex1">';
if ($title !== '') {
$inner .= '<div class="zps-card-title text-ellipsis font-bold">' . esc_html($title) . '</div>';
}
if ($desc !== '') {
$inner .= '<div class="zps-card-desc text-ellipsis muted-color em09 mt3">' . esc_html($desc) . '</div>';
}
$inner .= '</div>';
}
$link = RendererHelpers::link((array) ($partner['link'] ?? []));
$url = $link['url'] !== '' ? $link['url'] : 'javascript:void(0)';
if ($go_link && $url !== 'javascript:void(0)' && function_exists('aksrt_go_link')) {
$url = esc_url(aksrt_go_link($link['url'], true));
}
$target = $link['target'] === '_blank' ? ' target="_blank" rel="noopener noreferrer"' : '';
$base_class = 'zps-item flex ac shrink0';
if ($url !== 'javascript:void(0)') {
return '<a class="' . $base_class . '" href="' . $url . '"' . $target . '>' . $inner . '</a>';
}
return '<div class="' . $base_class . '">' . $inner . '</div>';
}
private static function render_combo_info(array $i): string {
$html = '<div class="aksrt-widget-grid aksrt-widget-combo">';
foreach ((array) $i['items'] as $item) {
if (!is_array($item)) {
$i = array_merge([
'pc_row' => 4, 'm_row' => 1, 'card_radius' => 12, 'top_band' => true, 'top_band_height' => 70,
'show_widget_bg' => false, 'cta_text' => '', 'cards' => [], 'obs_animation' => '', 'animation_repeat' => false,
'pc_gap' => 10, 'm_gap' => 5, 'go_link' => true, 'cta_color' => '',
], $i);
$is_mobile = function_exists('wp_is_mobile') && wp_is_mobile();
$pc_row = max(1, (int) $i['pc_row']);
$m_row = max(1, (int) $i['m_row']);
$col_class = 'col-sm-' . max(1, (int) (12 / $pc_row)) . ' col-xs-' . max(1, (int) (12 / $m_row));
$style_vars = '--zci-radius:' . (int) $i['card_radius'] . 'px;--zci-band-h:' . (int) $i['top_band_height'] . 'px;';
$wrap_class = 'zib-combo-info-card row gutters-' . (int) ($is_mobile ? $i['m_gap'] : $i['pc_gap']);
$wrap_class .= !empty($i['top_band']) ? ' zci-has-band' : '';
$default_cta = (string) ($i['cta_text'] !== '' ? $i['cta_text'] : __('立即体验', 'aksrt'));
$default_cta_color = (string) ($i['cta_color'] ?? '');
$auto_palette = ['blue', 'cyan', 'green', 'yellow', 'pink', 'purple'];
$auto_idx = 0;
$animation = (string) ($i['obs_animation'] ?? '');
$html = '';
foreach ((array) $i['cards'] as $key => $card) {
if (!is_array($card)) {
continue;
}
$tags = '';
foreach (preg_split('/\s*[,,]\s*/', (string) ($item['tags'] ?? '')) ?: [] as $tag) {
if ($tag !== '') {
$tags .= '<span>' . esc_html($tag) . '</span>';
$card_html = self::combo_card_item($card, $default_cta, $default_cta_color, $auto_palette, $auto_idx, !empty($i['go_link']));
if ($card_html === '') {
continue;
}
$attr = '';
$item_class = $col_class;
if ($animation !== '') {
$item_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($i['animation_repeat'])) {
$attr .= ' data-animation-repeat="true"';
}
}
$html .= '<article class="aksrt-combo-card">' . RendererHelpers::card_media($item) . ($tags ? '<div class="aksrt-combo-card__tags">' . $tags . '</div>' : '') . '</article>';
$html .= '<div class="' . esc_attr($item_class) . '"' . $attr . '>' . $card_html . '</div>';
}
return $html . '</div>';
if ($html === '') {
return '';
}
return '<div class="mb20"><div class="' . esc_attr($wrap_class) . '" style="' . esc_attr($style_vars) . '">' . $html . '</div></div>';
}
private static function combo_card_item(array $card, string $default_cta, string $default_cta_color, array $auto_palette, int &$auto_idx, bool $go_link): string {
$card = wp_parse_args($card, [
'icon_image' => '', 'icon' => '', 'title' => '', 'tags' => [], 'desc' => '',
'link' => [], 'cta_text' => '', 'cta_color' => '', 'band_color' => 'auto',
]);
$title = trim(wp_strip_all_tags((string) $card['title']));
$desc = trim((string) $card['desc']);
if ($title === '' && $desc === '') {
return '';
}
$band = (string) $card['band_color'];
if ($band === '' || $band === 'auto') {
$band = $auto_palette[$auto_idx % count($auto_palette)];
$auto_idx++;
}
$icon_html = '';
$icon_image = esc_url((string) $card['icon_image']);
if ($icon_image !== '') {
$icon_html = '<div class="zci-icon flex jc shrink0 mb20"><img src="' . $icon_image . '" alt="' . esc_attr($title !== '' ? $title : __('图标', 'aksrt')) . '"></div>';
} elseif (!empty($card['icon'])) {
$icon_html = '<div class="zci-icon flex jc shrink0 mb20">' . self::icon_markup((string) $card['icon']) . '</div>';
}
$tags_html = '';
$tags = (array) ($card['tags'] ?? []);
if ($tags) {
$tags_html .= '<div class="zci-tags mb10 text-ellipsis">';
foreach ($tags as $tag) {
if (!is_array($tag)) {
continue;
}
$tag_text = trim((string) ($tag['text'] ?? ''));
if ($tag_text === '') {
continue;
}
$tags_html .= '<span class="badg p2-10 radius px12 ' . esc_attr(sanitize_html_class((string) ($tag['color'] ?? ''))) . '">' . esc_html($tag_text) . '</span>';
}
$tags_html .= '</div>';
}
$desc_html = $desc !== '' ? '<div class="zci-desc text-ellipsis-3 muted-color">' . wp_kses_post($desc) . '</div>' : '';
$cta_text = trim((string) ($card['cta_text'] ?? '')) !== '' ? trim((string) $card['cta_text']) : $default_cta;
$cta_color = trim((string) ($card['cta_color'] ?? '')) !== '' ? trim((string) $card['cta_color']) : $default_cta_color;
$link = RendererHelpers::link((array) ($card['link'] ?? []));
$url = $link['url'];
if ($url !== '' && $go_link && function_exists('aksrt_go_link')) {
$url = esc_url(aksrt_go_link($link['url'], true));
}
$target = $link['target'] === '_blank' ? ' target="_blank" rel="noopener noreferrer"' : '';
$cta_html = $cta_text !== '' && $url !== ''
? '<a class="zci-cta inflex ac gap6 ' . esc_attr(sanitize_html_class($cta_color)) . '" href="' . $url . '"' . $target . '><span>' . esc_html($cta_text) . '</span><i class="fa fa-arrow-right zci-arrow" aria-hidden="true"></i></a>'
: '';
$inner = '<div class="zci-body flex xx flex1">' . $icon_html;
if ($title !== '') {
$inner .= '<div class="zci-title font-bold text-ellipsis mb10">' . esc_html($title) . '</div>';
}
$inner .= $tags_html . $desc_html . $cta_html . '</div>';
return '<div class="zci-card flex xx zci-band-' . esc_attr(sanitize_html_class($band)) . '">' . $inner . '</div>';
}
private static function render_text_title(array $i): string {
@@ -216,15 +482,11 @@ final class GeneralRenderer {
}
private static function render_recent_comments(array $i): string {
$comments = get_comments(['status' => 'approve', 'number' => (int) $i['count'], 'user__not_in' => RendererHelpers::ids((string) $i['exclude_users']), 'post__not_in' => RendererHelpers::ids((string) $i['exclude_posts'])]);
if (!$comments) {
$list = function_exists('aksrt_widget_comments_list') ? aksrt_widget_comments_list($i) : '';
if ($list === '') {
return RendererHelpers::empty_state(__('暂无评论', 'aksrt'));
}
$html = '<ul class="aksrt-widget-comments">';
foreach ($comments as $comment) {
$html .= '<li><a class="aksrt-widget-comments__avatar" href="' . esc_url(get_comment_link($comment)) . '">' . (function_exists('aksrt_get_avatar_html') ? aksrt_get_avatar_html($comment, 48) : get_avatar($comment, 48)) . '</a><div><strong>' . esc_html(get_comment_author($comment)) . '</strong><p>' . esc_html(wp_trim_words(wp_strip_all_tags((string) $comment->comment_content), 18)) . '</p>' . (!empty($i['show_source']) ? '<a class="aksrt-widget-comments__source" href="' . esc_url(get_comment_link($comment)) . '">' . esc_html(get_the_title((int) $comment->comment_post_ID)) . '</a>' : '') . '</div></li>';
}
return $html . '</ul>';
return '<div class="box-body comment-mini-lists zib-widget">' . $list . '</div>';
}
private static function render_links(array $i): string {
+33 -18
View File
@@ -51,44 +51,59 @@ final class SliderRenderer {
$option = (array) ($instance['option'] ?? []); $option['direction'] = 'horizontal'; $option['scale_height'] = true; $option['scale'] = (int) ($instance['slide_scale'] ?? 50);
$slide_list = (array) ($instance['slides'] ?? []);
$slider = !empty($slide_list[0]) ? self::slider($slide_list, $option) : '';
$covers = '';
$is_mobile = function_exists('wp_is_mobile') && wp_is_mobile();
$cover_list = array_values((array) ($instance['covers'] ?? []));
$is_row = count($cover_list) > 1;
$pc_row = max(1, min(3, (int) ($instance['pc_row'] ?? 2)));
$m_row = max(1, min(3, (int) ($instance['m_row'] ?? 2)));
$layout_type = (string) ($instance['m_layout_type'] ?? 'vertical');
$pc_row = max(1, min(4, (int) ($instance['pc_row'] ?? 2)));
$m_row = $layout_type === 'default' ? $pc_row : max(1, min(6, (int) ($instance['m_row'] ?? 2)));
$row_class = 'col-sm-' . (int) (12 / $pc_row) . ($m_row > 1 ? ' col-xs-' . (int) (12 / $m_row) : '');
$m_cover_ratio = max(0, (int) ($instance['m_cover_ratio'] ?? 45));
$mask = max(0, min(90, (int) ($instance['mask_opacity'] ?? 0))) / 100;
$cover_style_vars = '--font-size:' . (int) ($instance['font_size_pc'] ?? 16) . 'px;--font-size-sm:' . (int) ($instance['font_size_m'] ?? 14) . 'px;';
if (!empty($instance['font_bold'])) $cover_style_vars .= '--font-weight:bold;--font-weight-sm:bold;';
$font_color = (string) ($instance['font_color'] ?? '');
if ($font_color !== '') $cover_style_vars .= '--color:' . $font_color . ';--color-sm:' . $font_color . ';';
$covers = '';
foreach ($cover_list as $cover) {
if (!is_array($cover)) continue;
$hide = (string) ($cover['hide'] ?? '');
if (($is_mobile && $hide === 'm') || (!$is_mobile && $hide === 'pc')) continue;
$image = esc_url((string) ($cover['image'] ?? ''));
$video = esc_url((string) ($cover['video'] ?? ''));
if ($image === '' && ($video === '' || $is_mobile)) continue;
$media = $video !== '' && !$is_mobile ? '<video autoplay loop muted playsinline class="fit-cover" src="' . $video . '"' . ($image !== '' ? ' poster="' . $image . '"' : '') . '></video>' : '<img class="fit-cover" src="' . $image . '" alt="' . esc_attr(wp_strip_all_tags((string) ($cover['title'] ?? ''))) . '">';
$mask = max(0, min(90, (int) ($instance['mask_opacity'] ?? 0))) / 100;
$card = $media . ($mask ? '<i class="graphic-mask" style="opacity:' . esc_attr((string) $mask) . '"></i>' : '') . (!empty($cover['title']) ? '<div class="abs-center text-center graphic-text this-font">' . wp_kses_post((string) $cover['title']) . '</div>' : '');
$title = (string) ($cover['title'] ?? '');
$link = RendererHelpers::link((array) ($cover['link'] ?? []));
$card = $link['url'] !== '' ? '<a href="' . $link['url'] . '"' . RendererHelpers::link_attrs($link) . '>' . $card . '</a>' : '<span>' . $card . '</span>';
$covers .= '<div class="' . ($is_row ? esc_attr($row_class) : '') . '"><div class="graphic-cover-item noshadow mb10">' . $card . '</div></div>';
$card = aksrt_graphic_card([
'class' => 'noshadow mb10',
'img' => $image,
'video' => $video,
'alt' => wp_strip_all_tags($title),
'link' => ['url' => $link['url'], 'target' => $link['target']],
'more' => trim($title) !== '' ? '<div class="abs-center text-center graphic-text this-font">' . wp_kses_post($title) . '</div>' : '',
'height_scale' => 0,
'mask_opacity' => (int) ($instance['mask_opacity'] ?? 0),
]);
if ($card === '') continue;
$covers .= '<div class="' . ($is_row ? esc_attr($row_class) : '') . '">' . $card . '</div>';
}
if ($covers !== '') {
$cover_style = '--font-size:' . (int) ($instance['font_size_pc'] ?? 16) . 'px;--font-size-sm:' . (int) ($instance['font_size_m'] ?? 14) . 'px;';
if (!empty($instance['font_bold'])) $cover_style .= '--font-weight:bold;--font-weight-sm:bold;';
if (!empty($instance['font_color'])) $cover_style .= '--color:' . $instance['font_color'] . ';--color-sm:' . $instance['font_color'] . ';';
$covers = '<div class="widget-graphic-cover" style="' . esc_attr($cover_style) . '">' . ($is_row ? '<div class="row gutters-5">' : '') . $covers . ($is_row ? '</div>' : '') . '</div>';
$covers = '<div class="widget-graphic-cover" style="' . esc_attr($cover_style_vars) . '">' . ($is_row ? '<div class="row gutters-5">' : '') . $covers . ($is_row ? '</div>' : '') . '</div>';
}
if ($slider === '' && $covers === '') return RendererHelpers::empty_state(__('请配置幻灯片或卡片', 'aksrt'));
$layout = max(0, min(100, (int) ($instance['layout_ratio'] ?? 60)));
$slide_scale = max(0, (int) ($instance['slide_scale'] ?? 50));
$rows = max(1, (int) ($instance['pc_row'] ?? 2));
$rows = $pc_row;
$columns = max(1, (int) ($instance['pc_col'] ?? 2));
$base = $layout < 100 ? (($layout * $slide_scale) / (100 - $layout)) : 0;
$cover_ratio = (($base - (($columns - 1) * 3)) / $columns) * $rows;
$style = '--layout-ratio:' . $layout . '%;--cover-ratio:' . $cover_ratio . '%;--m-cover-ratio:' . (int) ($instance['m_cover_ratio'] ?? 45) . '%;--cover-cols:' . $rows . ';';
$class = 'flex widget-slider-graphic m-' . sanitize_html_class((string) ($instance['m_layout_type'] ?? 'vertical'));
$cover_ratio = $columns > 0 ? ((($base - (($columns - 1) * 3)) / $columns) * $rows) : 0;
$style = '--layout-ratio:' . $layout . '%;--cover-ratio:' . $cover_ratio . '%;--m-cover-ratio:' . $m_cover_ratio . '%;';
$animation = '';
if (!empty($instance['obs_animation'])) {
$animation = ' obs-animate ani-' . sanitize_html_class((string) $instance['obs_animation']) . (!empty($instance['animation_repeat']) ? ' obs-animate-repeat' : '');
}
$class = 'flex widget-slider-graphic m-' . sanitize_html_class($layout_type);
$html = '<div class="' . esc_attr($class) . '" style="' . esc_attr($style) . '" data-row="' . $rows . '" data-col="' . $columns . '">';
if ($slider !== '') $html .= '<div class="slider-col mb20">' . $slider . '</div>';
if ($slider !== '') $html .= '<div class="slider-col mb20' . esc_attr($animation) . '">' . $slider . '</div>';
if ($covers !== '') $html .= '<div class="cover-col mb10">' . $covers . '</div>';
return $html . '</div>';
}