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

+167
View File
@@ -0,0 +1,167 @@
<?php
namespace Jio\Forum;
defined('ABSPATH') || exit;
/** Forum compose/edit page (reference two-column editor + settings sidebar). */
final class EditView {
public static function render(): void {
$edit_id = absint(get_query_var('jio_forum_edit'));
$post = $edit_id > 0 ? get_post($edit_id) : null;
if ($edit_id > 0 && (!$post instanceof \WP_Post || !Access::can_manage_post(get_current_user_id(), $edit_id))) {
status_header(403);
echo '<main id="forum" class="jio-forum forum-edit"><div class="container"><div class="zib-widget full-widget-sm text-center" style="min-height:60vh;">' . jio_get_null('抱歉!您暂无编辑此内容的权限', 60, 'null-post.svg') . '</div></div></main>';
get_footer();
return;
}
$user_id = get_current_user_id();
$type = $post ? (string) get_post_meta($post->ID, '_jio_forum_type', true) : 'normal';
$plate_id = $post ? (int) get_post_meta($post->ID, '_jio_forum_plate_id', true) : absint(is_scalar($_GET['plate'] ?? 0) ? $_GET['plate'] : 0);
$view_limit = $post ? (string) get_post_meta($post->ID, '_jio_forum_view_limit', true) : 'all';
$vote_options = $post ? (array) get_post_meta($post->ID, '_jio_forum_vote_options', true) : [];
$topics = $post ? wp_get_post_terms($post->ID, Core::TOPIC, ['fields' => 'names']) : [];
$tags = $post ? wp_get_post_terms($post->ID, Core::TAG, ['fields' => 'names']) : [];
wp_enqueue_editor();
echo '<main id="forum" class="jio-forum forum-edit"><form id="bbs-posts-edit" data-forum-form="post" data-id="' . (int) $edit_id . '"><div class="container"><div class="content-wrap"><div class="content-layout">';
echo '<div class="zib-widget full-widget-sm editor-main-box" style="min-height:60vh">';
if ($user_id <= 0) {
echo jio_get_null('请先登录后再发布', 40, 'null-user.svg') . '<p class="text-center"><a class="but jb-blue padding-lg" href="' . esc_url(jio_get_login_url(Core::edit_url($edit_id))) . '"><i class="fa fas fa-sign-in-alt mr6"></i>登录</a></p>';
echo '</div>';
self::close();
return;
}
// Cover
$cover = $post ? get_the_post_thumbnail_url($post, 'large') : '';
echo '<div class="relative edit-posts-featured mb20" data-forum-cover>';
echo '<div class="forum-cover-preview' . ($cover ? '' : ' is-empty') . '">' . ($cover ? '<img src="' . esc_url($cover) . '" alt="">' : '<i class="fa far fa-image em3x"></i>') . '</div>';
echo '<button type="button" class="but hollow c-blue padding-lg" data-forum-cover-pick><i class="fa fas fa-image mr6"></i>' . ($cover ? '更换封面' : '添加封面') . '</button>';
echo '<input type="file" accept="image/*" data-forum-cover-file hidden><input type="hidden" name="featured_image_id" value="' . (int) get_post_thumbnail_id($edit_id) . '"></div>';
// Title
echo '<div class="relative edit-posts-title"><textarea class="line-form-input input-lg new-title" name="title" rows="1" maxlength="120" tabindex="1" placeholder="' . esc_attr((string) jio_get_setting('jio_forum_title_placeholder', '请输入标题')) . '">' . esc_textarea($post ? $post->post_title : '') . '</textarea><i class="line-form-line"></i></div>';
// Editor
wp_editor($post ? $post->post_content : '', 'jio_forum_content', [
'textarea_name' => 'content',
'textarea_rows' => 20,
'editor_height' => wp_is_mobile() ? 460 : 486,
'media_buttons' => false,
'quicktags' => false,
'default_editor' => 'tinymce',
'tinymce' => ['placeholder' => (string) jio_get_setting('jio_forum_content_placeholder', '请输入内容')],
]);
echo '<div class="jio-forum__editor-tools mt10">';
foreach (['image' => ['插入图片', 'fa-image'], 'video' => ['插入视频', 'fa-video'], 'resource' => ['插入附件', 'fa-paperclip']] as $kind => [$label, $icon]) {
$setting = 'jio_forum_upload_' . ($kind === 'resource' ? 'file' : $kind);
if (jio_get_setting($setting, $kind === 'image')) {
echo '<button type="button" class="but hollow px12" data-forum-upload="' . esc_attr($kind) . '"><i class="fa fas ' . esc_attr($icon) . ' mr6"></i>' . esc_html($label) . '</button>';
}
}
echo '<input type="file" data-forum-file hidden></div>';
self::hidden_fields($post, $type, $view_limit);
echo '</div>'; // editor-main-box
// Sidebar: submit, status, access, plate, topic, tag, type, vote
echo '<div class="sidebar show-sidebar">';
echo self::submit_box($post);
echo self::status_box($post);
echo self::allow_view_box($view_limit);
echo self::plate_box($plate_id);
echo self::term_box('topics', '话题', Core::TOPIC, $topics, 'fa-hashtag');
echo self::term_box('tags', '标签', Core::TAG, $tags, 'fa-tag');
echo self::type_box($type);
echo self::vote_box($vote_options, $post);
\Jio\WidgetSystem\WidgetAreas::render('forum_edit_sidebar');
echo '</div>';
self::close();
}
private static function hidden_fields(?\WP_Post $post, string $type, string $view_limit): void {
echo '<input type="hidden" name="type" value="' . esc_attr($type) . '">';
echo '<input type="hidden" name="view_limit" value="' . esc_attr($view_limit) . '">';
echo '<p class="jio-forum__message" role="status" aria-live="polite"></p>';
}
private static function submit_box(?\WP_Post $post): string {
$last = $post ? get_the_modified_time('Y-m-d H:i:s', $post->ID) : '';
$html = '<div class="zib-widget mb10-sm mb20"><div class="but-average mb20 main-radius">';
if (!$post || !in_array($post->post_status, ['publish', 'pending'], true)) {
$html .= '<button type="button" class="but jb-green padding-10" data-forum-draft><i class="fa far fa-save mr6"></i>保存草稿</button>';
}
$html .= '<button type="submit" class="but jb-blue padding-10 bbs-posts-submit"><i class="fa far fa-paper-plane mr6"></i>' . ($post ? '保存' : '发布') . '</button>';
$html .= '</div>';
if ($post && $last !== '') {
$html .= '<div class="muted-3-color px12"><span class="badg badg-sm mr6">编辑内容</span>最后更新:' . esc_html($last) . '</div>';
}
return $html . '</div>';
}
private static function status_box(?\WP_Post $post): string {
if (!$post instanceof \WP_Post) return '';
$status = in_array($post->post_status, ['pending', 'draft', 'publish'], true) ? $post->post_status : 'pending';
$list = '';
foreach (['pending' => '待审核', 'draft' => '草稿', 'publish' => '发布'] as $key => $label) {
$list .= '<span data-forum-status="' . $key . '" class="badg mr3 p2-10 pointer' . ($key === $status ? ' active' : '') . '">' . $label . '</span>';
}
return '<div class="zib-widget mb10 type-drop"><div class="flex ac jsb"><div class="flex ac c-red"><i class="fa fas fa-thumbtack mr10"></i>设置状态</div><div class="em09">' . $list . '</div></div><input type="hidden" name="post_status" value="' . esc_attr($status) . '"></div>';
}
private static function allow_view_box(string $current): string {
$labels = ['all' => '公开', 'login' => '登录后可查看', 'follow' => '关注版块后可查看', 'points' => '支付积分查看', 'pay' => '付费查看'];
$options = '';
foreach ($labels as $key => $label) {
$options .= '<div data-forum-view="' . $key . '" class="pointer but p2-10 mb6' . ($key === $current ? ' active' : '') . '">' . esc_html($label) . '</div>';
}
return '<div class="allow-view-drop drop-select dropdown zib-widget mb10-sm mb20"><div data-toggle-class="open" data-target=".allow-view-drop" class="flex ac jsb drop-btn"><div class="flex ac"><i class="fa far fa-eye mr6"></i>阅读权限</div><div class="flex ac"><span name="view_limit" class="em09 muted-2-color">' . esc_html($labels[$current] ?? '公开') . '</span><i class="ml6 fa fas fa-angle-right em12"></i></div></div><div class="dropdown-menu fluid"><div class="padding-10 flex jsb muted-2-color em09">设置阅读权限<span data-toggle-class="close" data-target=".allow-view-drop" class="pointer">完成</span></div><div class="mini-scrollbar padding-w10 select-drop-box">' . $options . '</div></div></div>';
}
private static function plate_box(int $current): string {
$plates = get_posts(['post_type' => Core::PLATE, 'post_status' => 'publish', 'posts_per_page' => 200, 'orderby' => 'title', 'order' => 'ASC']);
$options = '';
foreach ($plates as $plate) {
$options .= '<div data-forum-plate="' . (int) $plate->ID . '" class="pointer but p2-10 mb6' . ((int) $plate->ID === $current ? ' active' : '') . '">' . esc_html($plate->post_title) . '</div>';
}
$name = $current > 0 ? get_the_title($current) : '请选择';
return '<div class="plate-drop drop-select dropdown zib-widget mb10-sm mb20"><div data-toggle-class="open" data-target=".plate-drop" class="flex ac jsb drop-btn"><div class="flex ac"><i class="fa fas fa-cube mr6"></i>版块</div><div class="flex ac"><span name="plate_id" class="em09 muted-2-color text-ellipsis">' . esc_html($name) . '</span><i class="ml6 fa fas fa-angle-right em12"></i></div></div><input type="hidden" name="plate_id" value="' . ($current ?: '') . '"><div class="dropdown-menu fluid"><div class="mini-scrollbar padding-w10 select-drop-box">' . $options . '</div></div></div>';
}
/** @param string[] $selected */
private static function term_box(string $field, string $label, string $taxonomy, array $selected, string $icon): string {
$terms = get_terms(['taxonomy' => $taxonomy, 'hide_empty' => false, 'number' => 200, 'orderby' => 'count', 'order' => 'DESC']);
$selected = array_values(array_filter(array_map('strval', $selected)));
$options = '';
foreach (is_array($terms) ? $terms : [] as $term) {
$active = in_array($term->name, $selected, true);
$options .= '<label class="but p2-10 mb6 pointer' . ($active ? ' active' : '') . '"><input type="checkbox" data-forum-' . esc_attr($field) . ' value="' . esc_attr($term->name) . '"' . checked($active, true, false) . ' hidden> ' . esc_html($term->name) . '</label>';
}
return '<div class="' . esc_attr($field) . '-drop drop-select dropdown zib-widget mb10-sm mb20"><div data-toggle-class="open" data-target=".' . esc_attr($field) . '-drop" class="flex ac jsb drop-btn"><div class="flex ac"><i class="fa fas ' . esc_attr($icon) . ' mr6"></i>' . esc_html($label) . '</div><div class="flex ac"><span class="em09 muted-2-color text-ellipsis" data-forum-' . esc_attr($field) . '-label>' . esc_html($selected ? implode('、', array_slice($selected, 0, 2)) : '请选择') . '</span><i class="ml6 fa fas fa-angle-right em12"></i></div></div><input type="hidden" name="' . esc_attr($field) . '" value="' . esc_attr(implode(',', $selected)) . '"><div class="dropdown-menu fluid"><div class="mini-scrollbar padding-w10 select-drop-box">' . $options . '</div></div></div>';
}
private static function type_box(string $current): string {
$options = [];
$options['normal'] = '标准';
if (jio_get_setting('jio_forum_question_enabled', true)) $options['question'] = '提问';
if (jio_get_setting('jio_forum_vote_enabled', true)) $options['vote'] = '投票';
if (count($options) < 2) return '';
$list = '';
foreach ($options as $key => $label) {
$list .= '<span data-forum-type-set="' . $key . '" class="but p2-10 pointer' . ($key === $current ? ' active' : '') . '">' . esc_html($label) . '</span>';
}
return '<div class="type-drop zib-widget mb10-sm mb20"><div class="flex ac jsb"><div class="flex ac"><i class="fa fas fa-delicious mr6"></i>发布类型</div><div class="but-average radius em09">' . $list . '</div></div></div>';
}
/** @param string[] $options */
private static function vote_box(array $options, ?\WP_Post $post): string {
$type = $post ? (string) get_post_meta($post->ID, '_jio_forum_type', true) : 'normal';
return '<div class="type-drop zib-widget mb10-sm mb20 forum-type-vote"' . ($type === 'vote' ? '' : ' hidden') . '><div class="mb10"><i class="fa fas fa-poll mr6"></i>投票设置</div>'
. '<input class="form-control mb6" name="vote_title" maxlength="60" placeholder="投票标题" value="' . esc_attr($post ? (string) get_post_meta($post->ID, '_jio_forum_vote_title', true) : '') . '">'
. '<select class="form-control mb6" name="vote_type"><option value="single">单选</option><option value="multiple">多选</option><option value="pk">双选 PK</option></select>'
. '<textarea class="form-control mb6" name="vote_options" rows="4" placeholder="每行一个选项">' . esc_textarea(implode("\n", $options)) . '</textarea>'
. '<select class="form-control" name="vote_days"><option value="0">永久</option><option value="1">1 天</option><option value="7">7 天</option><option value="30">30 天</option></select></div>';
}
private static function close(): void {
echo '</div></div></div></form></main>';
get_footer();
}
}