feat(forum): rebuild plate, post and compose pages

Match the reference layouts for the remaining forum routes: plate pages
get the blurred header card, moderator row and the affix tab bar with
per-tab filtering, topping posts and moderator-only pending/trash tabs;
post pages get the article shell with breadcrumbs, cover, meta row,
vote box, tag row and footer actions; compose pages get the reference
editor plus settings sidebar (submit/draft, status, access, plate,
topic, tag, type, vote) with a cover picker. Saving now honours draft
state and the featured image through the REST contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-11-11 16:17:55 +08:00
1 parent eceddb5f7d
commit 5d3358d844
13 files changed
+891 -199

No files matched your search

+190
View File
@@ -0,0 +1,190 @@
<?php
namespace Jio\Forum;
defined('ABSPATH') || exit;
/** Forum post detail page (reference article layout). */
final class PostView {
public static function render(): void {
$post = get_post();
if (!$post instanceof \WP_Post || $post->post_type !== Core::POST) {
return;
}
$id = (int) $post->ID;
$plate_id = (int) get_post_meta($id, '_jio_forum_plate_id', true);
$type = (string) get_post_meta($id, '_jio_forum_type', true);
$user_id = get_current_user_id();
$can_view = Access::can_view($user_id, $id);
echo '<main id="forum" class="jio-forum forum-post"><div class="container"><div class="content-wrap"><div class="content-layout">';
echo '<div class="fixed-wrap single-wrap"><div class="fixed-wrap-content">';
\Jio\WidgetSystem\WidgetAreas::render('forum_post_top_fluid');
do_action('bbs_posts_page_content_top');
if ($can_view) echo self::cover($post);
echo self::breadcrumbs($post);
echo '<article id="article" class="article zib-widget forum-article relative">';
echo '<div class="article-header clearfix">' . self::title($post) . self::meta($post) . '</div>';
echo self::content($post, $can_view);
echo self::footer($post);
echo '</article>';
if ($can_view) {
self::comments($id);
}
do_action('bbs_posts_page_content_bottom');
\Jio\WidgetSystem\WidgetAreas::render('forum_post_bottom_fluid');
echo '</div></div></div>';
if (jio_is_show_sidebar()) {
echo '<div class="sidebar">';
\Jio\WidgetSystem\WidgetAreas::render('forum_post_sidebar');
echo '</div>';
}
echo '</div></div></main>';
}
private static function breadcrumbs(\WP_Post $post): string {
$plate_id = (int) get_post_meta($post->ID, '_jio_forum_plate_id', true);
$icon = '<i class="fa fas fa-map-marker-alt"></i> ';
$html = '<ul class="breadcrumb">';
$html .= '<li><a href="' . esc_url(home_url('/')) . '">' . $icon . '首页</a></li>';
$html .= '<li><a href="' . esc_url(Core::home_url()) . '">论坛</a></li>';
if ($plate_id > 0) {
$terms = wp_get_post_terms($plate_id, Core::CATEGORY);
if (is_array($terms) && $terms) {
$html .= '<li><a href="' . esc_url(get_term_link($terms[0])) . '">' . esc_html($terms[0]->name) . '</a></li>';
}
$html .= '<li><a href="' . esc_url(get_permalink($plate_id)) . '">' . esc_html(get_the_title($plate_id)) . '</a></li>';
}
$html .= '<li class="active">' . esc_html(wp_trim_words($post->post_title, 8)) . '</li>';
return $html . '</ul>';
}
private static function cover(\WP_Post $post): string {
if (!has_post_thumbnail($post)) return '';
return '<div class="forum-single-cover mb20"><div class="graphic imgbox-container" style="padding-bottom:45%">' . get_the_post_thumbnail($post, 'large', ['class' => 'fit-cover']) . '</div></div>';
}
private static function title(\WP_Post $post): string {
return Cards::title($post, 'article-title', '', true);
}
private static function meta(\WP_Post $post): string {
$author = (int) $post->post_author;
$name = function_exists('jio_get_user_name') ? jio_get_user_name(['id' => $author, 'class' => '', 'name_class' => 'focus-color']) : esc_html((string) get_the_author_meta('display_name', $author));
$left = '<div class="meta-left">' . $name
. '<span class="icon-spot"><span title="' . esc_attr(get_the_date('Y-m-d H:i:s', $post)) . '">' . esc_html(Cards::ago($post->post_date)) . '</span></span></div>';
$right = '<div class="meta-right">'
. '<item class="item item-view"><i class="fa far fa-eye mr3"></i>' . esc_html(jio_cut_count((int) get_post_meta($post->ID, '_jio_forum_views', true))) . '</item>'
. '<a class="item item-comment" href="#comments"><i class="fa far fa-comment mr3"></i>' . esc_html(jio_cut_count((int) $post->comment_count)) . '</a>'
. '</div>';
return '<div class="flex ac jsb forum-article-meta">' . $left . $right . '</div>';
}
private static function content(\WP_Post $post, bool $can_view): string {
$id = (int) $post->ID;
if (!$can_view) {
return '<div class="theme-box wp-posts-content"><div class="theme-box">' . self::locked($post) . '</div></div>';
}
$html = '<div class="theme-box wp-posts-content">' . apply_filters('the_content', $post->post_content) . '</div>';
$type = (string) get_post_meta($id, '_jio_forum_type', true);
if ($type === 'vote') {
$html .= self::vote($post);
}
return $html;
}
private static function locked(\WP_Post $post): string {
$id = (int) $post->ID;
$plate_id = (int) get_post_meta($id, '_jio_forum_plate_id', true);
$user_id = get_current_user_id();
$html = jio_get_null('抱歉!您暂无查看此内容的权限', 10, 'null-post.svg');
if ($user_id <= 0) {
return $html . '<p class="text-center"><a class="but jb-blue padding-lg" href="' . esc_url(jio_get_login_url(get_permalink($post))) . '"><i class="fa fas fa-sign-in-alt mr6"></i>登录后查看</a></p>';
}
$plate_rule = (string) get_post_meta($plate_id, '_jio_forum_view_limit', true);
$post_rule = (string) get_post_meta($id, '_jio_forum_view_limit', true);
$action = '';
if (in_array($plate_rule, ['points', 'pay'], true) && !Commerce::has_plate_entitlement($user_id, $plate_id)) {
$action = '<button type="button" class="but jb-blue padding-lg" data-forum-action="plate-unlock" data-id="' . $plate_id . '"><i class="fa fas fa-unlock-alt mr6"></i>解锁版块</button>';
} elseif (($plate_rule === 'follow' || $post_rule === 'follow') && !Access::follows($user_id, $plate_id)) {
$action = Cards::follow_button($plate_id, 'but jb-blue padding-lg');
} elseif (in_array($post_rule, ['points', 'pay'], true)) {
$action = '<button type="button" class="but jb-blue padding-lg" data-forum-action="unlock" data-id="' . $id . '"><i class="fa fas fa-unlock-alt mr6"></i>解锁帖子</button>';
}
return $html . ($action !== '' ? '<p class="text-center">' . $action . '</p>' : '');
}
private static function vote(\WP_Post $post): string {
$id = (int) $post->ID;
$options = (array) get_post_meta($id, '_jio_forum_vote_options', true);
if (!$options) return '';
$counts = (array) get_post_meta($id, '_jio_forum_vote_counts', true);
$total = (int) get_post_meta($id, '_jio_forum_vote_total', true);
$vote_type = (string) get_post_meta($id, '_jio_forum_vote_type', true);
$deadline = (int) get_post_meta($id, '_jio_forum_vote_deadline', true);
$expired = $deadline > 0 && current_time('timestamp') > $deadline;
$display = (string) jio_get_setting('jio_forum_vote_number_type', 'percentage');
$voted = get_current_user_id() > 0 && get_user_meta(get_current_user_id(), '_jio_forum_vote_' . $id, true) !== '';
$html = '<form class="vote-box forum-vote zib-widget" data-forum-form="vote" data-id="' . $id . '"><h2 class="mb20">' . esc_html((string) get_post_meta($id, '_jio_forum_vote_title', true) ?: '参与投票') . '</h2>';
foreach ($options as $index => $option) {
$count = (int) ($counts[$index] ?? 0);
$percent = $total > 0 ? (int) round(100 * $count / $total) : 0;
$html .= '<label class="vote-item"><input type="' . ($vote_type === 'multiple' ? 'checkbox' : 'radio') . '" name="' . ($vote_type === 'multiple' ? 'option[]' : 'option') . '" value="' . (int) $index . '"' . disabled($voted || $expired, true, false) . '><span class="vote-title">' . esc_html((string) $option) . '</span>';
if ($display !== 'hide') {
$html .= '<span class="vote-count em09 muted-2-color">' . esc_html($display === 'number' ? (string) $count : $percent . '%') . '</span>';
}
$html .= '<span class="vote-progress" style="width:' . $percent . '%"></span></label>';
}
$html .= '<p class="jio-forum__message" role="status"></p>';
if ($voted) $html .= '<p class="text-center muted-2-color">已投票</p>';
elseif ($expired) $html .= '<p class="text-center muted-2-color">投票已截止</p>';
else $html .= '<button type="submit" class="but jb-blue padding-lg"><i class="fa fas fa-check mr6"></i>提交投票</button>';
return $html . '</form>';
}
private static function footer(\WP_Post $post): string {
$id = (int) $post->ID;
$user_id = get_current_user_id();
$plate_id = (int) get_post_meta($id, '_jio_forum_plate_id', true);
$html = '<div class="article-tags mb20 em09">';
foreach (is_array($terms = wp_get_post_terms($id, Core::TOPIC)) ? $terms : [] as $term) {
$html .= '<a class="but radius mm3 c-red" href="' . esc_url(get_term_link($term)) . '"><i class="fa fas fa-hashtag mr6"></i>' . esc_html($term->name) . '</a>';
}
foreach (is_array($tags = wp_get_post_terms($id, Core::TAG)) ? $tags : [] as $term) {
$html .= '<a class="but radius mm3" href="' . esc_url(get_term_link($term)) . '"><i class="fa fas fa-tag mr6"></i>' . esc_html($term->name) . '</a>';
}
$html .= '</div>';
$html .= '<div class="forum-article-footer flex ac jsb mb10 footer-actions"><div class="left flex ac">';
if (jio_get_setting('jio_forum_score_enabled', true)) {
$score = (int) get_post_meta($id, '_jio_forum_score', true);
$html .= '<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(jio_cut_count($score)) . '</text></button>';
$html .= '<button type="button" class="item btn-score" data-forum-action="score" data-direction="down" data-id="' . $id . '"><i class="fa far fa-thumbs-down"></i></button>';
}
$html .= '</div><div class="right flex ac">';
if (function_exists('jio_get_post_share_btn')) $html .= jio_get_post_share_btn($post, 'item', true, false);
$html .= '<button type="button" class="item" data-forum-action="favorite" data-id="' . $id . '"><i class="fa far fa-bookmark"></i><text>收藏</text></button>';
$html .= '<button type="button" class="item" data-forum-action="report" data-id="' . $id . '"><i class="fa far fa-flag"></i><text>举报</text></button>';
if (Access::can_manage_post($user_id, $id)) {
$html .= '<a class="item" href="' . esc_url(Core::edit_url($id)) . '"><i class="fa far fa-edit"></i><text>编辑</text></a>';
$html .= '<button type="button" class="item" data-forum-action="delete" data-id="' . $id . '"><i class="fa far fa-trash-alt"></i><text>删除</text></button>';
}
$html .= '</div></div>';
$html .= '<div class="single-footer text-center mb20">';
if (Access::is_moderator($user_id, $plate_id)) {
foreach ([['essence', '精华'], ['pin', '置顶'], ['audit', '审核'], ['comments', $post->comment_status === 'open' ? '关闭回复' : '开放回复'], ['move', '移动版块']] as [$action, $label]) {
$html .= '<button type="button" class="but hollow px12 mr6" data-forum-action="' . $action . '" data-id="' . $id . '">' . esc_html($label) . '</button>';
}
}
$html .= '</div>';
if (get_post_meta($id, '_jio_forum_type', true) === 'question' && get_post_meta($id, '_jio_forum_adopted_comment', true)) {
$html .= '<p class="muted-2-color em09 mb20"><i class="fa fas fa-check-circle c-green mr6"></i>已采纳回复</p>';
}
return $html;
}
private static function comments(int $post_id): void {
echo '<div class="theme-box box-body radius8 main-shadow comments-card mb20">';
View::replies($post_id);
echo '</div>';
}
}