*/ private static array $hot = []; /** @var array */ private static array $last_reply = []; /** Prime authors, plates, hot replies and last replies for a page of posts in batch queries. */ public static function prime(array $posts): void { $ids = $authors = $plates = []; foreach ($posts as $post) { if (!$post instanceof \WP_Post) continue; $ids[] = $post->ID; $authors[] = (int) $post->post_author; $plates[] = (int) get_post_meta($post->ID, '_jio_forum_plate_id', true); } if (!$ids) return; cache_users(array_values(array_unique(array_filter($authors)))); _prime_post_caches(array_values(array_unique(array_filter($plates))), true, true); $wanted = array_flip($ids); $comments = get_comments(['status' => 'approve', 'meta_key' => '_jio_forum_hot', 'meta_value' => '1', 'number' => 200, 'orderby' => 'comment_date_gmt', 'order' => 'DESC']); foreach ($comments as $comment) { $post_id = (int) $comment->comment_post_ID; if (isset($wanted[$post_id]) && !isset(self::$hot[$post_id])) self::$hot[$post_id] = $comment; } self::prime_last_replies($ids); } /** @param int[] $ids */ private static function prime_last_replies(array $ids): void { $missing = array_values(array_diff(array_unique(array_map('absint', $ids)), array_keys(self::$last_reply))); if (!$missing) return; global $wpdb; $placeholders = implode(',', array_fill(0, count($missing), '%d')); $sql = "SELECT comment_post_ID AS post_id, MAX(comment_date) AS last_date FROM {$wpdb->comments} WHERE comment_approved = '1' AND comment_post_ID IN ($placeholders) GROUP BY comment_post_ID"; $rows = $wpdb->get_results($wpdb->prepare($sql, $missing)); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared foreach ((array) $rows as $row) { self::$last_reply[(int) $row->post_id] = (string) $row->last_date; } } public static function post_detail(\WP_Post $post, string $class = 'alone ajax-item', bool $show_plate = true, bool $show_topping = false): string { $id = $post->ID; $can_view = Access::can_view(get_current_user_id(), $id); $plate_id = (int) get_post_meta($id, '_jio_forum_plate_id', true); $plate = $show_plate ? self::plate_button($plate_id) : ''; $html = '
'; $html .= self::status_badge($post); $html .= self::author_box($post); $html .= $can_view ? self::cover($post) : ''; $html .= '
' . self::title($post, 'forum-title flex ac', '', $show_topping) . '
'; $html .= self::content($post, $can_view); $html .= '
' . ($plate !== '' ? $plate : '') . self::action_meta($post) . '
'; return $html . '
'; } public static function post_mini(\WP_Post $post, string $class = 'alone ajax-item', bool $show_topping = false): string { $author = (int) $post->post_author; $name = self::author_link($author); $html = '
'; $html .= self::status_badge($post); $html .= '
' . self::avatar($author, 'avatar-img forum-avatar') . '' . $name . '
'; $html .= ''; return $html . '
'; } /** Reference mini-card meta: "最后回复" when replies exist, otherwise the publish time. */ private static function reply_time(\WP_Post $post): string { $last = self::$last_reply[$post->ID] ?? ''; if ($last !== '') { return '' . esc_html(self::ago($last) . '回复') . ''; } return '' . esc_html(self::ago($post->post_date) . '发布') . ''; } public static function title(\WP_Post $post, string $class, string $link_class = '', bool $show_topping = false, string $tag = 'h2'): string { $id = $post->ID; $badges = ''; if ($show_topping && get_post_meta($id, '_jio_forum_pinned', true)) $badges .= '置顶'; if (get_post_meta($id, '_jio_forum_essence', true)) $badges .= '精华'; $type = (string) get_post_meta($id, '_jio_forum_type', true); if ($type === 'question') { $solved = (bool) get_post_meta($id, '_jio_forum_adopted_comment', true); $badges .= '问' . ($solved ? '已解决' : '待解决') . ''; } elseif ($type === 'vote') { $badges .= '投票'; } $url = $post->post_status === 'publish' ? get_permalink($post) : Core::edit_url($id); $link_class = $link_class !== '' ? ' class="' . esc_attr($link_class) . '"' : ''; $tag = in_array($tag, ['h1', 'h2'], true) ? $tag : 'h2'; return '<' . $tag . ' class="' . esc_attr($class) . '">' . $badges . esc_html($post->post_title) . ''; } /** Plate page header: blurred cover, title, stats and moderators (reference layout). */ public static function plate_header(\WP_Post $plate): string { $id = $plate->ID; PlateStats::prime([$id]); $stats = PlateStats::get($id); $followers = (int) get_post_meta($id, '_jio_forum_follow_count', true); $plate_name = (string) jio_get_setting('jio_forum_plate_name', '版块') ?: '版块'; $thumb = self::thumbnail($plate); $cats = ''; $terms = wp_get_post_terms($id, Core::CATEGORY); foreach (is_array($terms) ? $terms : [] as $term) { $cats .= '' . esc_html($term->name) . ''; } $meta = '
' . esc_html(sprintf('%s %s', $plate_name, jio_cut_count($stats['posts']))) . '' . '' . esc_html(sprintf('今日 %s', jio_cut_count($stats['today']))) . '' . ($followers > 0 ? '' . esc_html(sprintf('关注 %s', jio_cut_count($followers))) . '' : '') . '
'; $excerpt = wp_trim_words(wp_strip_all_tags($plate->post_excerpt ?: $plate->post_content), 40); $html = '
'; $html .= '
' . $thumb . '
'; $html .= self::plate_more_btns($plate); if (get_post_meta($id, '_jio_forum_hot', true)) { $html .= '热门'; } $html .= '
'; if ($cats !== '') $html .= '
' . $cats . '
'; $html .= '

' . esc_html($plate->post_title) . '

'; $html .= $meta; if ($excerpt !== '') $html .= '
' . esc_html($excerpt) . '
'; $html .= '
' . $thumb . '
'; $html .= self::plate_moderator_btns($plate); return $html . '
'; } /** Header corner actions: follow, search and share (reference `.more-btns`). */ private static function plate_more_btns(\WP_Post $plate): string { $html = '
' . self::follow_button($plate->ID, ''); if (jio_get_setting('jio_forum_search_enabled', true)) { $html .= ''; } if (function_exists('jio_get_post_share_btn')) { $html .= jio_get_post_share_btn($plate, 'item btn-share', true, true); } return $html . '
'; } /** Plate moderator avatars row under the header. */ private static function plate_moderator_btns(\WP_Post $plate): string { $ids = array_values(array_unique(array_filter(array_merge( [(int) $plate->post_author], array_map('absint', (array) get_post_meta($plate->ID, '_jio_forum_moderators', true)) )))); $btns = ''; foreach ($ids as $user_id) { $user = get_userdata($user_id); if (!$user) continue; $btns .= '' . get_avatar($user_id, 40) . '' . esc_html($user->display_name) . ''; } if (is_user_logged_in() && Access::can_publish(get_current_user_id(), $plate->ID)) { $btns .= '发布'; } $html = '
' . $btns; if (is_user_logged_in() && !Access::is_moderator(get_current_user_id(), $plate->ID) && jio_get_setting('jio_forum_moderator_apply', true)) { $html .= ''; } return $html . '
'; } public static function plate_item(\WP_Post $plate): string { $url = get_permalink($plate); $counts = PlateStats::get($plate->ID); $today = $counts['today'] > 0 ? '' . esc_html(sprintf('今日:%s', jio_cut_count($counts['today']))) . '' : ''; $excerpt = wp_trim_words(wp_strip_all_tags($plate->post_excerpt ?: $plate->post_content), 30); $html = '
'; if (jio_get_setting('jio_forum_show_plate_cover', true)) { $html .= '' . self::thumbnail($plate) . ''; } $html .= '
'; $html .= '' . esc_html($excerpt) . ''; $html .= '
帖子' . esc_html(jio_cut_count($counts['posts'])) . '关注' . esc_html(jio_cut_count((int) get_post_meta($plate->ID, '_jio_forum_follow_count', true))) . '
'; return $html . '
'; } public static function plate_card(\WP_Post $plate, string $class = '', bool $follow = true, bool $today = false): string { $url = get_permalink($plate); $counts = PlateStats::get($plate->ID); $spot = $today && $counts['today'] > 0 ? '' : ''; $html = ''; } /** @param \WP_Post[] $plates */ public static function plate_slide(array $plates, string $class = '', bool $follow = true, bool $today = false): string { if (!$plates) return ''; $slides = ''; foreach ($plates as $plate) { $slides .= '
' . self::plate_card($plate, $class, $follow, $today) . '
'; } return '
' . $slides . '
'; } public static function follow_button(int $plate_id, string $class): string { if (!is_user_logged_in()) { return ''; } $followed = Access::follows(get_current_user_id(), $plate_id); return ''; } public static function plate_button(int $plate_id): string { if ($plate_id <= 0 || get_queried_object_id() === $plate_id || get_post_status($plate_id) !== 'publish') return ''; return '' . esc_html(get_the_title($plate_id)) . ''; } public static function avatar(int $user_id, string $class): string { return function_exists('jio_get_avatar_box') ? jio_get_avatar_box($user_id, $class) : '' . get_avatar($user_id, 80) . ''; } public static function ago(string $date): string { return function_exists('jio_time_ago') ? jio_time_ago($date) : human_time_diff((int) strtotime($date), (int) current_time('timestamp')) . '前'; } private static function author_link(int $user_id): string { $url = function_exists('jio_get_n') ? jio_get_n($user_id) : get_author_posts_url($user_id); return '' . esc_html((string) get_the_author_meta('display_name', $user_id)) . ''; } /** Reference author row: avatar, name, publish time + views, follow and message actions. */ private static function author_box(\WP_Post $post): string { $author = (int) $post->post_author; $name = function_exists('jio_get_user_name') ? jio_get_user_name(['id' => $author, 'class' => 'flex ac flex1']) : self::author_link($author); $views = jio_cut_count((int) get_post_meta($post->ID, '_jio_forum_views', true)); $desc = '' . esc_html(self::ago($post->post_date) . '发布') . '' . esc_html(sprintf('%s次阅读', $views)) . ''; $actions = ''; if (function_exists('jio_get_user_follow')) { $actions .= jio_get_user_follow('px12-sm ml10 follow but c-red', $author); } if (function_exists('jio_get_messages_page_url')) { $messages = jio_get_messages_page_url(); if ($messages !== '' && get_current_user_id() !== $author) { $actions .= '私信'; } } $html = ''; } private static function status_badge(\WP_Post $post): string { $labels = ['pending' => '待审核', 'draft' => '草稿', 'trash' => '已删除']; return isset($labels[$post->post_status]) ? '' . esc_html($labels[$post->post_status]) . '' : ''; } private static function cover(\WP_Post $post): string { if (!has_post_thumbnail($post)) return ''; return '
' . get_the_post_thumbnail($post, 'large', ['class' => 'fit-cover']) . '
'; } private static function content(\WP_Post $post, bool $can_view): string { if (!$can_view) { return '
此处内容已隐藏
'; } $url = $post->post_status === 'publish' ? get_permalink($post) : Core::edit_url($post->ID); $images = self::images($post); $text = trim((string) preg_replace('/\s+/u', ' ', wp_strip_all_tags(strip_shortcodes($post->post_excerpt ?: $post->post_content)))); $text = jio_str_cut($text, 0, $images !== '' ? 60 : 120); $html = '
' . ($text !== '' ? '' . esc_html($text) . '' : '') . '
'; $html .= $images; $hot = self::$hot[$post->ID] ?? null; if ($hot instanceof \WP_Comment) { $html .= '
' . self::avatar((int) $hot->user_id, 'avatar-img') . '' . esc_html(get_comment_author($hot)) . '热门回复
'; $html .= '
' . esc_html(wp_trim_words(wp_strip_all_tags($hot->comment_content), 40)) . '
'; } return $html . '
'; } private static function images(\WP_Post $post): string { if (!preg_match_all('/]+src=["\']([^"\']+)["\']/i', $post->post_content, $matches)) return ''; $urls = array_values(array_unique(array_filter(array_map('esc_url_raw', $matches[1])))); $total = count($urls); if ($total === 0) return ''; $shown = min(4, $total); $html = '
'; foreach (array_slice($urls, 0, $shown) as $index => $url) { $more = $index === $shown - 1 && $total > $shown ? '+' . ($total - $shown) . '' : ''; $html .= '' . $more . ''; } return $html . '
'; } /** Reference action row: score up/down group, reply count and share. */ private static function action_meta(\WP_Post $post): string { $id = $post->ID; $html = '
'; if (jio_get_setting('jio_forum_score_enabled', true)) { $user_id = get_current_user_id(); $user_score = $user_id > 0 ? (int) get_user_meta($user_id, '_jio_forum_score_' . $id, true) : 0; $score = (int) get_post_meta($id, '_jio_forum_score', true); $html .= '
' . self::score_button($post, 'up', 'extra', $user_score > 0) . '' . esc_html($score ? jio_cut_count($score) : '评分') . '' . self::score_button($post, 'down', 'deduct', $user_score < 0) . '
'; } $reply = (int) $post->comment_count; $html .= '' . esc_html($reply ? jio_cut_count($reply) : '回复') . ''; if ($post->post_status === 'publish' && function_exists('jio_get_post_share_btn')) { $html .= jio_get_post_share_btn($post, 'item btn-share', true, true); } return $html . '
'; } private static function score_button(\WP_Post $post, string $direction, string $kind, bool $active): string { $icon = ''; $class = 'btn-score ' . $kind . ($active ? ' active' : ''); if (!is_user_logged_in()) { return ''; } return ''; } private static function icon_metas(\WP_Post $post): string { $id = $post->ID; return '' . esc_html(jio_cut_count((int) get_post_meta($id, '_jio_forum_views', true))) . '' . '' . esc_html(jio_cut_count((int) $post->comment_count)) . '' . '' . esc_html(jio_cut_count((int) get_post_meta($id, '_jio_forum_score', true))) . ''; } private static function thumbnail(\WP_Post $plate): string { $src = get_the_post_thumbnail_url($plate, 'thumbnail') ?: get_template_directory_uri() . '/assets/img/thumbnail.svg'; return ''; } }