feat(comments): AJAX fragment list/submit + load-more parity

Add REST comment/list + comment/submit HTML fragments, shared query
renderer for order/author/hottest, fix load-more policy and DOM
targets, and stop full-page scrapes for order/author/reload.
This commit is contained in:
2026-07-12 08:59:50 +08:00
parent 0342aad9fb
commit 00d335ba16
8 changed files with 648 additions and 222 deletions
+253
View File
@@ -142,6 +142,259 @@ if (!function_exists('aksrt_comments_apply_hottest_order')) :
}
endif;
if (!function_exists('aksrt_comments_normalize_order')) :
/**
* Normalize comment order key.
*/
function aksrt_comments_normalize_order(string $order = ''): string {
$order = sanitize_key($order);
if (!in_array($order, ['newest', 'oldest', 'hottest'], true)) {
$settings = class_exists('AKSRT_Settings')
? AKSRT_Settings::get_comment_display_settings()
: [];
$order = (($settings['order'] ?? 'desc') === 'asc') ? 'oldest' : 'newest';
}
if (!in_array($order, ['newest', 'oldest', 'hottest'], true)) {
$order = 'newest';
}
return $order;
}
endif;
if (!function_exists('aksrt_comments_query_for_post')) :
/**
* Shared comment query for first paint / fragment / load-more.
*
* @return array{comments:array<int,\WP_Comment>,total:int,max_pages:int,page:int,order:string,author_only:bool,settings:array}
*/
function aksrt_comments_query_for_post(int $post_id, array $args = []): array {
$settings = class_exists('AKSRT_Settings')
? AKSRT_Settings::get_comment_display_settings()
: [];
$order = aksrt_comments_normalize_order((string) ($args['order'] ?? ''));
$author_only = !empty($args['author_only']);
$page = max(1, (int) ($args['page'] ?? 1));
$per_page = max(1, (int) ($args['per_page'] ?? get_option('comments_per_page', 10)));
$include_unapproved_for_user = (int) ($args['include_unapproved_user'] ?? 0);
$query_args = [
'post_id' => $post_id,
'status' => 'approve',
'number' => $per_page,
'offset' => ($page - 1) * $per_page,
'orderby' => 'comment_date_gmt',
'order' => $order === 'oldest' ? 'ASC' : 'DESC',
'hierarchical' => 'threaded',
];
if ($author_only) {
$author_id = (int) get_post_field('post_author', $post_id);
if ($author_id > 0) {
$query_args['user_id'] = $author_id;
$query_args['hierarchical'] = false;
}
}
if ($include_unapproved_for_user > 0) {
// WP_Comment_Query does not support mixed status easily; keep approve only for list fragment.
}
// Hottest uses meta_value_num when meta exists; fall back to date.
$hottest_filter = null;
if ($order === 'hottest') {
$query_args['orderby'] = 'meta_value_num';
$query_args['meta_key'] = 'aksrt_comment_likes';
$query_args['order'] = 'DESC';
// Ensure comments without like meta still appear.
$query_args['meta_query'] = [
'relation' => 'OR',
[
'key' => 'aksrt_comment_likes',
'compare' => 'EXISTS',
],
[
'key' => 'aksrt_comment_likes',
'compare' => 'NOT EXISTS',
],
];
// Prefer clauses filter for stable hottest ordering.
$hottest_filter = static function ($clauses) {
global $wpdb;
if (strpos($clauses['join'], 'aksrt_cm_hot') === false) {
$clauses['join'] .= " LEFT JOIN {$wpdb->commentmeta} AS aksrt_cm_hot ON ({$wpdb->comments}.comment_ID = aksrt_cm_hot.comment_id AND aksrt_cm_hot.meta_key = 'aksrt_comment_likes')";
}
$clauses['orderby'] = 'CAST(COALESCE(aksrt_cm_hot.meta_value, 0) AS UNSIGNED) DESC, ' . $wpdb->comments . '.comment_date_gmt DESC';
return $clauses;
};
add_filter('comments_clauses', $hottest_filter, 20);
// Prefer date orderby with clauses override.
$query_args['orderby'] = 'comment_date_gmt';
unset($query_args['meta_key'], $query_args['meta_query']);
}
$comment_query = new WP_Comment_Query($query_args);
$comments = is_array($comment_query->comments) ? $comment_query->comments : [];
if ($hottest_filter) {
remove_filter('comments_clauses', $hottest_filter, 20);
}
// Pin reorder for page 1 when not author-only.
if ($page === 1 && !$author_only && function_exists('aksrt_reorder_pinned_comments')) {
$comments = aksrt_reorder_pinned_comments($comments, $post_id);
}
$count_args = [
'post_id' => $post_id,
'status' => 'approve',
'count' => true,
];
if ($author_only) {
$author_id = (int) get_post_field('post_author', $post_id);
if ($author_id > 0) {
$count_args['user_id'] = $author_id;
}
}
$total = (int) get_comments($count_args);
$max_pages = (int) max(1, (int) ceil($total / $per_page));
return [
'comments' => $comments,
'total' => $total,
'max_pages' => $max_pages,
'page' => $page,
'order' => $order,
'author_only' => $author_only,
'settings' => $settings,
'per_page' => $per_page,
];
}
endif;
if (!function_exists('aksrt_comments_render_items_html')) :
/**
* Render comment list items HTML via canonical callback.
*
* @param array<int,\WP_Comment> $comments
* @param array<string,mixed> $settings
* @param string $order
*/
function aksrt_comments_render_items_html(array $comments, array $settings = [], string $order = 'newest'): string {
if (empty($comments) || !function_exists('aksrt_comment_callback')) {
return '';
}
set_query_var('aksrt_comment_settings', $settings);
set_query_var('aksrt_comment_order', aksrt_comments_normalize_order($order));
$avatar = max(24, (int) ($settings['avatar_size'] ?? 48));
$depth = max(1, (int) ($settings['nested_depth'] ?? 3));
$reverse = $order === 'newest';
ob_start();
wp_list_comments([
'callback' => 'aksrt_comment_callback',
'style' => 'ol',
'avatar_size' => $avatar,
'max_depth' => $depth,
'reverse_top_level' => $reverse,
'reverse_children' => $reverse,
], $comments);
return (string) ob_get_clean();
}
endif;
if (!function_exists('aksrt_comments_render_single_html')) :
/**
* Render one comment item HTML.
*/
function aksrt_comments_render_single_html(\WP_Comment $comment, array $settings = []): string {
$html = aksrt_comments_render_items_html([$comment], $settings, 'newest');
return $html;
}
endif;
if (!function_exists('aksrt_comments_render_list_fragment')) :
/**
* Fragment payload for order/author/page AJAX.
*
* @return array{html:string,filter_html:string,pagination_html:string,count_text:string,total:int,max_pages:int,page:int,order:string,author_only:bool,empty:bool}
*/
function aksrt_comments_render_list_fragment(int $post_id, array $args = []): array {
$result = aksrt_comments_query_for_post($post_id, $args);
$settings = $result['settings'];
$order = $result['order'];
$author_only = !empty($result['author_only']);
$page = (int) $result['page'];
$max_pages = (int) $result['max_pages'];
$total = (int) $result['total'];
// Fake globals needed by pagination helpers when not on singular loop.
$GLOBALS['post'] = get_post($post_id);
setup_postdata($GLOBALS['post']);
set_query_var('cpage', $page);
set_query_var('aksrt_comment_order', $order);
set_query_var('aksrt_comment_settings', $settings);
$items_html = aksrt_comments_render_items_html($result['comments'], $settings, $order);
$ctx = [
'order' => $order,
'author_only' => $author_only,
'post_author_id' => (int) get_post_field('post_author', $post_id),
'corderby_enabled' => !empty($settings['corderby']),
'comment_count' => $total,
'empty_text' => (string) ($settings['empty_text'] ?? __('暂无评论,来做第一个评论的人吧~', 'aksrt')),
];
ob_start();
if (function_exists('aksrt_comments_render_filter') && !empty($ctx['corderby_enabled'])) {
aksrt_comments_render_filter($ctx);
}
$filter_html = (string) ob_get_clean();
ob_start();
if ($items_html === '' && function_exists('aksrt_comments_render_empty')) {
aksrt_comments_render_empty($ctx);
} else {
echo '<div id="postcomments"><ol class="commentlist list-unstyled" id="comment-list">';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- rendered comment markup.
echo $items_html;
echo '</ol></div>';
}
$list_html = (string) ob_get_clean();
ob_start();
if (function_exists('aksrt_render_comment_pagination') && $max_pages > 1) {
// Temporarily override page counts via filter.
$pages_filter = static function () use ($max_pages) {
return $max_pages;
};
add_filter('get_comment_pages_count', $pages_filter);
aksrt_render_comment_pagination();
remove_filter('get_comment_pages_count', $pages_filter);
}
$pagination_html = (string) ob_get_clean();
wp_reset_postdata();
return [
'html' => $list_html,
'items_html' => $items_html,
'filter_html' => $filter_html,
'pagination_html' => $pagination_html,
'count_text' => sprintf(__('共 %d 条', 'aksrt'), $total),
'total' => $total,
'max_pages' => $max_pages,
'page' => $page,
'order' => $order,
'author_only' => $author_only,
'empty' => $items_html === '',
];
}
endif;
if (!function_exists('aksrt_comments_render_signarea')) :
/**
* Login-required gate.