Files
wordpress-theme-aksrt/inc/helpers/aksrt-widgets-markup.php
T
kate522 f5a7c7a833 fix(widgets): resolve term covers through the canonical category chain
The shared term cover helper read aksrt_cover_url/cover_url term meta,
keys this theme never writes, so 分类图文卡片 dropped every card and
分类聚合卡片 always rendered an empty cover box. Resolve covers the way
the rest of the theme does (aksrt_category_cover_id → latest thumbnailed
post in the term), and let the aggregation card fall back to the theme
placeholder while term cards stay hidden without a real cover.
2026-09-05 12:05:12 +08:00

363 lines
15 KiB
PHP

<?php
/**
* Widget-system markup helpers (reference parity).
*
* Ports of the reference theme widget renderers that stay procedural so the
* schema renderers stay small: hot posts ranking, category aggregation card,
* recent comments list and the ajax empty state.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
if (!function_exists('aksrt_widget_str_cut')) :
/**
* Character-based truncation (reference zib_str_cut parity).
*/
function aksrt_widget_str_cut(string $text, int $length): string {
$text = trim($text);
if ($length <= 0 || function_exists('mb_strlen') === false) {
return $text;
}
return mb_strlen($text) > $length ? mb_substr($text, 0, $length) . '...' : $text;
}
endif;
if (!function_exists('aksrt_widget_ajax_null')) :
/**
* Empty state for ajax-paged lists (hidden pager stub included).
*/
function aksrt_widget_ajax_null(string $text = ''): string {
if ($text === '') {
$text = __('暂无内容', 'aksrt');
}
$html = '<div class="ajax-item">' . aksrt_get_null($text, 60, 'null.svg', '', 280) . '</div>';
$html .= '<div class="ajax-pag hide"><div class="next-page ajax-next"><a href="#"></a></div></div>';
return $html;
}
endif;
if (!function_exists('aksrt_widget_term_cover_url')) :
/**
* Category cover image URL, resolved through the theme's canonical chain:
* explicit term cover → latest thumbnailed post inside the term → optional
* default placeholder. The reference hides term cards without a real cover
* while the aggregation card always falls back to a default image, so the
* last step is opt-in via $default_fallback.
*/
function aksrt_widget_term_cover_url(int $term_id, bool $default_fallback = false): string {
$cover_id = (int) get_term_meta($term_id, 'aksrt_category_cover_id', true);
if ($cover_id > 0) {
$url = wp_get_attachment_url($cover_id);
if ($url !== '') {
return (string) $url;
}
}
$posts = get_posts([
'category__in' => [$term_id],
'posts_per_page' => 1,
'post_status' => 'publish',
'orderby' => 'date',
'order' => 'DESC',
'meta_query' => [['key' => '_thumbnail_id', 'compare' => 'EXISTS']],
'fields' => 'ids',
]);
if (!empty($posts)) {
$url = get_the_post_thumbnail_url((int) $posts[0], 'medium_large');
if ($url) {
return (string) $url;
}
}
return $default_fallback ? get_template_directory_uri() . '/assets/img/placeholder.svg' : '';
}
endif;
if (!function_exists('aksrt_widget_term_views_sum')) :
/**
* Sum of published post views inside a term (badge on the aggregation card).
*/
function aksrt_widget_term_views_sum(int $term_id): int {
static $cache = [];
if (isset($cache[$term_id])) {
return $cache[$term_id];
}
$sum = 0;
$term = get_term($term_id);
if ($term instanceof WP_Term && class_exists('wpdb')) {
global $wpdb;
$view_key = class_exists('AKSRT\\Core\\DomainModel') ? \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY : 'aksrt_post_views';
$tt_id = (int) $term->term_taxonomy_id;
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- prepare() below.
$sum = (int) $wpdb->get_var($wpdb->prepare(
"SELECT SUM(CAST(pm.meta_value AS UNSIGNED)) FROM {$wpdb->postmeta} AS pm
INNER JOIN {$wpdb->posts} AS p ON p.ID = pm.post_id
INNER JOIN {$wpdb->term_relationships} AS tr ON tr.object_id = p.ID
WHERE pm.meta_key = %s AND tr.term_taxonomy_id = %d
AND p.post_status = 'publish' AND p.post_type = 'post'",
$view_key,
$tt_id
));
}
$cache[$term_id] = max(0, $sum);
return $cache[$term_id];
}
endif;
if (!function_exists('aksrt_widget_hot_posts')) :
/**
* Hot posts ranking box (reference parity, sidebar sized).
*
* @param array<string,mixed> $args {orderby, limit_day, count, target_blank}.
*/
function aksrt_widget_hot_posts(array $args = []): string {
$instance = wp_parse_args($args, [
'orderby' => 'views',
'limit_day' => 0,
'count' => 6,
'target_blank' => false,
]);
$orderby = (string) $instance['orderby'];
$target = !empty($instance['target_blank']) ? ' target="_blank" rel="noopener noreferrer"' : '';
$instance['paginate'] = '';
$query = \AKSRT\WidgetSystem\PostQuery::build($instance, 1, []);
if (!$query->have_posts()) {
return '';
}
$posts_html = '';
$i = 0;
while ($query->have_posts()) {
$query->the_post();
$i++;
$data = aksrt_renderer_collect_post_data(['time_source' => 'date']);
if (!$data) {
continue;
}
$title = esc_html((string) $data['title']) . ($data['subtitle'] !== '' ? '<span class="focus-color">' . esc_html((string) $data['subtitle']) . '</span>' : '');
$badge_classes = ['', 'jb-red', 'jb-yellow'];
$badge_class = ($i === 1 ? 'em12 ' : '') . ($badge_classes[$i - 1] ?? 'b-gray');
$top_badge = '<badge class="img-badge left hot ' . esc_attr($badge_class) . '"><i>TOP' . $i . '</i></badge>';
$meta_value = '';
if ($orderby === 'favorite') {
$meta_value = aksrt_format_number((int) aksrt_get_post_favorites((int) $data['post_id'])) . __('人收藏', 'aksrt');
} elseif ($orderby === 'like') {
$meta_value = aksrt_format_number((int) aksrt_get_post_likes((int) $data['post_id'])) . __('人点赞', 'aksrt');
} elseif ($orderby === 'comment_count') {
$meta_value = aksrt_format_number((int) $data['comments']) . __('条讨论', 'aksrt');
}
if ($meta_value === '') {
$meta_value = aksrt_format_number((int) $data['views']) . __('人已阅读', 'aksrt');
}
$thumb = '';
if ((string) $data['primary'] !== '') {
$thumb = '<img' . aksrt_get_lazy_attr('lazy_posts_thumb', (string) $data['primary'], 'fit-cover radius8') . ' alt="' . esc_attr((string) $data['title']) . '">';
}
$time_ago = '<i class="fa fa-clock-o mr3" aria-hidden="true"></i>' . esc_html((string) $data['time_ago']);
$permalink = esc_url((string) $data['permalink']);
if ($i === 1) {
$posts_html .= '<div class="relative">';
$posts_html .= '<a' . $target . ' href="' . $permalink . '">';
$posts_html .= '<div class="graphic hover-zoom-img" style="padding-bottom: 60%!important;">' . $thumb;
$posts_html .= '<div class="absolute linear-mask"></div>';
$posts_html .= '<div class="abs-center left-bottom box-body">';
$posts_html .= '<div class="mb6"><span class="badg b-theme badg-sm">' . esc_html($meta_value) . '</span></div>';
$posts_html .= $title;
$posts_html .= '</div></div></a>';
$posts_html .= $top_badge;
$posts_html .= '</div>';
} else {
$posts_html .= '<div class="flex mt15 relative hover-zoom-img">';
$posts_html .= '<a' . $target . ' href="' . $permalink . '"><div class="graphic">' . $thumb . '</div></a>';
$posts_html .= '<div class="term-title ml10 flex xx flex1 jsb">';
$posts_html .= '<div class="text-ellipsis-2"><a' . $target . ' href="' . $permalink . '">' . $title . '</a></div>';
$posts_html .= '<div class="px12 muted-3-color text-ellipsis flex jsb"><span>' . $time_ago . '</span><span>' . esc_html($meta_value) . '</span></div>';
$posts_html .= '</div>';
$posts_html .= $top_badge;
$posts_html .= '</div>';
}
}
wp_reset_postdata();
return '<div class="zib-widget hot-posts">' . $posts_html . '</div>';
}
endif;
if (!function_exists('aksrt_widget_term_aggregation')) :
/**
* Category aggregation card: cover + description + latest posts list.
*
* @param array<string,mixed> $args {term_id, target_blank, orderby, order, count}.
*/
function aksrt_widget_term_aggregation(array $args = []): string {
$args = wp_parse_args($args, [
'term_id' => 0,
'target_blank' => false,
'orderby' => 'modified',
'order' => 'DESC',
'count' => 4,
]);
$term_id = (int) $args['term_id'];
$term = $term_id > 0 ? get_term($term_id, 'category') : null;
if (!$term instanceof WP_Term || is_wp_error($term)) {
return '';
}
$target = !empty($args['target_blank']) ? ' target="_blank" rel="noopener noreferrer"' : '';
$href = get_term_link($term);
if (is_wp_error($href)) {
return '';
}
$href = esc_url((string) $href);
$icon = '<i class="fa fa-folder-open-o mr6" aria-hidden="true"></i>';
$img = aksrt_widget_term_cover_url((int) $term->term_id, true);
$img_html = aksrt_graphic_card([
'class' => '',
'img' => $img,
'alt' => __('分类', 'aksrt') . '-' . $term->name,
'link' => ['url' => $href, 'target' => !empty($args['target_blank']) ? '_blank' : ''],
'height_scale' => 0,
'mask_opacity' => 0,
'more' => '<badge class="img-badge px12">' . aksrt_renderer_svg_icon('hot') . ' ' . esc_html(aksrt_format_number(aksrt_widget_term_views_sum((int) $term->term_id))) . '</badge>',
]);
$img_html = '<div class="term-img flex0 em09-sm">' . $img_html . '</div>';
$name = '<a class="em14 key-color text-ellipsis"' . $target . ' href="' . $href . '">' . esc_html($term->name) . '</a>';
$description = '<div class="text-ellipsis-2 muted-color">' . esc_html($term->description) . '</div>';
$query_args = [
'cat' => (string) $term->term_id,
'orderby' => (string) $args['orderby'],
'order' => strtoupper((string) $args['order']) === 'ASC' ? 'ASC' : 'DESC',
'count' => max(1, (int) $args['count']),
'paginate' => '',
];
$query = \AKSRT\WidgetSystem\PostQuery::build($query_args, 1, []);
$meta = '<span class="px12 muted-3-color text-ellipsis"><i class="fa fa-file-text-o fa-fw" aria-hidden="true"></i>' . sprintf(esc_html__('%s篇文章', 'aksrt'), esc_html(aksrt_format_number(max(0, (int) $query->found_posts)))) . '</span>';
$meta .= '<a class="but p2-10 px12 c-blue hide-sm shrink0"' . $target . ' href="' . $href . '"><i class="fa fa-angle-right" aria-hidden="true"></i>' . esc_html__('更多文章', 'aksrt') . '</a>';
$meta = '<div class="term-meta flex jsb ac">' . $meta . '</div>';
$posts_html = '';
while ($query->have_posts()) {
$query->the_post();
$data = aksrt_renderer_collect_post_data(['time_source' => 'date']);
if (!$data) {
continue;
}
$title = esc_html((string) $data['title']) . ($data['subtitle'] !== '' ? '<span class="focus-color">' . esc_html((string) $data['subtitle']) . '</span>' : '');
$title = '<div class="text-ellipsis"><a class="icon-circle mln3"' . $target . ' href="' . esc_url((string) $data['permalink']) . '">' . $title . '</a></div>';
$post_meta = '';
switch ((string) $args['orderby']) {
case 'views':
$post_meta = aksrt_format_number((int) $data['views']);
break;
case 'favorite':
$post_meta = aksrt_format_number((int) aksrt_get_post_favorites((int) $data['post_id']));
break;
case 'like':
$post_meta = aksrt_format_number((int) aksrt_get_post_likes((int) $data['post_id']));
break;
case 'comment_count':
$post_meta = aksrt_format_number((int) $data['comments']);
break;
}
if ($post_meta === '') {
$post_meta = '<i class="fa fa-clock-o mr3" aria-hidden="true"></i>' . esc_html((string) $data['time_ago']);
}
$posts_html .= '<div class="mt10 flex jsb ac">' . $title . '<div class="em09 muted-3-color flex0 ml10">' . $post_meta . '</div></div>';
}
wp_reset_postdata();
$html = '<div class="zib-widget term-aggregation mb10">';
$html .= '<div class="mb20 hover-zoom-img flex px12-sm px12-m-s">';
$html .= $img_html;
$html .= '<div class="term-title ml10 flex xx flex1 jsb">' . $name . $description . $meta . '</div>';
$html .= '</div>';
$html .= $posts_html;
$html .= '</div>';
return $html;
}
endif;
if (!function_exists('aksrt_widget_comments_list')) :
/**
* Recent comments widget list (reference parity).
*
* @param array<string,mixed> $args {limit, outer, outpost, style, show_source}.
*/
function aksrt_widget_comments_list(array $args = []): string {
$args = wp_parse_args($args, [
'limit' => 8,
'outer' => '',
'outpost' => '',
'style' => '',
'show_source' => false,
]);
$comments = get_comments([
'orderby' => 'comment_date',
'number' => max(1, (int) $args['limit']),
'status' => 'approve',
'post_type' => ['post'],
'author__not_in' => \AKSRT\WidgetSystem\RendererHelpers::ids((string) $args['outer']),
'post__not_in' => \AKSRT\WidgetSystem\RendererHelpers::ids((string) $args['outpost']),
]);
if (!$comments) {
return '';
}
$output = '';
foreach ($comments as $comment) {
$content = aksrt_widget_comment_excerpt((string) $comment->comment_content);
$link = esc_url((string) get_comment_link($comment));
$time = aksrt_time_ago((string) $comment->comment_date);
$user_name = function_exists('aksrt_get_comments_user_name_html')
? aksrt_get_comments_user_name_html($comment)
: esc_html(get_comment_author($comment));
$like = aksrt_get_comment_like_html($comment);
$avatar = aksrt_get_comment_avatar_box($comment, 'avatar-img');
$class = 'comment-widget-item' . ((string) $args['style'] === 'style_2' ? ' style-2' : '');
$source = '';
if (!empty($args['show_source'])) {
$post_title = esc_html(get_the_title((int) $comment->comment_post_ID));
$source = '<div class="muted-color mt6 em09 text-ellipsis"><a href="' . esc_url((string) get_permalink((int) $comment->comment_post_ID)) . '" class="muted-2-color"><span class="muted-3-color">' . esc_html__('评论于:', 'aksrt') . '</span>' . $post_title . '</a></div>';
}
$output .= '<div class="' . esc_attr($class) . '">';
$output .= '<div class="flex gap6">' . $avatar;
$output .= '<div class="posts-mini-con em09 ml10 flex xx jsb">';
$output .= '<p class="flex jsb"><span class="flex ellipsis-box">' . $user_name . '<span class="flex0 icon-spot muted-3-color" title="' . esc_attr((string) $comment->comment_date) . '">' . esc_html($time) . '</span></span><span class="ml10 flex0">' . $like . '</span></p>';
$output .= (string) $args['style'] === 'style_2' ? '' : '<a class="con-link muted-color text-ellipsis-5" href="' . $link . '">' . $content . '</a>';
$output .= '</div></div>';
$output .= (string) $args['style'] === 'style_2' ? '<a class="em09 con-link relative" href="' . $link . '"><div class="text-ellipsis-5 muted-box padding-10">' . $content . '</div></a>' : '';
$output .= $source;
$output .= '</div>';
}
return $output;
}
endif;
if (!function_exists('aksrt_widget_comment_excerpt')) :
/**
* Comment text reduced to a widget-safe plain snippet (images/code dropped).
*/
function aksrt_widget_comment_excerpt(string $content): string {
$content = convert_smilies($content);
$content = preg_replace('/\[img=(.*?)\]/', '[图片]', $content) ?? $content;
$content = preg_replace('/\[link=(.*?) target=_blank\](.*?)\[\/link\]/', '$2', $content) ?? $content;
$content = preg_replace('/\[link=(.*?)\](.*?)\[\/link\]/', '$2', $content) ?? $content;
$content = preg_replace('/\[code]([\s\S]*)\[\/code]/', '[代码]', $content) ?? $content;
$content = preg_replace('/\[g=(.*?)\]/', '[$1]', $content) ?? $content;
// Strip any remaining anchors but keep their text.
$content = preg_replace('/<a[^>]*>(.*?)<\/a>/', '$1', $content) ?? $content;
return wp_kses_post($content);
}
endif;