feat(forum): rebuild forum home page

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-11-11 14:41:02 +08:00
1 parent 51d5b8a2a1
commit 024ecddfed
16 files changed
+1061 -198

No files matched your search

+215
View File
@@ -0,0 +1,215 @@
<?php
namespace Jio\Forum;
defined('ABSPATH') || exit;
final class Cards {
/** @var array<int, \WP_Comment> */
private static array $hot = [];
/** Prime authors, plates and hot 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;
}
}
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 = '<article class="forum-posts detail ' . esc_attr($class) . '">';
$html .= self::status_badge($post);
$html .= self::author_box($post);
$html .= $can_view ? self::cover($post) : '';
$html .= '<div>' . self::title($post, 'forum-title flex ac', '', $show_topping) . '</div>';
$html .= self::content($post, $can_view);
$html .= '<div class="flex ac jsb mt10">' . ($plate !== '' ? $plate : '<span class="plate-null"></span>') . self::action_meta($post) . '</div>';
return $html . '</article>';
}
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 = '<article class="forum-posts mini ' . esc_attr($class) . '">';
$html .= self::status_badge($post);
$html .= '<div class="mr20 forum-user">' . self::avatar($author, 'avatar-img forum-avatar') . '<span class="show-sm ml6 flex ac" style="width: 90%;">' . $name . '</span></div>';
$html .= '<div class="entry-info">' . self::title($post, 'forum-title flex ac', 'text-ellipsis', $show_topping);
$html .= '<div class="flex ac jsb item-meta"><div class="meta-left em09-sm flex"><span class="hide-sm">' . $name . '</span>';
$html .= '<span class="icon-circle" title="' . esc_attr(sprintf('发布时间:%s', get_the_date('Y-m-d H:i:s', $post))) . '">' . esc_html(self::ago($post->post_date) . '发布') . '</span></div>';
$html .= '<div class="meta-right">' . self::icon_metas($post) . '</div></div></div>';
return $html . '</article>';
}
public static function title(\WP_Post $post, string $class, string $link_class = '', bool $show_topping = false): string {
$id = $post->ID;
$badges = '';
if ($show_topping && get_post_meta($id, '_jio_forum_pinned', true)) $badges .= '<span class="badg badg-sm c-blue mr3">置顶</span>';
if (get_post_meta($id, '_jio_forum_essence', true)) $badges .= '<span class="badg badg-sm jb-red mr3">精华</span>';
$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 .= '<span class="badg badg-sm badge-question mr3 ' . ($solved ? 'c-green' : 'c-yellow') . '">问<text>' . ($solved ? '已解决' : '待解决') . '</text></span>';
} elseif ($type === 'vote') {
$badges .= '<span class="badg badg-sm c-blue mr3">投票</span>';
}
$url = $post->post_status === 'publish' ? get_permalink($post) : Core::edit_url($id);
$link_class = $link_class !== '' ? ' class="' . esc_attr($link_class) . '"' : '';
return '<h2 class="' . esc_attr($class) . '"><a' . $link_class . ' href="' . esc_url($url) . '" title="' . esc_attr($post->post_title) . '">' . $badges . esc_html($post->post_title) . '</a></h2>';
}
public static function plate_item(\WP_Post $plate): string {
$url = get_permalink($plate);
$counts = PlateStats::get($plate->ID);
$today = $counts['today'] > 0 ? '<span class="badg but-plate hide-sm flex0" title="' . esc_attr(sprintf('今日新帖 %d', $counts['today'])) . '"><i class="fa fas fa-comment-dots c-red mr3"></i>' . esc_html(sprintf('今日:%s', jio_cut_count($counts['today']))) . '</span>' : '';
$excerpt = wp_trim_words(wp_strip_all_tags($plate->post_excerpt ?: $plate->post_content), 30);
$html = '<div class="plate-item flex">';
if (jio_get_setting('jio_forum_show_plate_cover', true)) {
$html .= '<a class="plate-thumb" href="' . esc_url($url) . '">' . self::thumbnail($plate) . '</a>';
}
$html .= '<div class="item-info flex1"><div class="info-header"><h2 class="forum-title"><a class="flex ac" href="' . esc_url($url) . '" title="' . esc_attr($plate->post_title) . '"><span class="mr6 text-ellipsis">' . esc_html($plate->post_title) . '</span>' . $today . '</a></h2></div>';
$html .= '<a class="excerpt em09" href="' . esc_url($url) . '">' . esc_html($excerpt) . '</a>';
$html .= '<div class="count-mates px12"><item><text>帖子</text>' . esc_html(jio_cut_count($counts['posts'])) . '</item><item class="ml10"><text>关注</text>' . esc_html(jio_cut_count((int) get_post_meta($plate->ID, '_jio_forum_follow_count', true))) . '</item></div>';
return $html . '</div></div>';
}
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 ? '<badge class="spot abs-tr" title="' . esc_attr(sprintf('今日有%d篇新帖', $counts['today'])) . '"></badge>' : '';
$html = '<div class="plate-card' . ($class !== '' ? ' ' . esc_attr($class) : '') . '"><a title="' . esc_attr($plate->post_title) . '" href="' . esc_url($url) . '">';
$html .= '<div class="plate-thumb">' . self::thumbnail($plate) . $spot . '</div>';
$html .= '<div class="title text-ellipsis mt10">' . esc_html($plate->post_title) . '</div>';
$html .= '<div class="mt3 px12 muted-2-color text-ellipsis count">' . esc_html(sprintf('%s关注', jio_cut_count((int) get_post_meta($plate->ID, '_jio_forum_follow_count', true)))) . '</div></a>';
if ($follow) $html .= self::follow_button($plate->ID, 'but jb-pink but-plate btn-block mt20');
return $html . '</div>';
}
/** @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 .= '<div class="swiper-slide">' . self::plate_card($plate, $class, $follow, $today) . '</div>';
}
return '<div class="swiper-container swiper-scroll scroll-plate"><div class="swiper-wrapper">' . $slides . '</div><div class="swiper-button-prev"></div><div class="swiper-button-next"></div></div>';
}
public static function follow_button(int $plate_id, string $class): string {
if (!is_user_logged_in()) {
return '<a class="btn-follow ' . esc_attr($class) . '" href="' . esc_url(jio_get_login_url(get_permalink($plate_id))) . '"><text>关注</text></a>';
}
$followed = Access::follows(get_current_user_id(), $plate_id);
return '<button type="button" class="btn-follow ' . esc_attr($class . ($followed ? ' active' : '')) . '" data-forum-action="follow" data-id="' . (int) $plate_id . '"><text>' . ($followed ? '已关注' : '关注') . '</text></button>';
}
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 '<a class="but but-plate text-ellipsis" href="' . esc_url(get_permalink($plate_id)) . '"><i class="fas fa-cube mr3"></i>' . esc_html(get_the_title($plate_id)) . '<i class="fas fa-angle-right ml6"></i></a>';
}
public static function avatar(int $user_id, string $class): string {
return function_exists('jio_get_avatar_box') ? jio_get_avatar_box($user_id, $class) : '<span class="' . esc_attr($class) . '">' . get_avatar($user_id, 80) . '</span>';
}
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 {
return '<a class="display-name text-ellipsis" href="' . esc_url(get_author_posts_url($user_id)) . '">' . esc_html((string) get_the_author_meta('display_name', $user_id)) . '</a>';
}
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 = '<span title="' . esc_attr(get_the_date('Y-m-d H:i:s', $post)) . '">' . esc_html(self::ago($post->post_date)) . '</span><span class="icon-spot">' . esc_html(sprintf('%s次阅读', $views)) . '</span>';
return '<div class="user-info flex ac mb10">' . self::avatar($author, 'avatar-img') . '<div class="user-right flex flex1 ac jsb ml10"><div class="flex1 overflow-hidden"><div class="flex ac">' . $name . '</div><div class="px12-sm muted-2-color text-ellipsis mt3">' . $desc . '</div></div></div></div>';
}
private static function status_badge(\WP_Post $post): string {
$labels = ['pending' => '待审核', 'draft' => '草稿', 'trash' => '已删除'];
return isset($labels[$post->post_status]) ? '<span class="img-badge badge-status top jb-yellow px12">' . esc_html($labels[$post->post_status]) . '</span>' : '';
}
private static function cover(\WP_Post $post): string {
if (!has_post_thumbnail($post)) return '';
return '<div class="forum-lists-cover mb10"><div class="graphic imgbox-container" style="padding-bottom:45%">' . get_the_post_thumbnail($post, 'large', ['class' => 'fit-cover']) . '</div></div>';
}
private static function content(\WP_Post $post, bool $can_view): string {
if (!$can_view) {
return '<div class="hide-post mt6"><i class="fa fas fa-unlock-alt mr6"></i>此处内容已隐藏</div>';
}
$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 = '<div><div class="content mt6">' . ($text !== '' ? '<a href="' . esc_url($url) . '" class="excerpt">' . esc_html($text) . '</a>' : '') . '</div>';
$html .= $images;
$hot = self::$hot[$post->ID] ?? null;
if ($hot instanceof \WP_Comment) {
$html .= '<div class="hot-comment mt10"><div class="flex ac mb6">' . self::avatar((int) $hot->user_id, 'avatar-img') . '<span class="muted-color em09">' . esc_html(get_comment_author($hot)) . '</span><span class="badg badg-sm c-red ml6">热门回复</span></div>';
$html .= '<div class="comment-content">' . esc_html(wp_trim_words(wp_strip_all_tags($hot->comment_content), 40)) . '</div></div>';
}
return $html . '</div>';
}
private static function images(\WP_Post $post): string {
if (!preg_match_all('/<img[^>]+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 = '<div class="imgbox-container lists-imgs mt6 count-' . $shown . '">';
foreach (array_slice($urls, 0, $shown) as $index => $url) {
$more = $index === $shown - 1 && $total > $shown ? '<span class="abs-center lists-imgs-more">+' . ($total - $shown) . '</span>' : '';
$html .= '<span><img' . jio_get_lazy_attr('lazy_posts_content', $url, 'fit-cover radius4') . ' alt="' . esc_attr($post->post_title) . '">' . $more . '</span>';
}
return $html . '</div>';
}
private static function action_meta(\WP_Post $post): string {
$id = $post->ID;
$score = (int) get_post_meta($id, '_jio_forum_score', true);
$reply = (int) $post->comment_count;
$html = '<div class="action-meta flex0 flex jsb">';
if (jio_get_setting('jio_forum_score_enabled', true)) {
$html .= is_user_logged_in()
? '<button type="button" class="item btn-score" data-forum-action="score" data-direction="up" data-id="' . $id . '"><i class="fa far fa-thumbs-up"></i><text>' . esc_html($score ? jio_cut_count($score) : '加分') . '</text></button>'
: '<a class="item btn-score" href="' . esc_url(jio_get_login_url(get_permalink($post))) . '"><i class="fa far fa-thumbs-up"></i><text>' . esc_html($score ? jio_cut_count($score) : '加分') . '</text></a>';
}
$html .= '<a rel="nofollow" href="' . esc_url(get_permalink($post) . '#comments') . '" class="item"><i class="fa far fa-comment"></i><text>' . esc_html($reply ? jio_cut_count($reply) : '回复') . '</text></a>';
if ($post->post_status === 'publish' && function_exists('jio_get_post_share_btn')) {
$html .= jio_get_post_share_btn($post, 'item btn-share', true, false);
}
return $html . '</div>';
}
private static function icon_metas(\WP_Post $post): string {
$id = $post->ID;
return '<item><i class="fa far fa-eye mr3"></i>' . esc_html(jio_cut_count((int) get_post_meta($id, '_jio_forum_views', true))) . '</item>'
. '<item><i class="fa far fa-comment mr3"></i>' . esc_html(jio_cut_count((int) $post->comment_count)) . '</item>'
. '<item><i class="fa far fa-thumbs-up mr3"></i>' . esc_html(jio_cut_count((int) get_post_meta($id, '_jio_forum_score', true))) . '</item>';
}
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 '<img' . jio_get_lazy_attr('lazy_other', (string) $src, 'forum-thumbnail fit-cover') . ' alt="' . esc_attr($plate->post_title) . '">';
}
}