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) . '">';
}
}
+8
View File
@@ -28,6 +28,14 @@ final class Core {
add_action('trashed_post', [self::class, 'cascade_trashed_plate']);
add_action('untrashed_post', [self::class, 'restore_trashed_plate']);
add_filter('wp_untrash_post_status', [self::class, 'restore_status'], 10, 2);
add_filter('jio_is_show_sidebar', [self::class, 'show_home_sidebar']);
}
/** The forum home renders its own sidebar column, so keep the layout slot open. */
public static function show_home_sidebar(bool $show): bool {
if ($show || !self::enabled() || !is_page_template('templates/pages/page-forums.php')) return $show;
$cfg = function_exists('jio_get_sidebar_config_safe') ? jio_get_sidebar_config_safe() : [];
return !empty($cfg['enabled']);
}
public static function enabled(): bool {
+196
View File
@@ -0,0 +1,196 @@
<?php
namespace Jio\Forum;
defined('ABSPATH') || exit;
final class HomeContent {
public static function render(array $tab, int $index): string {
$page = self::page();
$html = match ($tab['type']) {
'plate' => self::plate_tab(),
'follow' => self::follow_tab($tab, $index, $page),
'pending' => self::pending_tab($tab, $index, $page),
default => self::posts_tab($tab, $index, $page),
};
if ($page === 1 && $tab['type'] !== 'plate' && jio_get_setting('jio_forum_home_quick_post', true) && $index === (int) jio_get_setting('jio_forum_home_quick_post_tab', 2)) {
$html = QuickPost::render() . $html;
}
return $html;
}
public static function page(): int {
$raw = $_GET['fpage'] ?? 1;
return max(1, is_scalar($raw) ? absint($raw) : 1);
}
private static function posts_tab(array $tab, int $index, int $page): string {
$args = self::query_args($tab, $page);
$html = self::post_list($args, $tab, $index, $page, $tab['type'] === 'synthesis');
if ($html !== '') return $html;
$html = jio_get_ajax_null('内容空空如也');
if ($page === 1) {
$html .= '<div class="text-center mb20 ajax-item">' . self::create_plate_button('ml10 mr10 mt10 but hollow c-blue padding-lg') . self::publish_button('ml10 mr10 mt10 but hollow c-green padding-lg') . '</div>';
}
return $html;
}
private static function follow_tab(array $tab, int $index, int $page): string {
$user_id = get_current_user_id();
if (!$user_id) {
return '<div class="ajax-item">' . jio_get_null('登录后继续查看', 20, 'null-user.svg', '', 0, 220)
. '<p class="text-center mb20"><a class="but jb-blue padding-lg" href="' . esc_url(jio_get_login_url(Core::home_url())) . '"><i class="fa fas fa-sign-in-alt mr6"></i>登录</a></p></div>';
}
$followed = View::user_marked_ids($user_id, '_jio_forum_follow_');
$html = '';
if ($page === 1 && jio_get_setting('jio_forum_home_recommend_plate', true)) {
$plates = self::plates(['posts_per_page' => 8, 'post__not_in' => $followed]);
$slide = Cards::plate_slide($plates);
if ($slide !== '') {
$html .= '<div class="ajax-item mb20"><div class="box-body notop"><div class="title-theme">热门推荐</div></div>' . $slide . '</div>';
}
}
$list = '';
if ($followed) {
$args = self::query_args($tab, $page);
$args['meta_query'][] = ['key' => '_jio_forum_plate_id', 'value' => $followed, 'compare' => 'IN'];
$list = self::post_list($args, $tab, $index, $page);
}
return $html . ($list !== '' ? $list : jio_get_ajax_null('暂无内容', 60, 'null.svg'));
}
private static function pending_tab(array $tab, int $index, int $page): string {
$html = '';
if ($page === 1) {
foreach (get_posts(['post_type' => Core::PLATE, 'post_status' => 'pending', 'posts_per_page' => 20]) as $plate) {
$html .= '<div class="forum-pending-plate jio-widget-box flex ac jsb ajax-item"><span class="text-ellipsis"><span class="badg badg-sm c-yellow mr6">待审核版块</span>' . esc_html($plate->post_title) . '</span>'
. '<button type="button" class="but c-blue p2-10 px12 flex0" data-forum-action="plate-audit" data-id="' . (int) $plate->ID . '">通过审核</button></div>';
}
}
$list = self::post_list(['post_type' => Core::POST, 'post_status' => 'pending', 'posts_per_page' => self::per_page(), 'paged' => $page], $tab, $index, $page);
$html .= $list;
return $html !== '' ? $html : jio_get_ajax_null('暂无待审核内容');
}
private static function plate_tab(): string {
$html = '';
$user_id = get_current_user_id();
if ($user_id && jio_get_setting('jio_forum_home_follow_plate', true)) {
$followed = View::user_marked_ids($user_id, '_jio_forum_follow_');
$slide = $followed ? Cards::plate_slide(self::plates(['post__in' => $followed, 'posts_per_page' => 20]), 'mini', false, true) : '';
if ($slide !== '') $html .= self::panel('我关注的版块', $slide);
}
if (jio_get_setting('jio_forum_home_hot_plate', true)) {
$slide = Cards::plate_slide(self::plates(['posts_per_page' => 8, 'meta_key' => '_jio_forum_follow_count', 'orderby' => 'meta_value_num']));
if ($slide !== '') $html .= self::panel('热门推荐', $slide);
}
$all = self::plates(['posts_per_page' => -1, 'orderby' => 'menu_order title', 'order' => 'ASC']);
$groups = [];
$loose = [];
$terms = wp_get_object_terms(wp_list_pluck($all, 'ID'), Core::CATEGORY, ['fields' => 'all_with_object_id']);
$by_plate = [];
foreach (is_array($terms) ? $terms : [] as $term) {
$by_plate[(int) $term->object_id][] = $term;
}
foreach ($all as $plate) {
if (empty($by_plate[$plate->ID])) {
$loose[] = $plate;
continue;
}
foreach ($by_plate[$plate->ID] as $term) {
$groups[$term->term_id]['term'] = $term;
$groups[$term->term_id]['plates'][] = $plate;
}
}
uasort($groups, static fn(array $a, array $b): int => count($b['plates']) <=> count($a['plates']));
$plate_name = (string) jio_get_setting('jio_forum_plate_name', '版块') ?: '版块';
$single_group = count($groups) === 0 && $loose;
if ($loose) $groups[0] = ['term' => null, 'plates' => $loose];
$i = 1;
foreach ($groups as $term_id => $group) {
$name = $group['term'] instanceof \WP_Term ? $group['term']->name : ($single_group ? '全部' . $plate_name : '其他' . $plate_name);
$html .= self::plate_group('home-plate-panel-' . $i++, $name, $group['plates'], (int) $term_id);
}
if ($html === '') {
$html = jio_get_null('暂时没有版块', 60, 'null.svg', '', 0, 170) . '<div class="text-center mt20">' . self::create_plate_button('but hollow c-blue padding-lg') . '</div>';
}
return '<div class="ajax-item">' . $html . '</div>';
}
/** @param \WP_Post[] $plates */
private static function plate_group(string $id, string $name, array $plates, int $term_id): string {
$add = self::create_plate_button('plate-add ml20 but hollow c-blue p2-10 px12 flex0', $term_id);
$items = '';
foreach ($plates as $plate) {
$items .= Cards::plate_item($plate);
}
$title = '<span class="title-theme" title="' . esc_attr($name) . '"><span class="text-ellipsis">' . esc_html($name) . '</span></span>';
$top = wp_is_mobile()
? '<h4 class="panel-title flex ac">' . $title . $add . '</h4>'
: '<h4 class="panel-title flex ac"><a class="flex ac jsb flex1" data-toggle="collapse" href="#' . esc_attr($id) . '">' . $title . '<span><i class="ml10 mr10 fas fa-angle-up em14 muted-2-color"></i></span></a>' . $add . '</h4>';
return '<div class="panel panel-plate">' . $top . '<div id="' . esc_attr($id) . '" class="panel-collapse collapse in"><div class="plate-lists">' . $items . '</div></div></div>';
}
private static function panel(string $title, string $body): string {
return '<div class="panel panel-plate"><h4 class="panel-title" title="' . esc_attr($title) . '"><span class="title-theme">' . esc_html($title) . '</span></h4>' . $body . '</div>';
}
/** @return \WP_Post[] */
private static function plates(array $args): array {
$plates = get_posts(array_merge(['post_type' => Core::PLATE, 'post_status' => 'publish', 'orderby' => 'modified', 'order' => 'DESC', 'no_found_rows' => true], $args));
PlateStats::prime(wp_list_pluck($plates, 'ID'));
return $plates;
}
private static function query_args(array $tab, int $page): array {
$args = [
'post_type' => Core::POST,
'post_status' => 'publish',
'posts_per_page' => self::per_page(),
'paged' => $page,
'jio_forum_list' => $tab['orderby'],
'meta_query' => [],
];
if ($tab['filter'] === 'essence') {
$args['meta_query'][] = ['key' => '_jio_forum_essence', 'value' => 1];
} elseif (in_array($tab['filter'], ['question', 'vote'], true)) {
$args['meta_query'][] = ['key' => '_jio_forum_type', 'value' => $tab['filter']];
}
return $args;
}
private static function post_list(array $args, array $tab, int $index, int $page, bool $show_topping = false): string {
$query = new \WP_Query($args);
if (!$query->have_posts()) return '';
Cards::prime($query->posts);
$html = '';
foreach ($query->posts as $post) {
$html .= $tab['style'] === 'mini' || $tab['type'] === 'pending'
? Cards::post_mini($post, 'alone ajax-item', $show_topping)
: Cards::post_detail($post, 'alone ajax-item', true, $show_topping);
}
if ($page < (int) $query->max_num_pages) {
$next = add_query_arg(['index' => $index, 'fpage' => $page + 1], remove_query_arg(['index', 'fpage']));
$html .= '<div class="text-center theme-pagination forum-more"><a class="but hollow c-blue" href="' . esc_url($next) . '" data-forum-more>加载更多</a></div>';
}
$html .= '<div class="post_ajax_loader">' . HomeTabs::placeholder('posts', 2) . '</div>';
return $html;
}
private static function per_page(): int {
return max(1, min(50, (int) jio_get_setting('jio_forum_page_size', 20)));
}
public static function create_plate_button(string $class, int $term_id = 0): string {
$user_id = get_current_user_id();
if (!$user_id || (!Access::is_admin($user_id) && !jio_get_setting('jio_forum_user_plates', true))) return '';
$name = (string) jio_get_setting('jio_forum_plate_name', '版块') ?: '版块';
return '<button type="button" class="' . esc_attr($class) . '" data-forum-open="plate" data-category="' . (int) $term_id . '"><i class="fa fas fa-plus mr3"></i>创建' . esc_html($name) . '</button>';
}
public static function publish_button(string $class): string {
$name = (string) jio_get_setting('jio_forum_post_name', '帖子') ?: '帖子';
$url = is_user_logged_in() ? Core::edit_url() : jio_get_login_url(Core::edit_url());
return '<a class="' . esc_attr($class) . '" href="' . esc_url($url) . '"><i class="fa fas fa-edit mr3"></i>发布' . esc_html($name) . '</a>';
}
}
+98
View File
@@ -0,0 +1,98 @@
<?php
namespace Jio\Forum;
defined('ABSPATH') || exit;
final class HomeTabs {
public const TYPES = ['follow', 'synthesis', 'plate', 'posts'];
public const ORDERS = ['date', 'modified', 'last_reply', 'popular', 'views', 'score'];
public const FILTERS = ['', 'essence', 'question', 'vote'];
/** @return array<int, array<string, mixed>> */
public static function defaults(): array {
$row = static fn(string $type, string $title, string $orderby = 'date', string $filter = ''): array => [
'title' => $title, 'type' => $type, 'orderby' => $orderby, 'filter' => $filter, 'style' => 'detail', 'show' => ['pc_s', 'm_s'],
];
return [
$row('follow', '关注'),
$row('synthesis', '综合'),
$row('plate', '版块'),
$row('posts', '热门', 'views'),
$row('posts', '精华', 'modified', 'essence'),
$row('posts', '问答', 'modified', 'question'),
$row('posts', '投票', 'modified', 'vote'),
$row('posts', '最新回复', 'last_reply'),
$row('posts', '最高评分', 'score'),
];
}
/** Tabs visible on the current device, numbered from 1 like the `index` query argument. */
public static function visible(): array {
$rows = jio_get_setting('jio_forum_home_tab_items', self::defaults());
$device = wp_is_mobile() ? 'm_s' : 'pc_s';
$tabs = [];
foreach (is_array($rows) ? $rows : [] as $row) {
if (!is_array($row) || !in_array($device, (array) ($row['show'] ?? []), true)) continue;
$type = in_array($row['type'] ?? '', self::TYPES, true) ? $row['type'] : 'posts';
$tabs[] = [
'title' => sanitize_text_field((string) ($row['title'] ?? '')) ?: '栏目',
'type' => $type,
'orderby' => in_array($row['orderby'] ?? '', self::ORDERS, true) ? $row['orderby'] : 'date',
'filter' => in_array($row['filter'] ?? '', self::FILTERS, true) ? $row['filter'] : '',
'style' => ($row['style'] ?? '') === 'mini' ? 'mini' : 'detail',
];
}
if (Access::is_admin(get_current_user_id())) {
$tabs[] = ['title' => '待审核', 'type' => 'pending', 'orderby' => 'date', 'filter' => '', 'style' => 'mini'];
}
return $tabs ? array_combine(range(1, count($tabs)), $tabs) : [];
}
public static function active_index(int $count): int {
$raw = $_GET['index'] ?? 0;
$index = (is_scalar($raw) ? absint($raw) : 0) ?: (int) jio_get_setting('jio_forum_home_tab_active', 2);
return $index >= 1 && $index <= $count ? $index : 1;
}
public static function render(): void {
$tabs = self::visible();
if (!$tabs) return;
$active = self::active_index(count($tabs));
$base = remove_query_arg(['index', 'fpage']);
$nav = $panes = '';
foreach ($tabs as $index => $tab) {
$id = 'home-tab-' . $index;
$href = add_query_arg('index', $index, $base);
$label = esc_html($tab['title']);
$nav .= '<li' . ($index === $active ? ' class="active"' : '') . '><a href="' . esc_url($href) . '" data-forum-tab="#' . $id . '">' . $label . '</a></li>';
$panes .= '<div class="forum-tab-pane tab-pane fade' . ($index === $active ? ' in active' : '') . '" id="' . $id . '" data-href="' . esc_url($href) . '">';
if ($index === $active) {
$panes .= HomeContent::render($tab, $index);
} else {
$panes .= '<div class="post_ajax_loader">' . self::placeholder($tab['type'] === 'plate' ? 'plate' : 'posts', $tab['type'] === 'plate' ? 2 : 4) . '</div>';
}
$panes .= '</div>';
}
echo '<div class="fixed-wrap forum-home-tab">';
echo '<div class="fixed-wrap-nav jio-panel affix-header-sm" offset-top="-8"><ul class="list-inline scroll-x mini-scrollbar tab-nav-theme">' . $nav . '</ul></div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="fixed-wrap-content">';
\Jio\WidgetSystem\WidgetAreas::render('forum_home_top_content');
echo '<div class="tab-content forum-main-tab-content">' . $panes . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
\Jio\WidgetSystem\WidgetAreas::render('forum_home_bottom_content');
echo '</div></div>';
}
public static function placeholder(string $type, int $count): string {
if ($type === 'plate') {
$card = '<div class="plate-card" style="margin-right: 10px;"><div class="plate-thumb"><div style="width: 100%; height: 100%;" class="placeholder radius"></div></div><div style="height: 30px;" class="placeholder k2 mt10"></div><div class="placeholder s1 mt20"></div></div>';
$item = '<div class="plate-item"><div class="plate-thumb"><div style="height: 100%;" class="placeholder radius"></div></div><div class="item-info"><div class="placeholder k1"></div><div style="height: 30px;" class="placeholder k2 hide-sm mt10"></div><div class="placeholder s1 mt10"></div></div></div>';
$block = '<div class="panel-plate mb20"><i class="placeholder s1" style="height: 20px;"></i><div class="flex scroll-plate">' . str_repeat($card, 4) . '</div></div>'
. '<div class="panel-plate mb20"><i style="height: 20px;" class="placeholder s1"></i><div class="plate-lists">' . str_repeat($item, 4) . '</div></div>';
return str_repeat($block, max(1, $count));
}
$post = '<div class="forum-posts detail alone"><div class="user-info flex ac mb6"><div class="avatar-img"><div style="width: 100%;height: 100%;" class="placeholder radius"></div></div><div class="flex1 ml10"><div class="placeholder k1" style="height: 15px;"></div><div class="placeholder s1 mt6"></div></div></div><h2 style="height: 40px;margin-top: 0;" class="forum-title placeholder k2"></h2><div class="flex ac jsb mt10"><i><i class="placeholder s1"></i><i class="placeholder s1 ml10"></i></i><i><i class="placeholder s1 ml10"></i></i></div></div>';
return str_repeat($post, max(1, $count));
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
namespace Jio\Forum;
defined('ABSPATH') || exit;
final class HomeView {
/** Forum home: fluid widgets, tab content column and sidebar. */
public static function render(): void {
if (!Core::enabled()) {
status_header(404);
include get_404_template();
return;
}
get_header();
echo '<main id="forum" class="jio-forum forum-home">';
\Jio\WidgetSystem\WidgetAreas::render('forum_home_top_fluid', 'fluid-widget-wrap');
echo '<div class="container"><div class="content-wrap"><div class="content-layout">';
echo '<div class="forum-home__main">';
HomeTabs::render();
echo '</div></div></div>';
self::sidebar();
echo '</div>';
\Jio\WidgetSystem\WidgetAreas::render('forum_home_bottom_fluid', 'fluid-widget-wrap');
echo '</main>';
get_footer();
}
private static function sidebar(): void {
$plate_name = (string) jio_get_setting('jio_forum_plate_name', '版块') ?: '版块';
echo '<div class="sidebar forum-home__sidebar">';
if (is_user_logged_in()) {
echo '<div class="jio-widget-box"><a class="but jb-blue btn-block" href="' . esc_url(Core::edit_url()) . '"><i class="fa fas fa-edit mr6"></i>发布' . esc_html((string) jio_get_setting('jio_forum_post_name', '帖子')) . '</a>';
if (jio_get_setting('jio_forum_user_plates', true)) {
echo '<button type="button" class="but hollow c-blue btn-block mt10" data-forum-open="plate">创建' . esc_html($plate_name) . '</button>';
self::plate_dialog();
}
echo '</div>';
} else {
echo '<div class="jio-widget-box"><a class="but jb-blue btn-block" href="' . esc_url(jio_get_login_url(Core::edit_url())) . '"><i class="fa fas fa-sign-in-alt mr6"></i>登录后发帖</a></div>';
}
\Jio\WidgetSystem\WidgetAreas::render('forum_home_sidebar');
$plates = get_posts(['post_type' => Core::PLATE, 'post_status' => 'publish', 'posts_per_page' => 8, 'orderby' => 'modified']);
if ($plates) {
PlateStats::prime(wp_list_pluck($plates, 'ID'));
echo '<div class="jio-widget-box"><h4 class="title-theme mb10">热门' . esc_html($plate_name) . '</h4>';
foreach ($plates as $plate) {
$stats = PlateStats::get($plate->ID);
echo '<a class="jio-forum__widget-link" href="' . esc_url(get_permalink($plate)) . '"><span class="text-ellipsis">' . esc_html($plate->post_title) . '</span><span>' . esc_html(jio_cut_count($stats['posts'])) . '</span></a>';
}
echo '</div>';
}
echo '</div>';
}
private static function plate_dialog(): void {
echo '<dialog class="jio-forum__dialog" data-forum-dialog="plate"><form data-forum-form="plate"><h2>创建版块</h2>'
. '<label>名称<input name="title" required maxlength="80"></label>'
. '<label>介绍<textarea name="content" rows="5"></textarea></label>'
. '<label>分类<select name="category_id"><option value="0">未分类</option>';
$terms = get_terms(['taxonomy' => Core::CATEGORY, 'hide_empty' => false]);
foreach (is_array($terms) ? $terms : [] as $term) {
echo '<option value="' . (int) $term->term_id . '">' . esc_html($term->name) . '</option>';
}
echo '</select></label><p class="jio-forum__message" role="status"></p><div class="jio-forum__dialog-actions"><button type="button" data-forum-close>取消</button><button type="submit" class="jio-forum__primary">提交</button></div></form></dialog>';
}
}
+11 -1
View File
@@ -60,7 +60,7 @@ final class Integration {
public static function sort_forum_posts(array $clauses, \WP_Query $query): array {
$mode = sanitize_key((string) $query->get('jio_forum_list'));
if (!in_array($mode, ['latest', 'popular', 'question', 'vote', 'essence', 'follow'], true)) return $clauses;
if (!in_array($mode, ['latest', 'date', 'modified', 'views', 'score', 'popular', 'last_reply', 'question', 'vote', 'essence', 'follow'], true)) return $clauses;
global $wpdb;
$clauses['join'] .= " LEFT JOIN {$wpdb->postmeta} AS jio_forum_pin ON ({$wpdb->posts}.ID = jio_forum_pin.post_id AND jio_forum_pin.meta_key = '_jio_forum_pinned')";
$pin = 'CAST(COALESCE(jio_forum_pin.meta_value, 0) AS SIGNED) DESC';
@@ -71,6 +71,16 @@ final class Integration {
$clauses['join'] .= " LEFT JOIN {$wpdb->postmeta} AS jio_forum_views ON ({$wpdb->posts}.ID = jio_forum_views.post_id AND jio_forum_views.meta_key = '_jio_forum_views')";
$rank = "({$wpdb->posts}.comment_count * 2 + CAST(COALESCE(jio_forum_up.meta_value, 0) AS SIGNED) * 10 - CAST(COALESCE(jio_forum_down.meta_value, 0) AS SIGNED) * 15 + CAST(COALESCE(jio_forum_fav.meta_value, 0) AS SIGNED) * 10 + CAST(COALESCE(jio_forum_views.meta_value, 0) AS SIGNED))";
$clauses['orderby'] = $pin . ', ' . $rank . ' DESC, ' . $wpdb->posts . '.post_date DESC';
} elseif ($mode === 'views' || $mode === 'score') {
$key = $mode === 'views' ? '_jio_forum_views' : '_jio_forum_score';
$alias = 'jio_forum_sort_' . $mode;
$clauses['join'] .= " LEFT JOIN {$wpdb->postmeta} AS $alias ON ({$wpdb->posts}.ID = $alias.post_id AND $alias.meta_key = '$key')";
$clauses['orderby'] = $pin . ', CAST(COALESCE(' . $alias . '.meta_value, 0) AS SIGNED) DESC, ' . $wpdb->posts . '.post_date DESC';
} elseif ($mode === 'modified') {
$clauses['orderby'] = $pin . ', ' . $wpdb->posts . '.post_modified DESC';
} elseif ($mode === 'last_reply') {
$clauses['join'] .= " LEFT JOIN (SELECT comment_post_ID AS jio_reply_post_id, MAX(comment_date) AS jio_reply_last FROM {$wpdb->comments} WHERE comment_approved = '1' GROUP BY comment_post_ID) AS jio_forum_last ON ({$wpdb->posts}.ID = jio_forum_last.jio_reply_post_id)";
$clauses['orderby'] = $pin . ', COALESCE(jio_forum_last.jio_reply_last, ' . $wpdb->posts . '.post_date) DESC';
} else {
$clauses['orderby'] = $pin . ', ' . $wpdb->posts . '.post_date DESC';
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace Jio\Forum;
defined('ABSPATH') || exit;
final class PlateStats {
/** @var array<int, array{posts:int, today:int}> */
private static array $stats = [];
/** Load post totals and today's posts for many plates with one grouped query. */
public static function prime(array $plate_ids): void {
$ids = array_values(array_diff(array_unique(array_filter(array_map('absint', $plate_ids))), array_keys(self::$stats)));
if (!$ids) return;
foreach ($ids as $id) {
self::$stats[$id] = ['posts' => 0, 'today' => 0];
}
global $wpdb;
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
$today = wp_date('Y-m-d 00:00:00');
$sql = "SELECT CAST(pm.meta_value AS UNSIGNED) AS plate_id, COUNT(*) AS total, SUM(p.post_date >= %s) AS today
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_jio_forum_plate_id'
WHERE p.post_type = %s AND p.post_status = 'publish' AND CAST(pm.meta_value AS UNSIGNED) IN ($placeholders)
GROUP BY plate_id";
$rows = $wpdb->get_results($wpdb->prepare($sql, array_merge([$today, Core::POST], $ids))); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
foreach ((array) $rows as $row) {
self::$stats[(int) $row->plate_id] = ['posts' => (int) $row->total, 'today' => (int) $row->today];
}
}
/** @return array{posts:int, today:int} */
public static function get(int $plate_id): array {
if (!isset(self::$stats[$plate_id])) self::prime([$plate_id]);
return self::$stats[$plate_id] ?? ['posts' => 0, 'today' => 0];
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace Jio\Forum;
defined('ABSPATH') || exit;
final class QuickPost {
/** Inline "publish now" box shown on the configured home tab. */
public static function render(): string {
$user_id = get_current_user_id();
if (!$user_id) {
return '<div class="ajax-item-header jio-widget-box quick-posts-box mb20"><p class="muted-2-color">登录后即可快速发布' . esc_html(self::post_name()) . '</p><a class="but jb-blue padding-lg" href="' . esc_url(jio_get_login_url(Core::home_url())) . '"><i class="fa fas fa-sign-in-alt mr6"></i>登录</a></div>';
}
$plates = get_posts(['post_type' => Core::PLATE, 'post_status' => 'publish', 'posts_per_page' => 100, 'orderby' => 'title', 'order' => 'ASC']);
$options = '';
foreach ($plates as $plate) {
$options .= '<option value="' . (int) $plate->ID . '">' . esc_html($plate->post_title) . '</option>';
}
if ($options === '') return '';
return '<div class="ajax-item-header jio-widget-box quick-posts-box mb20">'
. '<form data-forum-form="post" class="quick-posts-form"><h4 class="title-theme mb10">快速发布' . esc_html(self::post_name()) . '</h4>'
. '<input class="form-control mb10" type="text" name="title" required maxlength="120" placeholder="输入标题" aria-label="标题">'
. '<textarea class="form-control mb10" name="content" rows="3" required placeholder="说点什么…" aria-label="正文"></textarea>'
. '<div class="flex ac jsb">'
. '<select class="form-control w-auto" name="plate_id" required aria-label="' . esc_attr(self::plate_name()) . '"><option value="">选择' . esc_html(self::plate_name()) . '</option>' . $options . '</select>'
. '<button type="submit" class="but jb-blue"><i class="fa fas fa-paper-plane mr6"></i>发布</button>'
. '</div><p class="jio-forum__message mt6 muted-2-color" role="status"></p></form></div>';
}
private static function post_name(): string {
return (string) jio_get_setting('jio_forum_post_name', '帖子') ?: '帖子';
}
private static function plate_name(): string {
return (string) jio_get_setting('jio_forum_plate_name', '版块') ?: '版块';
}
}
+33 -121
View File
@@ -24,10 +24,6 @@ final class View {
echo '<aside class="jio-forum__aside">';
if (is_user_logged_in()) {
echo '<a class="jio-forum__new" href="' . esc_url(Core::edit_url()) . '">发布帖子</a>';
if ($context === 'home' && jio_get_setting('jio_forum_user_plates', true)) {
echo '<button type="button" class="jio-forum__action jio-forum__create-plate" data-forum-open="plate">创建版块</button>';
echo '<dialog class="jio-forum__dialog" data-forum-dialog="plate"><form data-forum-form="plate"><h2>创建版块</h2><label>名称<input name="title" required maxlength="80"></label><label>介绍<textarea name="content" rows="5"></textarea></label><p class="jio-forum__message" role="status"></p><div class="jio-forum__dialog-actions"><button type="button" data-forum-close>取消</button><button type="submit" class="jio-forum__primary">提交</button></div></form></dialog>';
}
} else {
echo '<a class="jio-forum__new" href="' . esc_url(jio_get_login_url(Core::edit_url())) . '">登录后发帖</a>';
}
@@ -44,37 +40,7 @@ final class View {
}
public static function home(): void {
self::open('论坛', 'home');
\Jio\WidgetSystem\WidgetAreas::render('forum_home_top_fluid');
echo '<div class="jio-forum__layout"><div class="jio-forum__main">';
$name = (string) jio_get_setting('jio_forum_forum_name', '论坛');
echo '<header class="jio-forum__hero"><h1>' . esc_html($name) . '</h1></header>';
if (Access::is_admin(get_current_user_id())) {
$pending = get_posts(['post_type' => Core::PLATE, 'post_status' => 'pending', 'posts_per_page' => 20]);
if ($pending) {
echo '<section class="jio-forum__pending"><h2>待审核版块</h2>';
foreach ($pending as $plate) {
echo '<div><span>' . esc_html($plate->post_title) . '</span><button type="button" data-forum-action="plate-audit" data-id="' . (int) $plate->ID . '">通过审核</button></div>';
}
echo '</section>';
}
self::term_management();
}
self::tabs();
$categories = get_terms(['taxonomy' => Core::CATEGORY, 'hide_empty' => false]);
if (is_array($categories) && $categories) {
echo '<div class="jio-forum__chips">';
foreach ($categories as $category) {
echo '<a href="' . esc_url(get_term_link($category)) . '">' . esc_html($category->name) . '</a>';
}
echo '</div>';
}
self::posts(['post_type' => Core::POST, 'post_status' => 'publish'], 'home');
echo '</div>';
self::sidebar('home');
echo '</div>';
\Jio\WidgetSystem\WidgetAreas::render('forum_home_bottom_fluid');
self::close();
HomeView::render();
}
public static function listing(): void {
@@ -90,7 +56,7 @@ final class View {
if ($plate) {
echo '<div class="jio-forum__description">' . wp_kses_post(wpautop($plate->post_excerpt ?: $plate->post_content)) . '</div>';
echo '<p class="jio-forum__stats">' . (int) get_post_meta($plate->ID, '_jio_forum_follow_count', true) . ' 人关注</p>';
self::action('follow', $plate->ID, Access::follows(get_current_user_id(), $plate->ID) ? '已关注' : '关注版块');
echo Cards::follow_button($plate->ID, 'but c-blue'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
if (Access::is_moderator(get_current_user_id(), $plate->ID)) {
self::plate_editor($plate);
}
@@ -128,14 +94,12 @@ final class View {
self::close();
}
/** Listing pages (plate / taxonomy) use a fixed, always-available tab set. */
public static function tabs(): void {
$tabs = ['latest' => '最新', 'popular' => '热门', 'essence' => '精华', 'question' => '问答', 'vote' => '投票', 'plates' => '版块', 'follow' => '已关注'];
$enabled = (array) jio_get_setting('jio_forum_home_tabs', array_keys($tabs));
$tabs = ['latest' => '最新', 'popular' => '热门', 'essence' => '精华', 'question' => '问答', 'vote' => '投票'];
$base_url = is_singular(Core::PLATE) ? get_permalink(get_queried_object_id()) : (is_tax([Core::CATEGORY, Core::TOPIC, Core::TAG]) ? get_term_link(get_queried_object()) : Core::home_url());
echo '<nav class="jio-forum__tabs" aria-label="论坛栏目">';
foreach ($tabs as $key => $label) {
if (!in_array($key, $enabled, true)) continue;
if ($key === 'follow' && !is_user_logged_in()) continue;
$url = add_query_arg('forum_view', $key, $base_url);
$selected = (string) ($_GET['forum_view'] ?? 'latest') === $key;
echo '<a class="' . ($selected ? 'is-active' : '') . '" href="' . esc_url($url) . '">' . esc_html($label) . '</a>';
@@ -145,13 +109,8 @@ final class View {
public static function posts(array $args, string $context = 'home'): void {
$view = sanitize_key((string) ($_GET['forum_view'] ?? 'latest'));
if ($view === 'plates') {
self::plates([]);
return;
}
if ($view === 'follow') {
$ids = self::user_marked_ids(get_current_user_id(), '_jio_forum_follow_');
$args['meta_query'][] = ['key' => '_jio_forum_plate_id', 'value' => $ids ?: [0], 'compare' => 'IN'];
if (!in_array($view, ['latest', 'popular', 'essence', 'question', 'vote'], true)) {
$view = 'latest';
}
if (in_array($view, ['question', 'vote'], true)) {
$args['meta_query'][] = ['key' => '_jio_forum_type', 'value' => $view];
@@ -163,77 +122,43 @@ final class View {
$args['paged'] = max(1, (int) get_query_var('paged', 1));
$args['orderby'] = 'date';
$args['order'] = 'DESC';
$args['jio_forum_list'] = in_array($view, ['latest', 'popular', 'question', 'vote', 'essence', 'follow'], true) ? $view : 'latest';
$args['jio_forum_list'] = $view;
$query = new \WP_Query($args);
\Jio\WidgetSystem\WidgetAreas::render('forum_' . $context . '_top_content');
echo '<div class="jio-forum__list">';
if (!$query->have_posts()) {
echo '<p class="jio-forum__empty">暂无帖子</p>';
echo '<div class="jio-forum__list">' . jio_get_null('暂无帖子') . '</div>';
} else {
Cards::prime($query->posts);
echo '<div class="jio-forum__list">';
foreach ($query->posts as $post) {
echo Cards::post_detail($post, 'ajax-item'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo '</div>';
}
while ($query->have_posts()) {
$query->the_post();
self::post_card(get_post());
}
wp_reset_postdata();
echo '</div>';
self::pagination($query);
\Jio\WidgetSystem\WidgetAreas::render('forum_' . $context . '_bottom_content');
}
public static function plates(array $args): void {
$query = new \WP_Query(array_merge(['post_type' => Core::PLATE, 'post_status' => 'publish', 'posts_per_page' => 20], $args));
echo '<div class="jio-forum__plates">';
while ($query->have_posts()) {
$query->the_post();
$plate = get_post();
self::plate_card($plate);
if (!$query->have_posts()) {
echo jio_get_null('暂无版块'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return;
}
PlateStats::prime($query->posts ? wp_list_pluck($query->posts, 'ID') : []);
echo '<div class="plate-lists">';
foreach ($query->posts as $plate) {
echo Cards::plate_item($plate); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
wp_reset_postdata();
echo '</div>';
}
public static function plate_card(\WP_Post $plate): void {
$url = get_permalink($plate);
echo '<article class="jio-forum__plate"><a class="jio-forum__plate-cover" href="' . esc_url($url) . '">';
if (jio_get_setting('jio_forum_show_plate_cover', true) && has_post_thumbnail($plate)) {
echo get_the_post_thumbnail($plate, 'jio-card');
}
echo '</a><div><h2><a href="' . esc_url($url) . '">' . esc_html($plate->post_title) . '</a></h2><p>' . esc_html(wp_trim_words(wp_strip_all_tags($plate->post_excerpt ?: $plate->post_content), 22)) . '</p><small>' . (int) get_post_meta($plate->ID, '_jio_forum_follow_count', true) . ' 人关注</small></div></article>';
echo Cards::plate_item($plate); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public static function post_card(\WP_Post $post): void {
$plate_id = (int) get_post_meta($post->ID, '_jio_forum_plate_id', true);
$type = (string) get_post_meta($post->ID, '_jio_forum_type', true);
echo '<article class="jio-forum__post"><a class="jio-forum__avatar" href="' . esc_url(get_author_posts_url((int) $post->post_author)) . '">' . get_avatar((int) $post->post_author, 44) . '</a><div class="jio-forum__post-body">';
echo '<div class="jio-forum__post-top"><span>' . esc_html(get_the_author_meta('display_name', (int) $post->post_author)) . '</span><time datetime="' . esc_attr(get_the_date(DATE_W3C, $post)) . '">' . esc_html(get_the_date('', $post)) . '</time></div>';
$url = $post->post_status === 'publish' ? get_permalink($post) : Core::edit_url($post->ID);
echo '<h2><a href="' . esc_url($url) . '">' . esc_html($post->post_title) . '</a></h2>';
$excerpt = Access::can_view(get_current_user_id(), $post->ID) ? wp_trim_words(wp_strip_all_tags($post->post_excerpt ?: $post->post_content), 30) : '内容已隐藏';
echo '<p>' . esc_html($excerpt) . '</p>';
echo '<div class="jio-forum__post-meta">';
if (get_post_meta($post->ID, '_jio_forum_pinned', true)) {
echo '<span class="jio-forum__badge">置顶</span>';
}
if ($type !== '') {
echo '<span class="jio-forum__badge">' . esc_html($type === 'question' ? '问答' : '投票') . '</span>';
}
if ($type === 'question' && get_post_meta($post->ID, '_jio_forum_adopted_comment', true)) {
echo '<span class="jio-forum__badge">已解决</span>';
}
if (get_post_meta($post->ID, '_jio_forum_essence', true)) {
echo '<span class="jio-forum__badge">精华</span>';
}
if ($plate_id > 0) {
echo '<a href="' . esc_url(get_permalink($plate_id)) . '">' . esc_html(get_the_title($plate_id)) . '</a>';
}
echo '<span>' . (int) get_post_meta($post->ID, '_jio_forum_views', true) . ' 阅读</span>';
echo '<span>' . (int) $post->comment_count . ' 回复</span>';
echo '<span>' . (int) get_post_meta($post->ID, '_jio_forum_score', true) . ' 评分</span>';
echo '<span>' . (int) get_post_meta($post->ID, '_jio_forum_favorite_count', true) . ' 收藏</span></div></div></article>';
$hot = get_comments(['post_id' => $post->ID, 'status' => 'approve', 'number' => 1, 'meta_key' => '_jio_forum_hot', 'meta_value' => '1']);
if ($hot) {
echo '<div class="jio-forum__hot-reply">热门回复:' . esc_html(wp_trim_words(wp_strip_all_tags($hot[0]->comment_content), 25)) . '</div>';
}
echo Cards::post_detail($post); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public static function replies(int $post_id): void {
@@ -278,18 +203,6 @@ final class View {
echo '</section>';
}
private static function term_management(): void {
echo '<section class="jio-forum__term-management"><h2>分类与话题</h2><form data-forum-form="term"><label>类型<select name="type"><option value="category">版块分类</option><option value="topic">论坛话题</option><option value="tag">论坛标签</option></select></label><label>名称<input name="name" required maxlength="60"></label><p class="jio-forum__message" role="status"></p><button type="submit" class="jio-forum__action">添加</button></form>';
foreach (['category' => Core::CATEGORY, 'topic' => Core::TOPIC, 'tag' => Core::TAG] as $label => $taxonomy) {
$terms = get_terms(['taxonomy' => $taxonomy, 'hide_empty' => false, 'number' => 30]);
if (!is_array($terms)) continue;
foreach ($terms as $term) {
echo '<div class="jio-forum__term-row"><span>' . esc_html($term->name) . '</span><button type="button" data-forum-action="term-edit" data-id="' . (int) $term->term_id . '">编辑</button><button type="button" data-forum-action="term-delete" data-id="' . (int) $term->term_id . '">删除</button></div>';
}
}
echo '</section>';
}
private static function plate_editor(\WP_Post $plate): void {
$id = $plate->ID;
echo '<button type="button" class="jio-forum__action" data-forum-open="plate-edit-' . $id . '">编辑版块</button>';
@@ -371,17 +284,16 @@ final class View {
$query = new \WP_Query($args);
echo '<div class="jio-forum__list">';
if (!$query->have_posts()) echo '<p class="jio-forum__empty">暂无内容</p>';
while ($query->have_posts()) {
$query->the_post();
$post = get_post();
if ($post->post_type === Core::POST) self::post_card($post);
else {
$url = $post->post_status === 'publish' ? get_permalink($post) : '';
echo '<div class="jio-forum__user-row">' . ($url ? '<a href="' . esc_url($url) . '">' : '<span>') . esc_html($post->post_title) . ($url ? '</a>' : '</span>') . ($url ? '' : '<small>待审核</small>') . '</div>';
if ($owner && !$url) self::plate_editor($post);
Cards::prime($query->posts);
foreach ($query->posts as $post) {
if ($post->post_type === Core::POST) {
echo Cards::post_detail($post); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
continue;
}
$url = $post->post_status === 'publish' ? get_permalink($post) : '';
echo '<div class="jio-forum__user-row">' . ($url ? '<a href="' . esc_url($url) . '">' : '<span>') . esc_html($post->post_title) . ($url ? '</a>' : '</span>') . ($url ? '' : '<small>待审核</small>') . '</div>';
if ($owner && !$url) self::plate_editor($post);
}
wp_reset_postdata();
echo '</div></div>';
}
}