From 2396ca98fbcb5f5cd80fb4f6dd991c9d1fec61fe Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 20 Jul 2026 22:57:10 +0800 Subject: [PATCH] feat(forum): plate info follow/compose + mods grid + board depth Sidebar plate widgets gain follow/compose actions, super-mod roles, board list sorting/styles, and matching CSS polish. --- assets/css/frontend/widget-batch5.css | 96 +++++++ src/AKSRT/Widgets/WidgetForumBoardList.php | 249 ++++++++++++------ src/AKSRT/Widgets/WidgetForumPlateInfo.php | 115 ++++++-- .../Widgets/WidgetForumPlateModerator.php | 113 ++++---- 4 files changed, 424 insertions(+), 149 deletions(-) diff --git a/assets/css/frontend/widget-batch5.css b/assets/css/frontend/widget-batch5.css index 835d1ac8..3fc342b3 100644 --- a/assets/css/frontend/widget-batch5.css +++ b/assets/css/frontend/widget-batch5.css @@ -787,3 +787,99 @@ overflow: hidden; text-overflow: ellipsis; } + +/* ---- batch9: plate info actions / mods grid / board list / term polish ---- */ +.aksrt-plate-info__cover.is-icon { + display: flex; + align-items: center; + justify-content: center; + font-size: 32px; + color: var(--color-primary, #3b82f6); + background: linear-gradient(135deg, #eff6ff, #f5f3ff); +} +.aksrt-plate-info__actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} +.aksrt-plate-info__btn { + display: inline-flex; + align-items: center; + gap: 6px; + border: 0; + border-radius: 999px; + padding: 6px 12px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + text-decoration: none; + line-height: 1.2; +} +.aksrt-plate-info__follow { + background: #ef4444; + color: #fff !important; +} +.aksrt-plate-info__follow.is-followed { + background: var(--surface-2, #e5e7eb); + color: inherit !important; +} +.aksrt-plate-info__compose { + background: var(--color-primary, #3b82f6); + color: #fff !important; +} + +.aksrt-plate-mods.style-grid .aksrt-plate-mods__list { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; +} +.aksrt-plate-mods.style-grid .aksrt-plate-mods__item { + flex-direction: column; + text-align: center; + gap: 4px; +} +.aksrt-plate-mods.style-grid .aksrt-plate-mods__name { + font-size: 12px; + width: 100%; +} +.aksrt-plate-mods.style-grid .aksrt-plate-mods__role { + font-size: 10px; +} +.aksrt-plate-mods__item.role-author .aksrt-plate-mods__role, +.aksrt-plate-mods__item.role-super .aksrt-plate-mods__role { + color: #b45309; +} + +.aksrt-forum-widget__boards .aksrt-forum-widget__name { + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.aksrt-forum-widget__boards .aksrt-forum-widget__count { + margin-left: 8px; + color: var(--muted-2-color, #999); + font-size: 12px; + flex-shrink: 0; +} +.aksrt-forum-widget__boards.style-mini .aksrt-forum-widget__board a { + gap: 8px; +} +.aksrt-forum-widget__thumb { + width: 28px; + height: 28px; + border-radius: 6px; + object-fit: cover; + flex-shrink: 0; + margin-right: 4px; +} + +.aksrt-tag-cloud .c-blue-2 { background: #bfdbfe; color: #1e40af; } +.aksrt-tag-cloud .c-green-2 { background: #bbf7d0; color: #166534; } +.aksrt-tag-cloud .c-yellow-2 { background: #fde68a; color: #92400e; } +.aksrt-tag-cloud .c-purple-2 { background: #ddd6fe; color: #5b21b6; } +.aksrt-tag-cloud .c-red-2 { background: #fecaca; color: #991b1b; } + +.aksrt-home-widget__ajax .ajax-pag { margin-top: 12px; text-align: center; } diff --git a/src/AKSRT/Widgets/WidgetForumBoardList.php b/src/AKSRT/Widgets/WidgetForumBoardList.php index 5cd9d5d0..6127f973 100644 --- a/src/AKSRT/Widgets/WidgetForumBoardList.php +++ b/src/AKSRT/Widgets/WidgetForumBoardList.php @@ -1,5 +1,7 @@ __('显示论坛版块列表及话题数量。', 'aksrt')] - ); - } + public function __construct() { + parent::__construct( + 'aksrt_forum_board_list', + __('论坛 - 版块列表', 'aksrt'), + ['description' => __('显示论坛版块列表及话题数量。', 'aksrt')] + ); + } - public function widget($args, $instance): void { - $title = !empty($instance['title']) ? $instance['title'] : __('版块列表', 'aksrt'); + public static function render_html(array $instance): string { + $limit = max(1, min(50, (int) ($instance['count'] ?? $instance['showposts'] ?? 30))); + $orderby = sanitize_key((string) ($instance['orderby'] ?? 'menu_order')); + $style = sanitize_key((string) ($instance['style'] ?? 'list')); + if (!in_array($style, ['list', 'mini'], true)) { + $style = 'list'; + } - echo $args['before_widget']; - echo $args['before_title'] . esc_html($title) . $args['after_title']; + // Prefer plate CPT (same as plate_lists lite). + if (post_type_exists('aksrt_forum_plate')) { + $query_args = [ + 'post_type' => 'aksrt_forum_plate', + 'post_status' => 'publish', + 'posts_per_page' => $limit, + 'order' => 'ASC', + 'no_found_rows' => true, + ]; + switch ($orderby) { + case 'posts': + $query_args['meta_key'] = '_aksrt_posts_count'; + $query_args['orderby'] = 'meta_value_num'; + $query_args['order'] = 'DESC'; + break; + case 'views': + $query_args['meta_key'] = '_aksrt_views'; + $query_args['orderby'] = 'meta_value_num'; + $query_args['order'] = 'DESC'; + break; + case 'date': + $query_args['orderby'] = 'date'; + $query_args['order'] = 'DESC'; + break; + default: + $query_args['orderby'] = 'menu_order title'; + } + $plates = get_posts($query_args); + if ($plates) { + ob_start(); + echo ''; + return (string) ob_get_clean(); + } + } - $rendered = false; - if (post_type_exists('aksrt_forum_plate')) { - $plates = get_posts([ - 'post_type' => 'aksrt_forum_plate', - 'post_status' => 'publish', - 'posts_per_page' => 30, - 'orderby' => 'menu_order title', - 'order' => 'ASC', - ]); - if ($plates) { - $rendered = true; - echo ''; - } - } - if (!$rendered) { - $boards = get_terms([ - 'taxonomy' => 'aksrt_forum_board', - 'hide_empty' => true, - 'orderby' => 'meta_value_num', - 'meta_key' => '_aksrt_sort_order', - 'order' => 'ASC', - ]); - if (!is_wp_error($boards) && !empty($boards)) { - echo ''; - } else { - echo '

' . esc_html__('暂无版块', 'aksrt') . '

'; - } - } + // Taxonomy fallback. + $term_args = [ + 'taxonomy' => 'aksrt_forum_board', + 'hide_empty' => !isset($instance['hide_empty']) || !empty($instance['hide_empty']), + 'number' => $limit, + 'order' => 'ASC', + ]; + if ($orderby === 'count') { + $term_args['orderby'] = 'count'; + $term_args['order'] = 'DESC'; + } elseif ($orderby === 'name') { + $term_args['orderby'] = 'name'; + } else { + $term_args['orderby'] = 'meta_value_num'; + $term_args['meta_key'] = '_aksrt_sort_order'; + } + $boards = get_terms($term_args); + if (is_wp_error($boards) || empty($boards)) { + return function_exists('aksrt_get_ajax_null') + ? aksrt_get_ajax_null(__('暂无版块', 'aksrt')) + : '

' . esc_html__('暂无版块', 'aksrt') . '

'; + } + ob_start(); + echo ''; + return (string) ob_get_clean(); + } - echo $args['after_widget']; - } + public function widget($args, $instance): void { + $title = !empty($instance['title']) ? $instance['title'] : __('版块列表', 'aksrt'); + $html = self::render_html($instance); - public function form($instance): void { - $title = !empty($instance['title']) ? $instance['title'] : __('版块列表', 'aksrt'); - ?> -

- - -

- +

+ + +

+

+ +

+

+ +

+

+ +

+

+ +

+ sanitize_text_field($new_instance['title'] ?? ''), + 'count' => max(1, min(50, (int) ($new_instance['count'] ?? 30))), + 'orderby' => $orderby, + 'style' => ($new_instance['style'] ?? '') === 'mini' ? 'mini' : 'list', + 'hide_empty' => !empty($new_instance['hide_empty']) ? 1 : 0, + ]; + } } diff --git a/src/AKSRT/Widgets/WidgetForumPlateInfo.php b/src/AKSRT/Widgets/WidgetForumPlateInfo.php index b0bf8b8b..0a1a2957 100644 --- a/src/AKSRT/Widgets/WidgetForumPlateInfo.php +++ b/src/AKSRT/Widgets/WidgetForumPlateInfo.php @@ -22,41 +22,96 @@ class WidgetForumPlateInfo extends \WP_Widget { ); } - public function widget($args, $instance): void { - $plate = $this->resolve_current_plate(); + /** + * @return array{html:string} + */ + public static function render_payload(array $instance, ?\WP_Post $plate = null): array { + $plate = $plate ?: self::resolve_current_plate_static(); if (!$plate) { - return; + return ['html' => '']; } - echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + $show_follow = !isset($instance['show_follow']) || !empty($instance['show_follow']); + $show_compose = !isset($instance['show_compose']) || !empty($instance['show_compose']); + $show_stats = !isset($instance['show_stats']) || !empty($instance['show_stats']); - $id = (int) $plate->ID; - $url = get_permalink($id); - $thumb = get_the_post_thumbnail_url($id, 'medium'); - $posts = (int) get_post_meta($id, '_aksrt_posts_count', true); - $reply = (int) get_post_meta($id, '_aksrt_reply_count', true); + $id = (int) $plate->ID; + $url = get_permalink($id); + $thumb = get_the_post_thumbnail_url($id, 'medium'); + $posts = (int) get_post_meta($id, '_aksrt_posts_count', true); + $reply = (int) get_post_meta($id, '_aksrt_reply_count', true); $follow = (int) get_post_meta($id, '_aksrt_follow_count', true); - $desc = $plate->post_excerpt !== '' ? $plate->post_excerpt : wp_trim_words(wp_strip_all_tags($plate->post_content), 30); + $views = (int) get_post_meta($id, '_aksrt_views', true); + $desc = $plate->post_excerpt !== '' ? $plate->post_excerpt : wp_trim_words(wp_strip_all_tags($plate->post_content), 30); + $icon = (string) get_post_meta($id, '_aksrt_icon', true); - echo '
'; + $forum = class_exists('\AKSRT\Forum\Forum') ? \AKSRT\Forum\Forum::get_instance() : null; + $uid = get_current_user_id(); + $is_followed = $forum && $uid > 0 ? $forum->is_user_following_plate($uid, $id) : false; + + $compose_url = function_exists('aksrt_get_forum_compose_url') + ? aksrt_get_forum_compose_url(['board' => $id]) + : home_url('/'); + + ob_start(); + echo '
'; if ($thumb) { echo '
'; + } elseif ($icon !== '') { + echo '
'; } echo '
'; echo '' . esc_html(get_the_title($plate)) . ''; if ($desc !== '') { echo '
' . esc_html($desc) . '
'; } - echo '
'; - echo ' ' . esc_html((string) $posts) . ''; - echo ' ' . esc_html((string) $reply) . ''; - echo ' ' . esc_html((string) $follow) . ''; - echo '
'; + if ($show_stats) { + echo '
'; + echo ' ' . esc_html((string) $posts) . ''; + echo ' ' . esc_html((string) $reply) . ''; + if ($views > 0) { + echo ' ' . esc_html((string) $views) . ''; + } + echo ' ' . esc_html((string) $follow) . ''; + echo '
'; + } + $actions = []; + if ($show_follow && $uid > 0) { + $actions[] = sprintf( + '', + $is_followed ? ' is-followed' : '', + $id, + $is_followed ? '1' : '0', + esc_html($is_followed ? __('已关注', 'aksrt') : __('关注', 'aksrt')) + ); + } + if ($show_compose) { + $actions[] = ' ' . esc_html__('发帖', 'aksrt') . ''; + } + if ($actions) { + echo '
' . implode('', $actions) . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + echo '
'; + return ['html' => (string) ob_get_clean()]; + } + + public function widget($args, $instance): void { + $payload = self::render_payload($instance); + if ($payload['html'] === '') { + return; + } + + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + $title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base); + if ($title !== '') { + echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } - private function resolve_current_plate(): ?\WP_Post { + public static function resolve_current_plate_static(): ?\WP_Post { if (is_singular('aksrt_forum_plate')) { $p = get_queried_object(); return $p instanceof \WP_Post ? $p : null; @@ -65,7 +120,6 @@ class WidgetForumPlateInfo extends \WP_Widget { $topic_id = (int) get_queried_object_id(); $plate_id = (int) get_post_meta($topic_id, '_aksrt_plate_id', true); if ($plate_id <= 0) { - // Fallback: first linked board term → plate meta, or parent. $boards = get_the_terms($topic_id, 'aksrt_forum_board'); if (!is_wp_error($boards) && !empty($boards[0])) { $linked = (int) get_term_meta((int) $boards[0]->term_id, '_aksrt_plate_id', true); @@ -84,11 +138,32 @@ class WidgetForumPlateInfo extends \WP_Widget { return null; } + private function resolve_current_plate(): ?\WP_Post { + return self::resolve_current_plate_static(); + } + public function form($instance): void { - echo '

' . esc_html__('无需配置。仅在版块页或帖子页显示对应版块信息。', 'aksrt') . '

'; + ?> +

+ +

+

+
+
+ +

+

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'show_stats' => !empty($new_instance['show_stats']) ? 1 : 0, + 'show_follow' => !empty($new_instance['show_follow']) ? 1 : 0, + 'show_compose' => !empty($new_instance['show_compose']) ? 1 : 0, + ]; } } diff --git a/src/AKSRT/Widgets/WidgetForumPlateModerator.php b/src/AKSRT/Widgets/WidgetForumPlateModerator.php index d2fe667d..5ad4a27f 100644 --- a/src/AKSRT/Widgets/WidgetForumPlateModerator.php +++ b/src/AKSRT/Widgets/WidgetForumPlateModerator.php @@ -22,45 +22,65 @@ class WidgetForumPlateModerator extends \WP_Widget { ); } - public function widget($args, $instance): void { - $plate = $this->resolve_current_plate(); + /** + * @return array{html:string} + */ + public static function render_payload(array $instance, ?\WP_Post $plate = null): array { + $plate = $plate ?: WidgetForumPlateInfo::resolve_current_plate_static(); if (!$plate) { - return; + return ['html' => '']; } - $id = (int) $plate->ID; + $id = (int) $plate->ID; $author_id = (int) $plate->post_author; - $mods = get_post_meta($id, '_aksrt_moderators', true); + $mods = get_post_meta($id, '_aksrt_moderators', true); if (!is_array($mods)) { $mods = []; } $mods = array_values(array_unique(array_filter(array_map('intval', $mods)))); + // Super-mods meta (zibll-like). + $super = get_post_meta($id, '_aksrt_super_moderators', true); + if (!is_array($super)) { + $super = []; + } + $super = array_values(array_unique(array_filter(array_map('intval', $super)))); + $people = []; if ($author_id > 0) { $people[] = ['id' => $author_id, 'role' => 'author']; } + foreach ($super as $sid) { + if ($sid === $author_id) { + continue; + } + $people[] = ['id' => $sid, 'role' => 'super']; + } foreach ($mods as $mid) { - if ($mid === $author_id) { + if ($mid === $author_id || in_array($mid, $super, true)) { continue; } $people[] = ['id' => $mid, 'role' => 'moderator']; } if (!$people) { - return; + return ['html' => '']; } - $title_text = (string) ($instance['title_text'] ?? __('版主', 'aksrt')); + $title_text = (string) ($instance['title_text'] ?? __('版主', 'aksrt')); + $show_avatar = !isset($instance['show_avatar']) || !empty($instance['show_avatar']); + $style = sanitize_key((string) ($instance['style'] ?? 'list')); + if (!in_array($style, ['list', 'grid'], true)) { + $style = 'list'; + } - echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - - echo '
'; + ob_start(); + echo '
'; if ($title_text !== '') { echo '
' . esc_html($title_text) . '
'; } echo '
'; foreach ($people as $row) { - $uid = (int) $row['id']; + $uid = (int) $row['id']; $user = get_userdata($uid); if (!$user) { continue; @@ -71,61 +91,66 @@ class WidgetForumPlateModerator extends \WP_Widget { $home = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($uid) : get_author_posts_url($uid); - $role_label = $row['role'] === 'author' - ? __('超级版主', 'aksrt') - : __('版主', 'aksrt'); - echo ''; - echo ''; + if ($row['role'] === 'author' || $row['role'] === 'super') { + $role_label = __('超级版主', 'aksrt'); + } else { + $role_label = __('版主', 'aksrt'); + } + echo ''; + if ($show_avatar) { + echo ''; + } echo '' . esc_html($user->display_name) . ''; echo '' . esc_html($role_label) . ''; echo ''; } echo '
'; - - echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + return ['html' => (string) ob_get_clean()]; } - private function resolve_current_plate(): ?\WP_Post { - if (is_singular('aksrt_forum_plate')) { - $p = get_queried_object(); - return $p instanceof \WP_Post ? $p : null; + public function widget($args, $instance): void { + $payload = self::render_payload($instance); + if ($payload['html'] === '') { + return; } - if (is_singular('aksrt_forum_topic')) { - $topic_id = (int) get_queried_object_id(); - $plate_id = (int) get_post_meta($topic_id, '_aksrt_plate_id', true); - if ($plate_id <= 0) { - $boards = get_the_terms($topic_id, 'aksrt_forum_board'); - if (!is_wp_error($boards) && !empty($boards[0])) { - $linked = (int) get_term_meta((int) $boards[0]->term_id, '_aksrt_plate_id', true); - if ($linked > 0) { - $plate_id = $linked; - } - } - } - if ($plate_id > 0) { - $p = get_post($plate_id); - if ($p instanceof \WP_Post && $p->post_type === 'aksrt_forum_plate') { - return $p; - } - } + + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + // title_text is rendered inside payload head (zibll-like); optional WP title still allowed. + $title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base); + if ($title !== '') { + echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } - return null; + echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } public function form($instance): void { ?>

-

+

+ +

+

+ +

sanitize_text_field((string) ($new_instance['title_text'] ?? '')), + 'title_text' => sanitize_text_field((string) ($new_instance['title_text'] ?? '')), + 'style' => ($new_instance['style'] ?? '') === 'grid' ? 'grid' : 'list', + 'show_avatar' => !empty($new_instance['show_avatar']) ? 1 : 0, ]; } }