Files
wordpress-theme-aksrt/inc/helpers/aksrt-widgets-markup.php
T
kate522 d599bbccd9 fix(icons): swap FA4-only class names for FA5 equivalents
fa-clock-o/fa-folder-open-o did not exist in the bundled FA5 fonts and
rendered nothing; fa-file-text-o maps to fa-file-alt in FA5 Free (there
is no fa-file-text glyph either).
2026-09-06 12:59:31 +08:00

365 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 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 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']),
// The reference shows the term's total post count, so found rows
// must stay enabled (paginate '' would disable them).
'paginate' => 'number',
];
$query = \AKSRT\WidgetSystem\PostQuery::build($query_args, 1, []);
$meta = '<span class="px12 muted-3-color text-ellipsis"><i class="fa fa-file-alt 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 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;