feat(forum): add the reference forum widgets
Port the six forum widgets from the reference theme: posts list, multi-tab posts, plate list (default and card), plate info, moderator list and topic list. They join the schema widget catalog under a forum group that stays hidden while the forum module is off, reuse the shared forum cards, paginate through the widget REST client contract, and pull the forum stylesheet/runtime onto any sidebar that hosts them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
376827ea97
commit
eceddb5f7d
12 files changed
+724
-13
No files matched your search
@@ -256,3 +256,29 @@ body.dark-theme .jio-forum__dialog{background:var(--main-bg-color);color:var(--k
|
||||
body.dark-theme .jio-forum__dialog input,body.dark-theme .jio-forum__dialog select,body.dark-theme .jio-forum__dialog textarea{background:var(--main-bg-color);color:var(--key-color);border-color:var(--muted-4-color)}
|
||||
body.dark-theme #forum .jio-forum__action{color:var(--forum-muted)}
|
||||
body.dark-theme .panel.panel-plate,body.dark-theme .plate-lists .plate-item,body.dark-theme .forum-posts.alone,body.dark-theme .scroll-plate .plate-card{background:var(--main-bg-color)}
|
||||
|
||||
/* ── 论坛小工具(任意侧栏 / 全宽区域) ─────────────────────────── */
|
||||
.forum-header{box-shadow:0 0 10px var(--main-shadow);border-radius:var(--main-radius);position:relative;background:var(--main-bg-color)}
|
||||
.forum-header .header-content{padding:70px 50px 50px;background:var(--main-bg-color);border-radius:var(--main-radius)}
|
||||
.forum-header .plate-thumb{width:115px;height:115px;margin-left:auto;margin-right:auto}
|
||||
.forum-header .plate-thumb > img{border-radius:4px}
|
||||
.forum-header .count-mates{margin-top:10px}
|
||||
.forum-header .count-mates item{display:inline-block}
|
||||
.forum-header .count-mates > item + item{border-left:1px solid var(--main-border-color);padding-left:20px;margin-left:20px}
|
||||
.forum-header .img-badge.top{right:auto;left:1.2em}
|
||||
.plate-info-sidebar .header-content{padding:55px 20px 25px}
|
||||
.plate-info-sidebar .follow-info{background:var(--main-border-color);box-shadow:none;color:var(--focus-color);padding:6px 12px;margin-top:38px;opacity:.9}
|
||||
.jio-widget-box.moderator-box{padding:15px}
|
||||
.moderator-box .user-info + .user-info{border-top:1px solid var(--main-border-color)}
|
||||
.moderator-box .avatar-img img{width:100%;height:100%;border-radius:100px}
|
||||
.topic-item{transition:.2s;border-radius:var(--main-radius)}
|
||||
.topic-item:hover{background:var(--muted-border-color)}
|
||||
.topic-item .term-title{font-size:15px;font-weight:600}
|
||||
.topic-item .desc span{margin-right:10px}
|
||||
.jio-widget-box .widget-ajaxpager .forum-posts + .forum-posts{border-top:1px solid var(--main-border-color)}
|
||||
@media (max-width:640px){
|
||||
.forum-header .header-content{padding:60px 15px 30px}
|
||||
.forum-header .plate-thumb{width:105px;height:105px}
|
||||
.forum-header .count-mates > item + item{padding-left:15px;margin-left:15px}
|
||||
}
|
||||
body.dark-theme .forum-header,body.dark-theme .forum-header .header-content{background:var(--main-bg-color)}
|
||||
@@ -1,6 +1,8 @@
|
||||
(() => {
|
||||
const config = window.jioForum || {}
|
||||
const root = document.getElementById('forum')
|
||||
// 论坛路由根节点是 #forum;论坛小工具放在任意侧栏时没有该节点,
|
||||
// 此时把委托挂到 body,让 follow / apply 等小工具按钮共用同一套动作。
|
||||
const root = document.getElementById('forum') || document.body
|
||||
if (!root) return
|
||||
|
||||
const send = async (path, method = 'GET', data = {}, idempotencyKey = '') => {
|
||||
|
||||
+48
-2
@@ -23,6 +23,7 @@ final class Core {
|
||||
add_action('admin_init', [self::class, 'ensure_home']);
|
||||
add_action('init', [self::class, 'ensure_home'], 20);
|
||||
add_action('wp_enqueue_scripts', [self::class, 'assets'], 30);
|
||||
add_action('wp_enqueue_scripts', [self::class, 'widget_assets'], 31);
|
||||
add_action('widgets_init', static function (): void { register_widget(Widget::class); }, 11);
|
||||
add_action('wp_loaded', [self::class, 'maybe_flush_routes'], 30);
|
||||
add_action('trashed_post', [self::class, 'cascade_trashed_plate']);
|
||||
@@ -56,6 +57,20 @@ final class Core {
|
||||
return home_url('/' . self::slug('edit') . '/' . ($id > 0 ? $id . '/' : ''));
|
||||
}
|
||||
|
||||
/** Plate of the current request: the plate itself or the plate owning the current post/term. */
|
||||
public static function current_plate_id(): int {
|
||||
$obj = get_queried_object();
|
||||
if ($obj instanceof \WP_Post) {
|
||||
if ($obj->post_type === self::PLATE) return (int) $obj->ID;
|
||||
if ($obj->post_type === self::POST) return (int) get_post_meta($obj->ID, '_jio_forum_plate_id', true);
|
||||
}
|
||||
if ($obj instanceof \WP_Term && in_array($obj->taxonomy, [self::TOPIC, self::TAG], true)) {
|
||||
$post = get_posts(['post_type' => self::POST, 'post_status' => 'publish', 'posts_per_page' => 1, 'fields' => 'ids', 'tax_query' => [['taxonomy' => $obj->taxonomy, 'terms' => [$obj->term_id]]]]);
|
||||
if ($post) return (int) get_post_meta((int) $post[0], '_jio_forum_plate_id', true);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static function register(): void {
|
||||
if (!self::enabled()) {
|
||||
return;
|
||||
@@ -199,9 +214,8 @@ final class Core {
|
||||
if (!self::enabled() || !(is_singular([self::POST, self::PLATE]) || is_tax([self::CATEGORY, self::TOPIC, self::TAG]) || is_page_template('templates/pages/page-forums.php') || $account_route || get_query_var('jio_forum_edit') || (is_search() && in_array(sanitize_key((string) ($_GET['type'] ?? '')), ['forum', 'plate'], true)))) {
|
||||
return;
|
||||
}
|
||||
$css = get_theme_file_path('assets/css/routes/forum.css');
|
||||
$js = get_theme_file_path('assets/js/forum-runtime.js');
|
||||
wp_enqueue_style('jio-forum', get_theme_file_uri('assets/css/routes/forum.css'), ['jio-public'], (string) filemtime($css));
|
||||
self::enqueue_css();
|
||||
wp_enqueue_script('jio-forum', get_theme_file_uri('assets/js/forum-runtime.js'), [], (string) filemtime($js), true);
|
||||
wp_enqueue_script('jio-qrcode');
|
||||
$plates = get_posts(['post_type' => self::PLATE, 'post_status' => 'publish', 'posts_per_page' => 100, 'fields' => 'ids']);
|
||||
@@ -213,4 +227,36 @@ final class Core {
|
||||
'plates' => array_map(static fn(int $id): array => ['id' => $id, 'title' => get_the_title($id)], $plates),
|
||||
]) . ';', 'before');
|
||||
}
|
||||
|
||||
/** Forum card styles are shared by the forum routes and the forum widgets. */
|
||||
public static function enqueue_css(): void {
|
||||
$css = get_theme_file_path('assets/css/routes/forum.css');
|
||||
wp_enqueue_style('jio-forum', get_theme_file_uri('assets/css/routes/forum.css'), ['jio-public'], is_file($css) ? (string) filemtime($css) : null);
|
||||
}
|
||||
|
||||
/** A forum widget on any sidebar needs the card styles and the follow/apply runtime. */
|
||||
public static function widget_assets(): void {
|
||||
if (!self::enabled() || !self::has_widgets()) {
|
||||
return;
|
||||
}
|
||||
self::enqueue_css();
|
||||
$js = get_theme_file_path('assets/js/forum-runtime.js');
|
||||
wp_enqueue_script('jio-forum', get_theme_file_uri('assets/js/forum-runtime.js'), [], is_file($js) ? (string) filemtime($js) : null, true);
|
||||
wp_add_inline_script('jio-forum', 'window.jioForum=' . wp_json_encode([
|
||||
'api' => esc_url_raw(rest_url('jio/v1/forum/')),
|
||||
'nonce' => wp_create_nonce('wp_rest'),
|
||||
'login' => function_exists('jio_get_template_url') ? jio_get_template_url('sign') : wp_login_url(),
|
||||
'orders' => function_exists('jio_get_user_center_url') ? jio_get_user_center_url('orders') : home_url('/user/orders/'),
|
||||
'plates' => [],
|
||||
]) . ';', 'before');
|
||||
}
|
||||
|
||||
private static function has_widgets(): bool {
|
||||
foreach (\Jio\Forum\WidgetSchemas::SLUGS as $slug) {
|
||||
if (is_active_widget(false, false, 'jio_widget_' . $slug, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
namespace Jio\Forum;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** Queries and AJAX payloads shared by the forum widgets. */
|
||||
final class WidgetQuery {
|
||||
/** @return \WP_Query */
|
||||
public static function posts(array $instance, int $page = 1): \WP_Query {
|
||||
$count = max(1, min(20, (int) ($instance['count'] ?? 10)));
|
||||
$args = [
|
||||
'post_type' => Core::POST,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => $count,
|
||||
'paged' => max(1, $page),
|
||||
'ignore_sticky_posts' => true,
|
||||
];
|
||||
$plate = self::current_plate($instance);
|
||||
if ($plate > 0) {
|
||||
$args['meta_query'][] = ['key' => '_jio_forum_plate_id', 'value' => $plate];
|
||||
} else {
|
||||
$include = self::ids($instance['include_plate'] ?? '');
|
||||
$exclude = self::ids($instance['exclude_plate'] ?? '');
|
||||
if ($include) $args['meta_query'][] = ['key' => '_jio_forum_plate_id', 'value' => $include, 'compare' => 'IN'];
|
||||
if ($exclude) $args['meta_query'][] = ['key' => '_jio_forum_plate_id', 'value' => $exclude, 'compare' => 'NOT IN'];
|
||||
}
|
||||
$tax = [];
|
||||
foreach ([Core::TOPIC => 'include_topic', Core::TAG => 'include_tag'] as $taxonomy => $key) {
|
||||
$ids = self::ids($instance[$key] ?? '');
|
||||
if ($ids) $tax[] = ['taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $ids];
|
||||
}
|
||||
if ($tax) $args['tax_query'] = $tax;
|
||||
|
||||
$types = array_values(array_filter(array_map('sanitize_key', (array) ($instance['bbs_type'] ?? []))));
|
||||
if ($types) {
|
||||
$args['meta_query'][] = ['key' => '_jio_forum_type', 'value' => $types, 'compare' => 'IN'];
|
||||
}
|
||||
$filters = array_values(array_filter(array_map('sanitize_key', (array) ($instance['filter'] ?? []))));
|
||||
if (in_array('essence', $filters, true)) $args['meta_query'][] = ['key' => '_jio_forum_essence', 'value' => 1];
|
||||
if (in_array('topping', $filters, true)) $args['meta_query'][] = ['key' => '_jio_forum_pinned', 'value' => 1];
|
||||
if (in_array('vote', $filters, true)) $args['meta_query'][] = ['key' => '_jio_forum_type', 'value' => 'vote'];
|
||||
if (in_array('question_status', $filters, true)) $args['meta_query'][] = ['key' => '_jio_forum_adopted_comment', 'value' => '', 'compare' => '!='];
|
||||
|
||||
$orderby = sanitize_key((string) ($instance['orderby'] ?? 'date'));
|
||||
switch ($orderby) {
|
||||
case 'views':
|
||||
case 'score':
|
||||
case 'last_reply':
|
||||
$args['jio_forum_list'] = $orderby;
|
||||
break;
|
||||
case 'comment_count':
|
||||
$args['orderby'] = 'comment_count';
|
||||
break;
|
||||
case 'favorite_count':
|
||||
$args['meta_key'] = '_jio_forum_favorite_count';
|
||||
$args['orderby'] = 'meta_value_num';
|
||||
break;
|
||||
case 'name':
|
||||
$args['orderby'] = 'title';
|
||||
$args['order'] = 'ASC';
|
||||
break;
|
||||
case 'rand':
|
||||
$args['orderby'] = 'rand';
|
||||
break;
|
||||
case 'modified':
|
||||
$args['orderby'] = 'modified';
|
||||
break;
|
||||
default:
|
||||
$args['orderby'] = 'date';
|
||||
}
|
||||
return new \WP_Query($args);
|
||||
}
|
||||
|
||||
/** @return \WP_Post[] */
|
||||
public static function plates(array $instance): array {
|
||||
$count = max(1, min(20, (int) ($instance['count'] ?? 6)));
|
||||
$args = ['post_type' => Core::PLATE, 'post_status' => 'publish', 'posts_per_page' => $count, 'no_found_rows' => true];
|
||||
$cats = array_values(array_filter(array_map('absint', (array) ($instance['category'] ?? []))));
|
||||
if ($cats) {
|
||||
$args['tax_query'] = [['taxonomy' => Core::CATEGORY, 'field' => 'term_id', 'terms' => $cats, 'include_children' => true]];
|
||||
}
|
||||
switch (sanitize_key((string) ($instance['orderby'] ?? 'modified'))) {
|
||||
case 'name':
|
||||
$args['orderby'] = 'title';
|
||||
$args['order'] = 'ASC';
|
||||
break;
|
||||
case 'date':
|
||||
$args['orderby'] = 'date';
|
||||
break;
|
||||
case 'follow_count':
|
||||
$args['meta_key'] = '_jio_forum_follow_count';
|
||||
$args['orderby'] = 'meta_value_num';
|
||||
break;
|
||||
case 'posts_count':
|
||||
case 'today_count':
|
||||
// Resolved from the batched stats table after the query.
|
||||
$args['orderby'] = 'modified';
|
||||
$args['posts_per_page'] = -1;
|
||||
break;
|
||||
case 'rand':
|
||||
$args['orderby'] = 'rand';
|
||||
break;
|
||||
default:
|
||||
$args['orderby'] = 'modified';
|
||||
}
|
||||
$plates = get_posts($args);
|
||||
PlateStats::prime(wp_list_pluck($plates, 'ID'));
|
||||
$by_stat = sanitize_key((string) ($instance['orderby'] ?? ''));
|
||||
if (in_array($by_stat, ['posts_count', 'today_count'], true)) {
|
||||
$key = $by_stat === 'posts_count' ? 'posts' : 'today';
|
||||
usort($plates, static fn(\WP_Post $a, \WP_Post $b): int => PlateStats::get($b->ID)[$key] <=> PlateStats::get($a->ID)[$key]);
|
||||
$plates = array_slice($plates, 0, $count);
|
||||
}
|
||||
return $plates;
|
||||
}
|
||||
|
||||
/** @return \WP_Term[] */
|
||||
public static function topics(array $instance): array {
|
||||
$count = max(0, (int) ($instance['count'] ?? 6));
|
||||
$orderby = sanitize_key((string) ($instance['orderby'] ?? 'count'));
|
||||
$include = array_values(array_filter(array_map('absint', (array) ($instance['include'] ?? []))));
|
||||
$args = [
|
||||
'taxonomy' => Core::TOPIC,
|
||||
'hide_empty' => !empty($instance['hide_empty']),
|
||||
'number' => $count,
|
||||
'orderby' => $orderby === 'include' ? 'include' : ($orderby === 'name' ? 'name' : 'count'),
|
||||
'order' => $orderby === 'name' ? 'ASC' : 'DESC',
|
||||
];
|
||||
if ($include) {
|
||||
$args['include'] = $include;
|
||||
if ($orderby === 'include') $args['orderby'] = 'include';
|
||||
}
|
||||
$terms = get_terms($args);
|
||||
return is_array($terms) ? $terms : [];
|
||||
}
|
||||
|
||||
/** AJAX payload for the paginated forum lists. */
|
||||
public static function payload(string $slug, array $instance, int $page = 1, string $segment = ''): array {
|
||||
if ($slug === 'forum_tab_posts') {
|
||||
$tabs = array_values((array) ($instance['tabs'] ?? []));
|
||||
$index = ctype_digit($segment) ? (int) $segment : 0;
|
||||
$tab = $tabs[$index] ?? ($tabs[0] ?? []);
|
||||
if (is_array($tab)) {
|
||||
$instance = array_merge($instance, $tab);
|
||||
}
|
||||
}
|
||||
$query = self::posts($instance, $page);
|
||||
$html = WidgetRenderer::items($query, $instance);
|
||||
$max = max(1, (int) $query->max_num_pages);
|
||||
return [
|
||||
'html' => $html,
|
||||
'paginate_html' => self::pagination($instance, $page, $max, $segment),
|
||||
'has_more' => $page < $max,
|
||||
'page' => $page,
|
||||
'max_pages' => $max,
|
||||
'total' => (int) $query->found_posts,
|
||||
];
|
||||
}
|
||||
|
||||
private static function pagination(array $instance, int $page, int $max, string $segment): string {
|
||||
$mode = sanitize_key((string) ($instance['paginate'] ?? 'none'));
|
||||
if ($mode === 'none' || $max <= 1) return '';
|
||||
$segment_attr = $segment !== '' ? ' data-segment="' . esc_attr($segment) . '"' : '';
|
||||
if ($mode === 'default') {
|
||||
$links = '';
|
||||
for ($number = 1; $number <= $max; $number++) {
|
||||
$links .= $number === $page
|
||||
? '<span class="page-numbers current">' . $number . '</span>'
|
||||
: '<a class="next-page page-numbers" ajaxpager-target=".widget-ajaxpager" data-widget-page="' . $number . '"' . $segment_attr . ' role="button" tabindex="0" href="#page-' . $number . '">' . $number . '</a>';
|
||||
}
|
||||
return '<div class="pagenav ajax-pag" role="navigation" aria-label="' . esc_attr__('分页', 'jio') . '">' . $links . '</div>';
|
||||
}
|
||||
if ($page >= $max) return '';
|
||||
return '<div class="text-center theme-pagination ajax-pag"><div no-scroll="true" ajaxpager-target=".widget-ajaxpager" class="next-page"><a role="button" tabindex="0" href="#next" data-next-page="' . (int) ($page + 1) . '"' . $segment_attr . '>' . esc_html__('加载更多', 'jio') . '</a></div></div>';
|
||||
}
|
||||
|
||||
/** @return int[] */
|
||||
private static function ids($raw): array {
|
||||
$values = is_array($raw) ? $raw : preg_split('/[,,\s]+/', (string) $raw);
|
||||
return array_values(array_unique(array_filter(array_map('absint', $values ?: []))));
|
||||
}
|
||||
|
||||
/** Current plate when the widget is pinned to the page it sits on. */
|
||||
private static function current_plate(array $instance): int {
|
||||
if (empty($instance['current_plate'])) return 0;
|
||||
return Core::current_plate_id();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace Jio\Forum;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** Renderers for the six forum widgets (reference parity). */
|
||||
final class WidgetRenderer {
|
||||
public const SUPPORTS_ITEMS = ['forum_posts', 'forum_tab_posts'];
|
||||
|
||||
public static function render(string $slug, array $instance, array $context = []): string {
|
||||
switch ($slug) {
|
||||
case 'forum_posts':
|
||||
return self::posts($instance, (string) ($context['widget_id'] ?? ''));
|
||||
case 'forum_tab_posts':
|
||||
return self::tab_posts($instance, (string) ($context['widget_id'] ?? ''));
|
||||
case 'forum_plates':
|
||||
return self::plates($instance);
|
||||
case 'forum_plate_info':
|
||||
return self::plate_info();
|
||||
case 'forum_moderators':
|
||||
return self::moderators($instance);
|
||||
case 'forum_topics':
|
||||
return self::topics($instance);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private static function posts(array $instance, string $widget_id): string {
|
||||
$pager = self::pager($instance, 'forum_posts', $widget_id, 1, '');
|
||||
$style = self::style($instance);
|
||||
$html = '<div class="widget-main-post style-' . esc_attr($style) . '">';
|
||||
if ($style === 'mini') $html .= '<div class="jio-widget-box posts-mini-lists">';
|
||||
$html .= $pager;
|
||||
if ($style === 'mini') $html .= '</div>';
|
||||
return $html . '</div>';
|
||||
}
|
||||
|
||||
private static function tab_posts(array $instance, string $widget_id): string {
|
||||
$tabs = array_values((array) ($instance['tabs'] ?? []));
|
||||
$tabs = array_values(array_filter($tabs, static fn($tab): bool => is_array($tab) && trim(wp_strip_all_tags((string) ($tab['title'] ?? ''))) !== ''));
|
||||
if (!$tabs) return '';
|
||||
$key = sanitize_html_class($widget_id) ?: 'preview';
|
||||
$buttons = $panels = '';
|
||||
foreach ($tabs as $index => $tab) {
|
||||
$selected = $index === 0;
|
||||
$panel_id = 'forum-tab-' . $key . '-' . $index;
|
||||
$buttons .= '<li class="' . ($selected ? 'active' : '') . '"><a href="#' . esc_attr($panel_id) . '" role="tab" aria-selected="' . ($selected ? 'true' : 'false') . '" tabindex="' . ($selected ? '0' : '-1') . '" data-segment="' . $index . '">' . wp_kses_post((string) $tab['title']) . '</a></li>';
|
||||
$body = $selected
|
||||
? self::pager(array_merge($instance, $tab), 'forum_tab_posts', $widget_id, 1, (string) $index)
|
||||
: '<div class="widget-ajaxpager" data-widget-items data-widget="forum_tab_posts" data-instance="' . esc_attr($widget_id) . '" data-page="1" data-segment="' . $index . '"><div class="post_ajax_loader" role="status">' . HomeTabs::placeholder('posts', (int) ($instance['count'] ?? 10)) . '</div></div>';
|
||||
$panels .= '<div class="tab-pane fade' . ($selected ? ' active in' : '') . '" id="' . esc_attr($panel_id) . '" role="tabpanel">' . $body . '</div>';
|
||||
}
|
||||
$style = self::style($instance);
|
||||
return '<div class="widget-tab-bbs-posts style-' . esc_attr($style) . '"><div class="index-tab rectangular relative mb10"><ul class="list-inline scroll-x no-scrollbar" role="tablist">' . $buttons . '</ul></div><div class="tab-content">' . $panels . '</div></div>';
|
||||
}
|
||||
|
||||
public static function items(\WP_Query $query, array $instance): string {
|
||||
if (!$query->have_posts()) {
|
||||
return jio_get_ajax_null('暂无内容');
|
||||
}
|
||||
Cards::prime($query->posts);
|
||||
$style = self::style($instance);
|
||||
$class = empty($instance['alone']) ? 'ajax-item' : 'alone ajax-item';
|
||||
$show_topping = in_array('topping', array_map('sanitize_key', (array) ($instance['filter'] ?? [])), true);
|
||||
$html = '';
|
||||
foreach ($query->posts as $post) {
|
||||
if ($style === 'detail') {
|
||||
$html .= Cards::post_detail($post, $class, true, $show_topping);
|
||||
} elseif ($style === 'minimalism') {
|
||||
$html .= '<article class="forum-posts minimalism ' . esc_attr($class) . '">' . Cards::title($post, 'forum-title', 'em09', $show_topping) . '</article>';
|
||||
} else {
|
||||
$html .= Cards::post_mini($post, $class, $show_topping);
|
||||
}
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
private static function plates(array $instance): string {
|
||||
$plates = WidgetQuery::plates($instance);
|
||||
if (!$plates) {
|
||||
return jio_get_ajax_null('暂无版块');
|
||||
}
|
||||
if (sanitize_key((string) ($instance['style'] ?? 'default')) === 'card') {
|
||||
return '<div class="mb20">' . Cards::plate_slide($plates) . '</div>';
|
||||
}
|
||||
$items = '';
|
||||
foreach ($plates as $plate) {
|
||||
$items .= Cards::plate_item($plate);
|
||||
}
|
||||
return '<div class="mb20"><div class="plate-lists">' . $items . '</div></div>';
|
||||
}
|
||||
|
||||
private static function plate_info(): string {
|
||||
$plate_id = Core::current_plate_id();
|
||||
$plate = $plate_id > 0 ? get_post($plate_id) : null;
|
||||
if (!$plate instanceof \WP_Post || $plate->post_type !== Core::PLATE) return '';
|
||||
PlateStats::prime([$plate_id]);
|
||||
$stats = PlateStats::get($plate_id);
|
||||
$followers = (int) get_post_meta($plate_id, '_jio_forum_follow_count', true);
|
||||
$plat_name = (string) jio_get_setting('jio_forum_plate_name', '版块') ?: '版块';
|
||||
$html = '<div class="forum-header relative-h plate-info-sidebar">';
|
||||
if ($followers > 0) {
|
||||
$html .= '<badge class="img-badge px12 follow-info">' . esc_html(sprintf('%s人已关注', jio_cut_count($followers))) . '</badge>';
|
||||
}
|
||||
$html .= '<div class="relative text-center header-content">';
|
||||
$html .= '<div class="plate-thumb flex0 mb20">' . self::plate_thumb($plate) . '</div>';
|
||||
$html .= '<div class="item-info">';
|
||||
$html .= '<h1 class="forum-title em14"><a href="' . esc_url(get_permalink($plate)) . '">' . esc_html($plate->post_title) . '</a></h1>';
|
||||
$excerpt = wp_trim_words(wp_strip_all_tags($plate->post_excerpt ?: $plate->post_content), 40);
|
||||
if ($excerpt !== '') $html .= '<div class="em09 muted-2-color mt10">' . esc_html($excerpt) . '</div>';
|
||||
$html .= '<div class="mt20 mb20 buts"><a class="but hollow c-blue mr10" href="' . esc_url(Core::edit_url()) . '">发布' . esc_html((string) jio_get_setting('jio_forum_post_name', '帖子')) . '</a>' . Cards::follow_button($plate_id, 'but hollow c-red') . '</div>';
|
||||
$html .= '<div class="count-mates text-center"><item class="mate-views"><div class="em09 opacity5 mb6">' . esc_html($plat_name) . '帖子</div><div class="em14">' . esc_html(jio_cut_count($stats['posts'])) . '</div></item>';
|
||||
$html .= '<item class="mate-views"><div class="em09 opacity5 mb6">今日</div><div class="em14">' . esc_html(jio_cut_count($stats['today'])) . '</div></item>';
|
||||
$html .= '<item class="mate-views"><div class="em09 opacity5 mb6">关注</div><div class="em14">' . esc_html(jio_cut_count($followers)) . '</div></item></div>';
|
||||
return $html . '</div></div></div>';
|
||||
}
|
||||
|
||||
private static function moderators(array $instance): string {
|
||||
$plate_id = Core::current_plate_id();
|
||||
$plate = $plate_id > 0 ? get_post($plate_id) : null;
|
||||
if (!$plate instanceof \WP_Post || $plate->post_type !== Core::PLATE) return '';
|
||||
$ids = array_values(array_unique(array_filter(array_merge(
|
||||
[(int) $plate->post_author],
|
||||
array_map('absint', (array) get_post_meta($plate_id, '_jio_forum_moderators', true))
|
||||
))));
|
||||
$list = '';
|
||||
foreach ($ids as $user_id) {
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user) continue;
|
||||
$owner = $user_id === (int) $plate->post_author;
|
||||
$list .= '<div class="user-info flex ac padding-h10"><span class="avatar-img">' . get_avatar($user_id, 80) . '</span>'
|
||||
. '<div class="flex1 ml10 flex ac jsb"><div class="flex1"><a class="display-name" href="' . esc_url(get_author_posts_url($user_id)) . '">' . esc_html($user->display_name) . '</a>'
|
||||
. '<div class="mt3"><span class="badg badg-sm ' . ($owner ? 'c-blue' : 'c-green') . '">' . ($owner ? '版主' : '副版主') . '</span></div></div>'
|
||||
. ($owner ? '' : '<div class="flex0 em09 ml10">' . jio_get_user_follow('follow c-red', $user_id) . '</div>')
|
||||
. '</div></div>';
|
||||
}
|
||||
if ($list === '') return '';
|
||||
$title = trim(wp_strip_all_tags((string) ($instance['title_text'] ?? '')));
|
||||
$apply = !empty($instance['apply_btn']) && is_user_logged_in() && !Access::is_moderator(get_current_user_id(), $plate_id) && jio_get_setting('jio_forum_moderator_apply', true)
|
||||
? '<button type="button" class="but hollow c-blue p2-10 em09" data-forum-action="apply" data-id="' . $plate_id . '">申请版主</button>'
|
||||
: '';
|
||||
$head = ($title !== '' || $apply !== '') ? '<div class="mb10 flex jsb ac">' . ($title !== '' ? '<div class="title-theme">' . esc_html($title) . '</div>' : '<div></div>') . $apply . '</div>' : '';
|
||||
return '<div class="jio-widget-box moderator-box">' . $head . $list . '</div>';
|
||||
}
|
||||
|
||||
private static function topics(array $instance): string {
|
||||
$terms = WidgetQuery::topics($instance);
|
||||
if (!$terms) return '<div class="jio-widget-box padding-6">' . jio_get_ajax_null('暂无内容', 20) . '</div>';
|
||||
$list = '';
|
||||
foreach ($terms as $term) {
|
||||
$icon = '<i class="fas fa-hashtag mr3 c-blue" aria-hidden="true"></i>';
|
||||
$list .= '<a href="' . esc_url(get_term_link($term)) . '"><div class="flex topic-item padding-10"><div class="flex1"><div class="term-title">' . $icon . esc_html($term->name) . $icon . '</div>'
|
||||
. '<div class="muted-3-color em09 desc"><span class="mr20">帖子:' . esc_html(jio_cut_count($term->count)) . '</span></div></div></div></a>';
|
||||
}
|
||||
return '<div class="jio-widget-box padding-6">' . $list . '</div>';
|
||||
}
|
||||
|
||||
/** Loader-bearing container consumed by assets/widgets/frontend.js. */
|
||||
private static function pager(array $instance, string $slug, string $widget_id, int $page, string $segment): string {
|
||||
$attrs = ' data-widget-items data-widget="' . esc_attr($slug) . '" data-instance="' . esc_attr($widget_id) . '" data-page="' . $page . '"';
|
||||
if ($segment !== '') $attrs .= ' data-segment="' . esc_attr($segment) . '"';
|
||||
$payload = WidgetQuery::payload($slug, $instance, $page, $segment);
|
||||
$loader = '<div class="post_ajax_loader" role="status" aria-label="' . esc_attr__('加载中', 'jio') . '">' . HomeTabs::placeholder('posts', (int) ($instance['count'] ?? 10)) . '</div>';
|
||||
return '<div class="widget-ajaxpager"' . $attrs . ' data-loaded="1" aria-live="polite" aria-busy="false">' . $payload['html'] . $payload['paginate_html'] . $loader . '</div>';
|
||||
}
|
||||
|
||||
private static function style(array $instance): string {
|
||||
$style = sanitize_key((string) ($instance['style'] ?? 'detail'));
|
||||
return in_array($style, ['detail', 'mini', 'minimalism'], true) ? $style : 'detail';
|
||||
}
|
||||
|
||||
private static function plate_thumb(\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) . '">';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace Jio\Forum;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** Field schemas for the six forum widgets (reference parity). */
|
||||
final class WidgetSchemas {
|
||||
public const SLUGS = ['forum_posts', 'forum_tab_posts', 'forum_plates', 'forum_plate_info', 'forum_moderators', 'forum_topics'];
|
||||
|
||||
public static function for(string $slug): array {
|
||||
switch ($slug) {
|
||||
case 'forum_posts':
|
||||
return self::posts_fields();
|
||||
case 'forum_tab_posts':
|
||||
return self::tab_posts_fields();
|
||||
case 'forum_plates':
|
||||
return self::plates_fields();
|
||||
case 'forum_moderators':
|
||||
return self::moderator_fields();
|
||||
case 'forum_topics':
|
||||
return self::topic_fields();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private static function posts_fields(): array {
|
||||
return array_merge(self::post_filter_fields(), [
|
||||
self::orderby_field(),
|
||||
['id' => 'style', 'type' => 'radio', 'label' => '列表样式', 'inline' => true, 'default' => 'detail', 'options' => [
|
||||
'detail' => '详细内容',
|
||||
'mini' => '简约风格',
|
||||
'minimalism' => '极简风格',
|
||||
]],
|
||||
['id' => 'alone', 'type' => 'switcher', 'label' => '列表独立', 'default' => false, 'description' => '每一个列表都独立显示为模块'],
|
||||
['id' => 'count', 'type' => 'spinner', 'label' => '显示数量', 'default' => 10, 'min' => 4, 'max' => 20, 'step' => 1, 'unit' => '篇'],
|
||||
self::paginate_field(),
|
||||
]);
|
||||
}
|
||||
|
||||
private static function tab_posts_fields(): array {
|
||||
return [
|
||||
['id' => 'style', 'type' => 'radio', 'label' => '列表样式', 'inline' => true, 'default' => 'detail', 'options' => [
|
||||
'detail' => '详细内容',
|
||||
'mini' => '简约风格',
|
||||
'minimalism' => '极简风格',
|
||||
]],
|
||||
['id' => 'alone', 'type' => 'switcher', 'label' => '列表独立', 'default' => false, 'description' => '每一个列表都独立显示为模块'],
|
||||
['id' => 'count', 'type' => 'spinner', 'label' => '显示数量', 'default' => 10, 'min' => 4, 'max' => 20, 'step' => 1, 'unit' => '篇'],
|
||||
self::paginate_field(),
|
||||
[
|
||||
'id' => 'tabs', 'type' => 'group', 'label' => '栏目', 'max_items' => 10, 'item_label' => '栏目',
|
||||
'default' => [
|
||||
['title' => '热门推荐', 'orderby' => 'views'],
|
||||
['title' => '最新帖子', 'orderby' => 'modified'],
|
||||
['title' => '精华帖子', 'orderby' => 'modified', 'filter' => ['essence']],
|
||||
],
|
||||
'fields' => array_merge(
|
||||
[['id' => 'title', 'type' => 'textarea', 'label' => '标题(必填)', 'default' => '', 'allow_html' => true, 'attributes' => ['rows' => 1], 'description' => '栏目显示的标题,支持HTML代码']],
|
||||
self::post_filter_fields(),
|
||||
[self::orderby_field()]
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private static function plates_fields(): array {
|
||||
return [
|
||||
['id' => 'category', 'type' => 'terms', 'label' => '分类筛选', 'taxonomy' => ['jio_forum_plate_cat'], 'default' => [], 'description' => '仅显示所选分类的版块,留空则显示全部分类'],
|
||||
['id' => 'orderby', 'type' => 'select', 'label' => '排序方式', 'default' => 'modified', 'options' => [
|
||||
'name' => '标题名称',
|
||||
'modified' => '更新时间',
|
||||
'date' => '创建时间',
|
||||
'follow_count' => '最多关注',
|
||||
'posts_count' => '最多帖子',
|
||||
'today_count' => '今日最多新帖',
|
||||
'rand' => '随机',
|
||||
]],
|
||||
['id' => 'style', 'type' => 'radio', 'label' => '列表样式', 'inline' => true, 'default' => 'default', 'options' => [
|
||||
'default' => '默认列表',
|
||||
'card' => '单行滚动卡片',
|
||||
]],
|
||||
['id' => 'count', 'type' => 'spinner', 'label' => '显示数量', 'default' => 6, 'min' => 4, 'max' => 20, 'step' => 1, 'unit' => '个'],
|
||||
];
|
||||
}
|
||||
|
||||
private static function moderator_fields(): array {
|
||||
return [
|
||||
['id' => 'title_text', 'type' => 'textarea', 'label' => '标题内容', 'default' => '版块版主', 'attributes' => ['rows' => 1]],
|
||||
['id' => 'apply_btn', 'type' => 'switcher', 'label' => '显示申请按钮', 'default' => true],
|
||||
];
|
||||
}
|
||||
|
||||
private static function topic_fields(): array {
|
||||
return [
|
||||
['id' => 'include', 'type' => 'terms', 'label' => '显示话题', 'taxonomy' => ['jio_forum_topic'], 'default' => [], 'description' => '仅显示所选话题,留空则显示全部话题'],
|
||||
['id' => 'orderby', 'type' => 'select', 'label' => '排序方式', 'default' => 'count', 'options' => [
|
||||
'name' => '名称排序',
|
||||
'count' => '帖子数量',
|
||||
'include' => '手动排序(上方选项不为空时才有效)',
|
||||
]],
|
||||
['id' => 'hide_empty', 'type' => 'switcher', 'label' => '排除帖子为空的话题', 'default' => true],
|
||||
['id' => 'count', 'type' => 'spinner', 'label' => '显示数量(0 为显示全部)', 'default' => 6, 'min' => 0, 'max' => 20, 'step' => 1, 'unit' => '个'],
|
||||
];
|
||||
}
|
||||
|
||||
/** Filters shared by the posts list and every tab of the tab list. */
|
||||
private static function post_filter_fields(): array {
|
||||
return [
|
||||
['id' => 'current_plate', 'type' => 'switcher', 'label' => '仅显示当前版块的帖子', 'default' => false, 'description' => '放置在版块页面时按当前版块筛选;开启该模块只会在版块和帖子页面显示'],
|
||||
['id' => 'include_plate', 'type' => 'text', 'label' => '包含版块', 'default' => '', 'sanitize' => 'ids', 'description' => '填写版块 ID,多个用逗号隔开'],
|
||||
['id' => 'exclude_plate', 'type' => 'text', 'label' => '排除版块', 'default' => '', 'sanitize' => 'ids', 'description' => '填写需要排除的版块 ID,多个用逗号隔开'],
|
||||
['id' => 'include_topic', 'type' => 'text', 'label' => '包含话题', 'default' => '', 'sanitize' => 'ids', 'description' => '填写话题 ID,多个用逗号隔开'],
|
||||
['id' => 'include_tag', 'type' => 'text', 'label' => '包含标签', 'default' => '', 'sanitize' => 'ids', 'description' => '填写论坛标签 ID,多个用逗号隔开'],
|
||||
['id' => 'bbs_type', 'type' => 'checkbox', 'label' => '类型筛选', 'inline' => true, 'default' => [], 'options' => ['normal' => '标准', 'question' => '提问', 'vote' => '投票']],
|
||||
['id' => 'filter', 'type' => 'checkbox', 'label' => '其它筛选', 'inline' => true, 'default' => [], 'options' => [
|
||||
'topping' => '置顶帖子',
|
||||
'vote' => '投票帖子',
|
||||
'essence' => '精华帖子',
|
||||
'question_status' => '提问已解决',
|
||||
]],
|
||||
];
|
||||
}
|
||||
|
||||
private static function orderby_field(): array {
|
||||
return ['id' => 'orderby', 'type' => 'select', 'label' => '排序方式', 'default' => 'date', 'options' => [
|
||||
'name' => '标题名称',
|
||||
'date' => '最新发布',
|
||||
'modified' => '最近更新',
|
||||
'last_reply' => '最新回复',
|
||||
'views' => '最多查看',
|
||||
'score' => '评分最高',
|
||||
'comment_count' => '最多回复',
|
||||
'favorite_count' => '最多收藏',
|
||||
'rand' => '随机',
|
||||
]];
|
||||
}
|
||||
|
||||
private static function paginate_field(): array {
|
||||
return ['id' => 'paginate', 'type' => 'radio', 'label' => '翻页按钮', 'inline' => true, 'default' => 'none', 'options' => [
|
||||
'none' => '不允许翻页',
|
||||
'ajax_lists' => 'AJAX追加列表翻页',
|
||||
'default' => '数字翻页按钮',
|
||||
]];
|
||||
}
|
||||
}
|
||||
@@ -33,16 +33,23 @@ final class FrontendRenderer {
|
||||
self::render_reference_widget($definition, $instance, $widget, $content, true);
|
||||
return;
|
||||
}
|
||||
if (in_array($definition->slug(), ['user_card', 'author_card', 'tab_posts', 'posts'], true)) {
|
||||
if (in_array($definition->slug(), ['user_card', 'author_card', 'tab_posts', 'posts'], true)
|
||||
|| in_array($definition->slug(), ['forum_posts', 'forum_tab_posts'], true)) {
|
||||
self::render_reference_widget($definition, $instance, $widget, $content);
|
||||
return;
|
||||
}
|
||||
if (in_array($definition->slug(), ['forum_plate_info', 'forum_moderators'], true)) {
|
||||
// Reference: the plate card and the moderator box ship their own
|
||||
// surface and title, so they only take the wrapper layer.
|
||||
self::render_reference_widget($definition, $instance, $widget, $content, true, true);
|
||||
return;
|
||||
}
|
||||
if (in_array($definition->slug(), [
|
||||
'post_pay', 'oneline_posts', 'hot_posts',
|
||||
'slider', 'slider_media_combo', 'icon_cards', 'icon_cover_cards',
|
||||
'buttons', 'notice', 'term_cards', 'term_aggregation', 'media_cards',
|
||||
'combo_info', 'partners', 'recent_comments',
|
||||
], true)) {
|
||||
], true) || in_array($definition->slug(), ['forum_plates', 'forum_topics'], true)) {
|
||||
echo (string) ($args['before_widget'] ?? '');
|
||||
if ($definition->slug() !== 'post_pay') echo self::reference_title($instance);
|
||||
$content_classes = ['widget-content'];
|
||||
@@ -100,7 +107,7 @@ final class FrontendRenderer {
|
||||
* 搜索框 / 一言自带卡片(.zib-widget.widget-search / .yiyan-box),
|
||||
* 此时不再套 .widget-content 与第二层卡片;其余模块保持 .widget-content 结构层。
|
||||
*/
|
||||
private static function render_reference_widget(WidgetDefinition $definition, array $instance, SchemaWidget $widget, string $content, bool $own_card = false): void {
|
||||
private static function render_reference_widget(WidgetDefinition $definition, array $instance, SchemaWidget $widget, string $content, bool $own_card = false, bool $own_title = false): void {
|
||||
$classes = ['jio-widget-box-wrap', 'jio-' . str_replace('_', '-', $definition->slug()) . '-wrap'];
|
||||
if (in_array($definition->slug(), ['user_card', 'author_card'], true)) {
|
||||
$classes[] = 'jio-user-widget-wrap';
|
||||
@@ -127,7 +134,7 @@ final class FrontendRenderer {
|
||||
if ($own_card) {
|
||||
// 参考主题:搜索框/一言的中间层是 .theme-box,承载 20px 底部间距与入场动画。
|
||||
echo '<div class="widget-container"><div class="theme-box' . esc_attr($animation) . '">';
|
||||
echo self::reference_title($instance); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
if (!$own_title) echo self::reference_title($instance); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo '</div></div></div>';
|
||||
return;
|
||||
|
||||
@@ -37,6 +37,9 @@ final class RendererRegistry {
|
||||
'slider' => SliderRenderer::class, 'slider_media_combo' => SliderRenderer::class,
|
||||
'user_card' => UserRenderer::class, 'author_card' => UserRenderer::class,
|
||||
'user_ranking' => UserRenderer::class, 'user_list' => UserRenderer::class,
|
||||
'forum_posts' => \Jio\Forum\WidgetRenderer::class, 'forum_tab_posts' => \Jio\Forum\WidgetRenderer::class,
|
||||
'forum_plates' => \Jio\Forum\WidgetRenderer::class, 'forum_plate_info' => \Jio\Forum\WidgetRenderer::class,
|
||||
'forum_moderators' => \Jio\Forum\WidgetRenderer::class, 'forum_topics' => \Jio\Forum\WidgetRenderer::class,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -49,6 +52,6 @@ final class RendererRegistry {
|
||||
}
|
||||
|
||||
public static function supports_items(string $slug): bool {
|
||||
return in_array($slug, ['posts', 'tab_posts', 'oneline_posts'], true);
|
||||
return in_array($slug, ['posts', 'tab_posts', 'oneline_posts'], true) || in_array($slug, \Jio\Forum\WidgetRenderer::SUPPORTS_ITEMS, true);
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,9 @@ final class RestController extends \WP_REST_Controller {
|
||||
}
|
||||
$page = max(1, (int) $request->get_param('page'));
|
||||
$segment = (string) $request->get_param('segment');
|
||||
$payload = PostRenderer::payload($instance, $slug, $page, $segment);
|
||||
$payload = in_array($slug, \Jio\Forum\WidgetRenderer::SUPPORTS_ITEMS, true)
|
||||
? \Jio\Forum\WidgetQuery::payload($slug, $instance, $page, $segment)
|
||||
: PostRenderer::payload($instance, $slug, $page, $segment);
|
||||
return rest_ensure_response(['success' => true, 'code' => 'ok'] + $payload);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Jio\WidgetSystem;
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Canonical catalog of the 37 modern Jio widgets.
|
||||
* Canonical catalog of the modern Jio widgets, including the forum group.
|
||||
*/
|
||||
final class WidgetCatalog {
|
||||
public static function definitions(): array {
|
||||
@@ -57,11 +57,26 @@ final class WidgetCatalog {
|
||||
['user_ranking', '用户排行榜', '按签到、积分或粉丝生成排行', 'sidebar'],
|
||||
['user_list', '用户列表', '筛选并显示注册用户列表', 'wide'],
|
||||
],
|
||||
'forum' => [
|
||||
['forum_posts', '论坛帖子列表', '按版块、话题、标签、类型筛选论坛帖子,支持多种排序、样式与翻页', 'sidebar'],
|
||||
['forum_tab_posts', '论坛多栏目帖子', '多个栏目切换显示论坛帖子列表', 'wide'],
|
||||
['forum_plates', '论坛版块列表', '显示论坛版块,支持分类筛选、排序与卡片样式', 'sidebar'],
|
||||
['forum_plate_info', '论坛版块信息', '显示当前版块的信息,只能放在版块或帖子页面侧边栏', 'sidebar'],
|
||||
['forum_moderators', '论坛版主列表', '显示当前版块的版主与副版主,只能放在版块或帖子页面侧边栏', 'sidebar'],
|
||||
['forum_topics', '论坛话题列表', '显示论坛话题及帖子数量,可排除空话题', 'sidebar'],
|
||||
],
|
||||
];
|
||||
|
||||
$definitions = [];
|
||||
// Forum widgets exist only while the forum module is on; standalone
|
||||
// harnesses have no settings layer, so they keep their own catalog.
|
||||
$forum_on = function_exists('jio_get_setting') && \Jio\Forum\Core::enabled();
|
||||
foreach ($groups as $group => $items) {
|
||||
if ($group === 'forum' && !$forum_on) {
|
||||
continue;
|
||||
}
|
||||
foreach ($items as [$slug, $name, $description, $size]) {
|
||||
$forum = str_starts_with($slug, 'forum_');
|
||||
$definitions[] = [
|
||||
'slug' => $slug,
|
||||
'name' => 'Jio ' . $name,
|
||||
@@ -69,14 +84,14 @@ final class WidgetCatalog {
|
||||
'group' => $group,
|
||||
'renderer' => $slug,
|
||||
'size' => $size,
|
||||
'common' => [
|
||||
'common' => $forum ? self::forum_common($slug) : [
|
||||
'title' => !in_array($slug, ['post_pay', 'layout_gap', 'slider', 'slider_media_combo', 'buttons', 'posts_toc'], true),
|
||||
'affix' => ($size === 'sidebar' && $slug !== 'posts_toc') || in_array($slug, ['buttons', 'notice', 'posts'], true),
|
||||
'visibility' => $slug !== 'posts_toc',
|
||||
'layout' => $slug !== 'posts_toc',
|
||||
'animation' => !in_array($slug, ['buttons', 'term_cards', 'term_aggregation', 'media_cards', 'combo_info', 'slider_media_combo', 'posts_toc'], true),
|
||||
],
|
||||
'fields' => WidgetSchemas::for($slug),
|
||||
'fields' => $forum ? \Jio\Forum\WidgetSchemas::for($slug) : WidgetSchemas::for($slug),
|
||||
'assets' => self::assets($slug),
|
||||
'title_default' => $slug === 'search' ? '搜索' : '',
|
||||
];
|
||||
@@ -85,6 +100,18 @@ final class WidgetCatalog {
|
||||
return $definitions;
|
||||
}
|
||||
|
||||
/** Forum widgets reuse the reference flags; plate-scoped ones hide themselves off their pages. */
|
||||
private static function forum_common(string $slug): array {
|
||||
$scoped = in_array($slug, ['forum_plate_info', 'forum_moderators'], true);
|
||||
return [
|
||||
'title' => !in_array($slug, ['forum_plate_info'], true),
|
||||
'affix' => $slug !== 'forum_tab_posts',
|
||||
'visibility' => !$scoped,
|
||||
'layout' => true,
|
||||
'animation' => $slug !== 'forum_tab_posts',
|
||||
];
|
||||
}
|
||||
|
||||
private static function assets(string $slug): array {
|
||||
if (in_array($slug, ['slider', 'slider_media_combo', 'reviews', 'oneline_posts'], true)) {
|
||||
return ['swiper'];
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..')
|
||||
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
|
||||
|
||||
const SLUGS = ['forum_posts', 'forum_tab_posts', 'forum_plates', 'forum_plate_info', 'forum_moderators', 'forum_topics']
|
||||
|
||||
test('every reference forum widget is catalogued with a schema and a renderer', () => {
|
||||
const catalog = read('src/WidgetSystem/WidgetCatalog.php')
|
||||
const registry = read('src/WidgetSystem/RendererRegistry.php')
|
||||
const schemas = read('src/Forum/WidgetSchemas.php')
|
||||
const renderer = read('src/Forum/WidgetRenderer.php')
|
||||
for (const slug of SLUGS) {
|
||||
assert.ok(catalog.includes(`['${slug}',`), `${slug} missing from the widget catalog`)
|
||||
assert.ok(registry.includes(`'${slug}' => \\Jio\\Forum\\WidgetRenderer::class`), `${slug} missing from the renderer registry`)
|
||||
}
|
||||
// The plate info card is configuration-free, like the reference widget.
|
||||
for (const slug of SLUGS.filter((slug) => slug !== 'forum_plate_info')) {
|
||||
assert.ok(schemas.includes(`case '${slug}':`), `${slug} missing a field schema`)
|
||||
}
|
||||
for (const slug of SLUGS) {
|
||||
assert.ok(renderer.includes(`case '${slug}':`), `${slug} renderer missing`)
|
||||
}
|
||||
assert.match(catalog, /\$forum \? \\Jio\\Forum\\WidgetSchemas::for\(\$slug\) : WidgetSchemas::for\(\$slug\)/)
|
||||
})
|
||||
|
||||
test('forum widgets reuse the shared reference cards instead of forking markup', () => {
|
||||
const renderer = read('src/Forum/WidgetRenderer.php')
|
||||
for (const helper of ['Cards::post_detail(', 'Cards::post_mini(', 'Cards::title(', 'Cards::plate_item(', 'Cards::plate_slide(', 'Cards::follow_button(']) {
|
||||
assert.ok(renderer.includes(helper), `forum widgets must reuse ${helper}`)
|
||||
}
|
||||
assert.doesNotMatch(renderer, /zib|zibll|Qinver/i)
|
||||
})
|
||||
|
||||
test('forum widget lists paginate through the widget REST client contract', () => {
|
||||
const renderer = read('src/Forum/WidgetRenderer.php')
|
||||
const query = read('src/Forum/WidgetQuery.php')
|
||||
const registry = read('src/WidgetSystem/RendererRegistry.php')
|
||||
const rest = read('src/WidgetSystem/RestController.php')
|
||||
// The container carries the attributes frontend.js reads, with the skeleton inside it.
|
||||
assert.match(renderer, /class="widget-ajaxpager"/)
|
||||
assert.match(renderer, /data-widget-items data-widget=/)
|
||||
assert.match(renderer, /post_ajax_loader/)
|
||||
assert.doesNotMatch(renderer, /post_ajax_trigger/)
|
||||
// Item payloads resolve through the forum query builder, not the article one.
|
||||
assert.match(rest, /\\Jio\\Forum\\WidgetQuery::payload/)
|
||||
assert.match(registry, /WidgetRenderer::SUPPORTS_ITEMS/)
|
||||
assert.match(query, /jio_forum_list/)
|
||||
// Pagination exposes the widget client's page hooks and the reference modes.
|
||||
assert.match(query, /data-widget-page=/)
|
||||
assert.match(query, /data-next-page=/)
|
||||
assert.match(query, /ajaxpager-target="\.widget-ajaxpager"/)
|
||||
const schemas = read('src/Forum/WidgetSchemas.php')
|
||||
assert.match(schemas, /'ajax_lists'/)
|
||||
assert.match(schemas, /'default' => '数字翻页按钮'/)
|
||||
})
|
||||
|
||||
test('plate-scoped forum widgets stay silent outside plate and post pages', () => {
|
||||
const renderer = read('src/Forum/WidgetRenderer.php')
|
||||
const core = read('src/Forum/Core.php')
|
||||
assert.match(core, /public static function current_plate_id\(\)/)
|
||||
for (const method of ['plate_info', 'moderators']) {
|
||||
const start = renderer.indexOf(`private static function ${method}(`)
|
||||
const body = renderer.slice(start, renderer.indexOf('private static function', start + 10))
|
||||
assert.match(body, /Core::current_plate_id\(\)/)
|
||||
assert.match(body, /Core::PLATE/)
|
||||
}
|
||||
})
|
||||
|
||||
test('forum widgets load the forum card styles and runtime on any sidebar', () => {
|
||||
const core = read('src/Forum/Core.php')
|
||||
const runtime = read('assets/js/forum-runtime.js')
|
||||
assert.match(core, /add_action\('wp_enqueue_scripts', \[self::class, 'widget_assets'\]/)
|
||||
assert.match(core, /public static function enqueue_css\(\)/)
|
||||
assert.match(core, /is_active_widget\(false, false, 'jio_widget_' \. \$slug, true\)/)
|
||||
assert.match(runtime, /document\.getElementById\('forum'\) \|\| document\.body/)
|
||||
})
|
||||
|
||||
test('forum widgets keep the module off until it is enabled', () => {
|
||||
const catalog = read('src/WidgetSystem/WidgetCatalog.php')
|
||||
assert.match(catalog, /if \(\$group === 'forum' && !\$forum_on\)/)
|
||||
assert.match(catalog, /function_exists\('jio_get_setting'\) && \\Jio\\Forum\\Core::enabled\(\)/)
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generated_at": "2026-11-11T06:33:58+00:00",
|
||||
"generated_at": "2026-11-11T07:46:58+00:00",
|
||||
"errors": [],
|
||||
"warnings": [
|
||||
"customPage tools-update section 'tools/update' not in PHP schema (ok if pure operational page)",
|
||||
|
||||
Reference in new issue
Block a user