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

+198
View File
@@ -0,0 +1,198 @@
<?php
namespace Jio\Forum;
defined('ABSPATH') || exit;
/** Forum listing pages: plate header card, plate tab bar and post lists. */
final class ListingView {
/** Plate page: fluid widgets, blur header card, tab bar, then the list. */
public static function plate(): void {
$plate = get_queried_object();
if (!$plate instanceof \WP_Post || $plate->post_type !== Core::PLATE) {
self::term();
return;
}
self::open($plate->post_title, 'plate', $plate);
\Jio\WidgetSystem\WidgetAreas::render('forum_plate_top_fluid');
echo '<div class="fixed-wrap"><div class="fixed-wrap-content">';
echo Cards::plate_header($plate); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo self::tab_bar($plate); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</div></div>';
self::close('plate');
}
/** Taxonomy archives (plate category / topic / tag). */
public static function term(): void {
$term = get_queried_object();
if (!$term instanceof \WP_Term) {
self::open('论坛', 'term', null);
self::close('term');
return;
}
self::open($term->name, 'term', null, $term);
\Jio\WidgetSystem\WidgetAreas::render('forum_term_top_fluid');
echo '<div class="zib-widget">';
echo self::tab_bar(null, $term); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</div>';
self::close('term');
}
private static function open(string $title, string $context, ?\WP_Post $plate, ?\WP_Term $term = null): void {
echo '<main id="forum" class="jio-forum forum-listing forum-listing--' . esc_attr($context) . '">';
echo '<div class="container"><div class="content-wrap"><div class="content-layout">';
echo '<div class="jio-forum__list-main">';
if ($plate) {
$plate_name = (string) jio_get_setting('jio_forum_plate_name', '版块') ?: '版块';
echo '<nav class="jio-forum__breadcrumbs" aria-label="路径"><a href="' . esc_url(home_url('/')) . '">首页</a><span>/</span><a href="' . esc_url(Core::home_url()) . '">论坛</a><span>/</span><span>' . esc_html($title !== '' ? $title : $plate_name) . '</span></nav>';
} elseif ($term instanceof \WP_Term) {
echo '<div class="zib-widget"><div class="box-body notop"><div class="title-theme">' . esc_html($term->name) . '</div></div>';
if ($term->description !== '') echo '<p class="muted-2-color em09 padding-w10">' . esc_html($term->description) . '</p>';
echo '</div>';
}
}
private static function close(string $context): void {
echo '</div>';
self::sidebar($context);
echo '</div></div>';
\Jio\WidgetSystem\WidgetAreas::render('forum_' . $context . '_bottom_fluid');
echo '</main>';
}
private static function sidebar(string $context): void {
if (!jio_is_show_sidebar()) return;
echo '<div class="sidebar">';
\Jio\WidgetSystem\WidgetAreas::render('forum_' . $context . '_sidebar');
echo '</div>';
}
/** Plate tab bar + panes; the first tab renders server-side, the rest load by `?plate_tab=N`. */
public static function tab_bar(?\WP_Post $plate = null, ?\WP_Term $term = null): string {
$tabs = self::tabs($plate);
if (!$tabs) return '';
$active = self::active_index(count($tabs));
$base = remove_query_arg(['plate_tab', 'fpage']);
if ($term instanceof \WP_Term) $base = get_term_link($term);
$nav = $panes = '';
foreach ($tabs as $index => $tab) {
$id = 'plate-tab-' . $index;
$href = add_query_arg('plate_tab', $index, is_string($base) ? $base : Core::home_url());
$nav .= '<li' . ($index === $active ? ' class="active"' : '') . '><a href="' . esc_url($href) . '" data-forum-tab="#' . $id . '" data-plate-tab="' . $index . '">' . $tab['title'] . '</a></li>';
$panes .= '<div class="forum-tab-pane tab-pane fade' . ($index === $active ? ' in active' : '') . '" id="' . $id . '" data-href="' . esc_url($href) . '">';
$panes .= $index === $active
? self::pane($tab, $plate, $term, $index)
: '<div class="post_ajax_loader">' . HomeTabs::placeholder('posts', 3) . '</div>';
$panes .= '</div>';
}
return '<div class="plate-tab zib-widget"><div class="affix-header-sm" offset-top="7"><ul class="list-inline scroll-x mini-scrollbar tab-nav-theme" role="tablist">' . $nav . '</ul></div><div class="tab-content">' . $panes . '</div></div>';
}
/** @return array<int, array<string, mixed>> */
public static function tabs(?\WP_Post $plate = null): array {
$tabs = [];
$configured = jio_get_setting('jio_forum_plate_tab_items', null);
if (is_array($configured) && $configured) {
foreach ($configured as $row) {
if (!is_array($row)) continue;
$tabs[] = [
'title' => wp_kses_post((string) ($row['title'] ?? '') ?: '栏目'),
'orderby' => sanitize_key((string) ($row['orderby'] ?? 'date')),
'filter' => sanitize_key((string) ($row['filter'] ?? '')),
'style' => sanitize_key((string) ($row['style'] ?? 'detail')),
];
}
} else {
foreach ([
['最新发布', 'date', '', 'detail'],
['最近回复', 'last_reply', '', 'detail'],
['热门', 'views', '', 'detail'],
['精华', 'modified', 'essence', 'detail'],
] as [$title, $orderby, $filter, $style]) {
$tabs[] = ['title' => $title, 'orderby' => $orderby, 'filter' => $filter, 'style' => $style];
}
}
$user_id = get_current_user_id();
if ($plate instanceof \WP_Post && Access::is_moderator($user_id, $plate->ID)) {
$tabs[] = ['title' => '待审核', 'orderby' => 'date', 'filter' => '', 'style' => 'mini', 'status' => 'pending'];
$tabs[] = ['title' => '<i class="fa fas fa-trash-alt mr3"></i>回收站', 'orderby' => 'date', 'filter' => '', 'style' => 'mini', 'status' => 'trash'];
}
return $tabs;
}
public static function active_index(int $count): int {
$raw = $_GET['plate_tab'] ?? 0;
$index = (is_scalar($raw) ? absint($raw) : 0) ?: (int) jio_get_setting('jio_forum_plate_tab_active', 1);
return $index >= 1 && $index <= $count ? $index : 1;
}
private static function pane(array $tab, ?\WP_Post $plate, ?\WP_Term $term, int $index): string {
$page = max(1, is_scalar($_GET['fpage'] ?? 1) ? absint($_GET['fpage'] ?? 1) : 1);
$args = [
'post_type' => Core::POST,
'post_status' => $tab['status'] ?? 'publish',
'posts_per_page' => max(1, min(50, (int) jio_get_setting('jio_forum_page_size', 20))),
'paged' => $page,
'jio_forum_list' => $tab['orderby'],
];
if ($plate instanceof \WP_Post) {
$args['meta_query'][] = ['key' => '_jio_forum_plate_id', 'value' => $plate->ID];
$topping = self::topping($plate->ID, $tab);
if ($topping) {
$args['post__not_in'] = $topping;
}
} elseif ($term instanceof \WP_Term) {
$args['tax_query'][] = ['taxonomy' => $term->taxonomy, 'terms' => [$term->term_id]];
}
if (($tab['filter'] ?? '') === 'essence') {
$args['meta_query'][] = ['key' => '_jio_forum_essence', 'value' => 1];
}
$query = new \WP_Query($args);
$html = '';
if ($plate instanceof \WP_Post && ($tab['status'] ?? '') === '') {
Cards::prime($topping_posts = self::topping_posts($plate->ID, $tab));
foreach ($topping_posts as $post) {
$html .= Cards::post_mini($post, 'ajax-item', true);
}
}
if ($query->have_posts()) {
Cards::prime($query->posts);
foreach ($query->posts as $post) {
$html .= ($tab['style'] ?? 'detail') === 'detail'
? Cards::post_detail($post, 'ajax-item')
: Cards::post_mini($post, 'ajax-item');
}
}
$base = $plate instanceof \WP_Post ? get_permalink($plate) : ($term instanceof \WP_Term ? get_term_link($term) : Core::home_url());
$next = add_query_arg(['plate_tab' => $index, 'fpage' => $page + 1], is_string($base) ? $base : Core::home_url());
if ($page < (int) $query->max_num_pages) {
$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', 3) . '</div>';
if ($html !== '' && trim(wp_strip_all_tags($html)) !== '') return $html;
$empty = jio_get_ajax_null('内容空空如也');
if ($index === 1 && $page === 1 && $plate instanceof \WP_Post) {
$empty .= '<div class="text-center mb20"><a class="ml10 mr10 mt10 but hollow c-green padding-lg" href="' . esc_url(Core::edit_url()) . '"><i class="fa fas fa-plus mr3"></i>发布' . esc_html((string) jio_get_setting('jio_forum_post_name', '帖子')) . '</a></div>';
}
return $empty;
}
/** @return int[] */
private static function topping(int $plate_id, array $tab): array {
if (($tab['filter'] ?? '') === 'essence' || ($tab['status'] ?? '') !== '') return [];
return wp_list_pluck(self::topping_posts($plate_id, $tab), 'ID');
}
/** @return \WP_Post[] */
private static function topping_posts(int $plate_id, array $tab): array {
return get_posts([
'post_type' => Core::POST,
'post_status' => 'publish',
'posts_per_page' => 5,
'meta_query' => [
['key' => '_jio_forum_plate_id', 'value' => $plate_id],
['key' => '_jio_forum_pinned', 'value' => 1],
],
]);
}
}