diff --git a/inc/bootstrap.php b/inc/bootstrap.php
index e8ec4665..2828ee90 100644
--- a/inc/bootstrap.php
+++ b/inc/bootstrap.php
@@ -32,6 +32,7 @@ require_once AKSRT_DIR . '/inc/helpers/aksrt-archive.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-archive-filters.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-comments.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-forum.php';
+require_once AKSRT_DIR . '/inc/helpers/aksrt-forum-widgets.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-slider.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-vip.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-auth-ui.php';
diff --git a/inc/helpers/aksrt-forum-widgets.php b/inc/helpers/aksrt-forum-widgets.php
new file mode 100644
index 00000000..49ff9e22
--- /dev/null
+++ b/inc/helpers/aksrt-forum-widgets.php
@@ -0,0 +1,363 @@
+ (string) get_post_meta($post_id, '_aksrt_topping', true) === '1'
+ || (string) get_post_meta($post_id, '_aksrt_is_top', true) === '1',
+ 'essence' => (string) get_post_meta($post_id, '_aksrt_essence', true) === '1',
+ 'views' => (int) get_post_meta($post_id, '_aksrt_views', true),
+ 'replies' => (int) get_comments_number($post_id),
+ 'plate_id' => (int) get_post_meta($post_id, '_aksrt_plate_id', true),
+ ];
+ }
+endif;
+
+if (!function_exists('aksrt_forum_topic_badges_html')) :
+ /**
+ * Topping / essence badges for forum topic rows.
+ */
+ function aksrt_forum_topic_badges_html(int $post_id, bool $show_topping = true): string {
+ $flags = aksrt_forum_topic_meta_flags($post_id);
+ $html = '';
+ if ($show_topping && !empty($flags['topping'])) {
+ $html .= '' . esc_html__('置顶', 'aksrt') . '';
+ }
+ if (!empty($flags['essence'])) {
+ $html .= '' . esc_html__('精华', 'aksrt') . '';
+ }
+ return $html;
+ }
+endif;
+
+if (!function_exists('aksrt_render_forum_topic_mini')) :
+ /**
+ * Mini forum topic row (zibll zib_bbs_get_posts_mini_list parity).
+ *
+ * @param int|null $post_id Null = current post in loop.
+ * @param string $class Extra classes on outer element.
+ * @param bool $show_topping Whether to show topping badge.
+ */
+ function aksrt_render_forum_topic_mini(?int $post_id = null, string $class = 'ajax-item', bool $show_topping = true): string {
+ $post_id = $post_id ?: (int) get_the_ID();
+ if ($post_id <= 0) {
+ return '';
+ }
+ $post = get_post($post_id);
+ if (!$post || $post->post_status !== 'publish') {
+ return '';
+ }
+
+ $author_id = (int) $post->post_author;
+ $flags = aksrt_forum_topic_meta_flags($post_id);
+ $permalink = get_permalink($post_id);
+ $title = get_the_title($post_id);
+
+ $avatar_url = function_exists('aksrt_get_avatar_url')
+ ? aksrt_get_avatar_url($author_id, ['size' => 48])
+ : get_avatar_url($author_id, ['size' => 48]);
+ $home = function_exists('aksrt_get_user_home_url')
+ ? aksrt_get_user_home_url($author_id)
+ : get_author_posts_url($author_id);
+
+ $name = function_exists('aksrt_get_user_name')
+ ? aksrt_get_user_name([
+ 'id' => $author_id,
+ 'class' => 'flex ac flex1',
+ 'follow' => false,
+ 'medal' => false,
+ 'level' => false,
+ ])
+ : '' . esc_html(get_the_author_meta('display_name', $author_id)) . '';
+
+ // Prefer last reply time when available.
+ $time_raw = (string) get_post_meta($post_id, '_aksrt_last_reply_time', true);
+ if ($time_raw === '' || !strtotime($time_raw)) {
+ $time_raw = (string) $post->post_date;
+ }
+ $time_txt = function_exists('aksrt_time_ago')
+ ? aksrt_time_ago($time_raw)
+ : human_time_diff(strtotime($time_raw), current_time('timestamp')) . __('前', 'aksrt');
+
+ $views_txt = function_exists('aksrt_format_number')
+ ? aksrt_format_number($flags['views'])
+ : (string) $flags['views'];
+
+ $cls = trim('forum-posts mini aksrt-ftopic-mini relative ' . $class);
+ $html = '';
+ $html .= aksrt_forum_topic_badges_html($post_id, $show_topping);
+ $html .= '';
+
+ return $html;
+ }
+endif;
+
+if (!function_exists('aksrt_render_forum_topic_detail')) :
+ /**
+ * Detail forum topic row (richer entry meta).
+ *
+ * @param int|null $post_id
+ * @param string $class
+ * @param bool $show_topping
+ */
+ function aksrt_render_forum_topic_detail(?int $post_id = null, string $class = 'ajax-item', bool $show_topping = true): string {
+ $post_id = $post_id ?: (int) get_the_ID();
+ if ($post_id <= 0) {
+ return '';
+ }
+ $post = get_post($post_id);
+ if (!$post || $post->post_status !== 'publish') {
+ return '';
+ }
+
+ $author_id = (int) $post->post_author;
+ $flags = aksrt_forum_topic_meta_flags($post_id);
+ $permalink = get_permalink($post_id);
+ $title = get_the_title($post_id);
+ $excerpt = wp_trim_words(wp_strip_all_tags((string) $post->post_content), 36, '…');
+
+ $avatar_url = function_exists('aksrt_get_avatar_url')
+ ? aksrt_get_avatar_url($author_id, ['size' => 64])
+ : get_avatar_url($author_id, ['size' => 64]);
+ $home = function_exists('aksrt_get_user_home_url')
+ ? aksrt_get_user_home_url($author_id)
+ : get_author_posts_url($author_id);
+
+ $name = function_exists('aksrt_get_user_name')
+ ? aksrt_get_user_name([
+ 'id' => $author_id,
+ 'class' => 'flex ac',
+ 'follow' => false,
+ 'medal' => true,
+ 'level' => true,
+ ])
+ : '' . esc_html(get_the_author_meta('display_name', $author_id)) . '';
+
+ $time_raw = (string) get_post_meta($post_id, '_aksrt_last_reply_time', true);
+ if ($time_raw === '' || !strtotime($time_raw)) {
+ $time_raw = (string) $post->post_date;
+ }
+ $time_txt = function_exists('aksrt_time_ago')
+ ? aksrt_time_ago($time_raw)
+ : human_time_diff(strtotime($time_raw), current_time('timestamp')) . __('前', 'aksrt');
+
+ $views_txt = function_exists('aksrt_format_number')
+ ? aksrt_format_number($flags['views'])
+ : (string) $flags['views'];
+
+ // Optional plate name chip.
+ $plate_chip = '';
+ if ($flags['plate_id'] > 0) {
+ $plate = get_post($flags['plate_id']);
+ if ($plate instanceof WP_Post) {
+ $plate_chip = '' . esc_html(get_the_title($plate)) . '';
+ }
+ }
+
+ $cls = trim('forum-posts detail aksrt-ftopic-detail relative ' . $class);
+ $html = '';
+ $html .= aksrt_forum_topic_badges_html($post_id, $show_topping);
+ $html .= '';
+ $html .= '';
+ if ($excerpt !== '') {
+ $html .= '' . esc_html($excerpt) . '
';
+ }
+ $html .= '';
+ $html .= $plate_chip;
+ $html .= ' ' . esc_html((string) $views_txt) . '';
+ $html .= ' ' . esc_html((string) $flags['replies']) . '';
+ $html .= '
';
+
+ return $html;
+ }
+endif;
+
+if (!function_exists('aksrt_render_plate_thumb_html')) :
+ /**
+ * Shared plate thumb with lazy pattern.
+ */
+ function aksrt_render_plate_thumb_html(int $plate_id, string $size = 'thumbnail'): string {
+ $thumb = get_the_post_thumbnail_url($plate_id, $size);
+ $icon = (string) get_post_meta($plate_id, '_aksrt_icon', true);
+ $ph = get_template_directory_uri() . '/assets/img/placeholder.svg';
+ if ($thumb) {
+ return '
';
+ }
+ if ($icon !== '') {
+ return '';
+ }
+ return '';
+ }
+endif;
+
+if (!function_exists('aksrt_render_plate_row')) :
+ /**
+ * Plate list row (zibll plate-item flex).
+ *
+ * @param WP_Post|int $plate
+ */
+ function aksrt_render_plate_row($plate): string {
+ $post = $plate instanceof WP_Post ? $plate : get_post((int) $plate);
+ if (!$post instanceof WP_Post) {
+ return '';
+ }
+ $id = (int) $post->ID;
+ $url = get_permalink($id);
+ $count = (int) get_post_meta($id, '_aksrt_posts_count', true);
+ $follow = (int) get_post_meta($id, '_aksrt_follow_count', true);
+ $desc = $post->post_excerpt !== '' ? $post->post_excerpt : wp_trim_words(wp_strip_all_tags((string) $post->post_content), 18);
+
+ $html = '';
+ $html .= aksrt_render_plate_thumb_html($id, 'thumbnail');
+ $html .= '';
+ $html .= '';
+ if ($desc !== '') {
+ $html .= '
' . esc_html($desc) . '
';
+ }
+ $html .= '
';
+ $html .= '' . esc_html(sprintf(/* translators: %d topic count */ __('%d 帖', 'aksrt'), $count)) . '';
+ if ($follow > 0) {
+ $html .= '' . esc_html(sprintf(/* translators: %d follow count */ __('%d 关注', 'aksrt'), $follow)) . '';
+ }
+ $html .= '
';
+ return $html;
+ }
+endif;
+
+if (!function_exists('aksrt_render_plate_card')) :
+ /**
+ * Plate card slide item.
+ *
+ * @param WP_Post|int $plate
+ */
+ function aksrt_render_plate_card($plate): string {
+ $post = $plate instanceof WP_Post ? $plate : get_post((int) $plate);
+ if (!$post instanceof WP_Post) {
+ return '';
+ }
+ $id = (int) $post->ID;
+ $url = get_permalink($id);
+ $thumb = get_the_post_thumbnail_url($id, 'medium');
+ $count = (int) get_post_meta($id, '_aksrt_posts_count', true);
+ $follow = (int) get_post_meta($id, '_aksrt_follow_count', true);
+ $ph = get_template_directory_uri() . '/assets/img/placeholder.svg';
+
+ $html = '';
+ $html .= '';
+ if ($thumb) {
+ $html .= '
 . ')
';
+ } else {
+ $html .= '
';
+ }
+ $html .= '
';
+ $html .= '' . esc_html(get_the_title($post)) . '
';
+ $html .= '';
+ $html .= '' . esc_html(sprintf(__('%d 帖', 'aksrt'), $count)) . '';
+ $html .= '' . esc_html(sprintf(__('%d 关注', 'aksrt'), $follow)) . '';
+ $html .= '
';
+ return $html;
+ }
+endif;
+
+if (!function_exists('aksrt_render_moderator_row')) :
+ /**
+ * Moderator identity row with optional follow.
+ *
+ * @param int $user_id
+ * @param string $role author|super|moderator
+ * @param bool $show_avatar
+ * @param string $style list|grid
+ */
+ function aksrt_render_moderator_row(int $user_id, string $role = 'moderator', bool $show_avatar = true, string $style = 'list'): string {
+ if ($user_id <= 0) {
+ return '';
+ }
+ $user = get_userdata($user_id);
+ if (!$user) {
+ return '';
+ }
+ $avatar = function_exists('aksrt_get_avatar_url')
+ ? aksrt_get_avatar_url($user_id, ['size' => 64])
+ : get_avatar_url($user_id, ['size' => 64]);
+ $home = function_exists('aksrt_get_user_home_url')
+ ? aksrt_get_user_home_url($user_id)
+ : get_author_posts_url($user_id);
+ $ph = get_template_directory_uri() . '/assets/img/placeholder.svg';
+
+ if ($role === 'author' || $role === 'super') {
+ $role_label = __('超级版主', 'aksrt');
+ } else {
+ $role_label = __('版主', 'aksrt');
+ }
+
+ $name = function_exists('aksrt_get_user_name')
+ ? aksrt_get_user_name([
+ 'id' => $user_id,
+ 'class' => 'flex ac flex1',
+ 'follow' => false,
+ 'medal' => true,
+ 'level' => true,
+ ])
+ : '' . esc_html($user->display_name) . '';
+
+ // Follow control (not for self).
+ $follow_html = '';
+ $cuid = get_current_user_id();
+ if ($cuid > 0 && $cuid !== $user_id && class_exists('\AKSRT\User\Follow')) {
+ $following = \AKSRT\User\Follow::get_instance()->is_following($cuid, $user_id);
+ $label = $following ? __('已关注', 'aksrt') : __('关注', 'aksrt');
+ $follow_html = '';
+ }
+
+ if ($style === 'grid') {
+ $html = '
';
+ if ($show_avatar) {
+ $html .= '
 . ')
';
+ }
+ $html .= '
' . $name . '
';
+ $html .= '
' . esc_html($role_label) . '
';
+ $html .= $follow_html;
+ $html .= '
';
+ return $html;
+ }
+
+ $html = '';
+ if ($show_avatar) {
+ $html .= '
 . ')
';
+ }
+ $html .= '
';
+ $html .= '
' . $name . '
';
+ $html .= '
' . esc_html($role_label) . '
';
+ $html .= '
';
+ $html .= $follow_html;
+ $html .= '
';
+ return $html;
+ }
+endif;
diff --git a/src/AKSRT/Widgets/WidgetForumBoardList.php b/src/AKSRT/Widgets/WidgetForumBoardList.php
index 6127f973..c66869fa 100644
--- a/src/AKSRT/Widgets/WidgetForumBoardList.php
+++ b/src/AKSRT/Widgets/WidgetForumBoardList.php
@@ -57,27 +57,44 @@ class WidgetForumBoardList extends \WP_Widget {
$plates = get_posts($query_args);
if ($plates) {
ob_start();
- echo '';
return (string) ob_get_clean();
}
}
+
// Taxonomy fallback.
$term_args = [
'taxonomy' => 'aksrt_forum_board',
diff --git a/src/AKSRT/Widgets/WidgetForumPlateLists.php b/src/AKSRT/Widgets/WidgetForumPlateLists.php
index 89d0e256..93bb6184 100644
--- a/src/AKSRT/Widgets/WidgetForumPlateLists.php
+++ b/src/AKSRT/Widgets/WidgetForumPlateLists.php
@@ -88,20 +88,27 @@ class WidgetForumPlateLists extends \WP_Widget {
return '';
}
ob_start();
- $self = new self();
if ($style === 'card') {
- echo '