diff --git a/assets/js/comment-ajax.js b/assets/js/comment-ajax.js index ab3a1360..7a4a8ac4 100644 --- a/assets/js/comment-ajax.js +++ b/assets/js/comment-ajax.js @@ -28,7 +28,7 @@ if (!window.aksrtData) return; var COMMENT_SELECTOR = '.aksrt-comment, .comment'; - var CONTENT_SELECTOR = '.aksrt-comment__text'; + var CONTENT_SELECTOR = '.aksrt-comment__text, .comment-content'; var REST_BASE = (aksrtData.siteUrl || '').replace(/\/$/, '') + '/wp-json/aksrt/v1'; var REST_NONCE = aksrtData.restNonce || ''; @@ -174,7 +174,17 @@ commentEl.style.maxHeight = '0'; commentEl.style.overflow = 'hidden'; }); - setTimeout(function () { commentEl.remove(); }, 300); + setTimeout(function () { + commentEl.remove(); + var countEl = document.querySelector('.comments-count'); + if (countEl) { + var m = (countEl.textContent || '').match(/(\d+)/); + if (m) { + var n = Math.max(0, parseInt(m[1], 10) - 1); + countEl.textContent = (countEl.textContent || '').replace(m[1], String(n)); + } + } + }, 300); } else { notify((result.data && result.data.message) || result.message || '删除失败', 'error'); } @@ -206,7 +216,7 @@ } function syncPinnedBadge(commentEl, pinned) { - var header = commentEl.querySelector('.aksrt-comment__header'); + var header = commentEl.querySelector('.aksrt-comment__header, .comment-header'); if (!header) return; var badge = header.querySelector('.aksrt-comment__badge--pinned'); if (pinned && !badge) { diff --git a/assets/js/core/comments.js b/assets/js/core/comments.js index aaa3810b..1670db93 100644 --- a/assets/js/core/comments.js +++ b/assets/js/core/comments.js @@ -205,7 +205,8 @@ setLoading(true); var formData = collectFormData(); - var restBase = '/wp-json/aksrt/v1/comment/submit'; + var restRoot = (window.aksrtData && aksrtData.siteUrl) ? String(aksrtData.siteUrl).replace(/\/$/, '') : ''; + var restBase = (restRoot || '') + '/wp-json/aksrt/v1/comment/submit'; // Try REST endpoint for local-insert HTML response var restBody = {}; @@ -407,57 +408,150 @@ form.addEventListener('submit', handleSubmit, true); } + + function getCommentsRestBase() { + var root = (window.aksrtData && aksrtData.siteUrl) ? String(aksrtData.siteUrl).replace(/\/$/, "") : ""; + return root + "/wp-json/aksrt/v1"; + } + + function getCurrentCommentPostId() { + var input = document.querySelector('input[name="comment_post_ID"]'); + if (input && input.value) return parseInt(input.value, 10) || 0; + var comments = document.getElementById("comments"); + if (comments && comments.dataset.postId) return parseInt(comments.dataset.postId, 10) || 0; + return 0; + } + + function getCurrentCommentOrder() { + var active = document.querySelector("#comments .comment-order-box li.active .comment-orderby"); + if (active && active.dataset && active.dataset.order) return active.dataset.order; + var url = new URL(window.location.href); + return url.searchParams.get("aksrt_comment_order") || "newest"; + } + + function getCurrentAuthorOnly() { + var url = new URL(window.location.href); + if (url.searchParams.get("aksrt_author_only") === "1") return true; + var btn = document.querySelector("#comments .comment-author-filter"); + return !!(btn && btn.classList.contains("active") && btn.dataset.authorOnly === "0"); + } + + function applyFragmentPayload(commentsArea, data, urlState) { + if (!commentsArea || !data) return; + + if (data.filter_html) { + var currentFilter = commentsArea.querySelector(".comment-filter"); + var temp = document.createElement("div"); + temp.innerHTML = data.filter_html; + var newFilter = temp.firstElementChild; + if (newFilter) { + if (currentFilter) currentFilter.replaceWith(newFilter); + else { + var listAnchor = commentsArea.querySelector("#postcomments, #comment-list"); + if (listAnchor && listAnchor.parentNode) { + listAnchor.parentNode.insertBefore(newFilter, listAnchor); + } + } + } + } + + var currentListWrap = commentsArea.querySelector("#postcomments"); + var currentList = commentsArea.querySelector("#comment-list"); + var currentEmpty = commentsArea.querySelector(".comments-empty"); + var tempList = document.createElement("div"); + tempList.innerHTML = data.html || ""; + var newListWrap = tempList.querySelector("#postcomments"); + var newEmpty = tempList.querySelector(".comments-empty"); + + if (newListWrap) { + if (currentListWrap) currentListWrap.replaceWith(newListWrap); + else if (currentList) currentList.replaceWith(newListWrap); + else commentsArea.appendChild(newListWrap); + if (currentEmpty) currentEmpty.remove(); + } else if (newEmpty) { + if (currentListWrap) currentListWrap.remove(); + if (currentList) currentList.remove(); + if (currentEmpty) currentEmpty.replaceWith(newEmpty); + else commentsArea.appendChild(newEmpty); + } + + var currentPagination = commentsArea.querySelector(".aksrt-comment-pagination, .comment-navigation, .pagenav.ajax-pag"); + if (data.pagination_html) { + var ptemp = document.createElement("div"); + ptemp.innerHTML = data.pagination_html; + var newPag = ptemp.firstElementChild; + if (newPag) { + if (currentPagination) currentPagination.replaceWith(newPag); + else { + var list = commentsArea.querySelector("#postcomments, #comment-list"); + if (list && list.parentNode) list.parentNode.insertBefore(newPag, list.nextSibling); + } + } + } else if (currentPagination) { + currentPagination.remove(); + } + + var currentCount = commentsArea.querySelector(".comments-count"); + if (currentCount && data.count_text) currentCount.textContent = data.count_text; + + if (urlState && window.history && typeof window.history.replaceState === "function") { + window.history.replaceState(null, "", urlState); + } + if (window.AksrtFrontend) AksrtFrontend.rehydrate(commentsArea, "ajax"); + } + + function fetchCommentFragment(opts) { + opts = opts || {}; + var postId = opts.postId || getCurrentCommentPostId(); + if (!postId) return Promise.reject(new Error("missing post id")); + var order = opts.order || getCurrentCommentOrder() || "newest"; + var authorOnly = !!opts.authorOnly; + var page = opts.page || 1; + var qs = new URLSearchParams(); + qs.set("post_id", String(postId)); + qs.set("order", order); + qs.set("page", String(page)); + if (authorOnly) qs.set("author_only", "1"); + var url = getCommentsRestBase() + "/comment/list?" + qs.toString(); + return fetch(url, { + method: "GET", + credentials: "same-origin", + headers: { + "X-WP-Nonce": (window.aksrtData && aksrtData.restNonce) || "" + } + }).then(function (r) { return r.json(); }); + } + /* ====== 评论列表动态刷新 ====== */ function reloadCommentList() { + var commentsArea = document.getElementById('comments'); var listContainer = document.getElementById('comment-list'); - if (!listContainer) { - // 降级:整页刷新 + if (!commentsArea && !listContainer) { window.location.reload(); return; } + if (listContainer) { + listContainer.innerHTML = '
  • ' + + '
    ' + + '
    ' + + '
    ' + + '
  • '; + } - var postIdInput = document.querySelector('input[name="comment_post_ID"]'); - if (!postIdInput) return; - - // 显示骨架屏加载效果 - listContainer.innerHTML = '
  • ' + - '
    ' + - '
    ' + - '
    ' + - '
  • '; - // 获取当前页面 URL(带时间戳避免缓存) - var currentUrl = window.location.href.split('#')[0]; - var bustUrl = currentUrl + (currentUrl.indexOf('?') === -1 ? '?' : '&') + '_cache_bust=' + Date.now(); - - fetch(bustUrl, { - method: 'GET', - credentials: 'same-origin' - }) - .then(function (response) { return response.text(); }) - .then(function (html) { - // 从新页面提取评论列表 - var parser = new DOMParser(); - var doc = parser.parseFromString(html, 'text/html'); - var newList = doc.getElementById('comment-list'); - - if (newList && newList.innerHTML.trim()) { - listContainer.innerHTML = newList.innerHTML; - - // 更新评论计数 - var countEl = document.querySelector('.comments-count'); - var newCountEl = doc.querySelector('.comments-count'); - if (countEl && newCountEl) { - countEl.textContent = newCountEl.textContent; - } - } else { - listContainer.innerHTML = ''; - } - }) - .catch(function (error) { - console.error('[AKSRT Comment] 刷新评论列表失败:', error); - // 失败时降级为整页刷新 - window.location.reload(); - }); + fetchCommentFragment({ + order: getCurrentCommentOrder(), + authorOnly: getCurrentAuthorOnly(), + page: 1 + }).then(function (json) { + if (json && json.success && json.data) { + applyFragmentPayload(commentsArea || document, json.data, null); + return; + } + throw new Error('fragment failed'); + }).catch(function (error) { + console.error('[AKSRT Comment] 刷新评论列表失败:', error); + window.location.reload(); + }); } /* ====== 评论排序(最新 / 最早) ====== */ @@ -486,63 +580,21 @@ if (order !== "newest" && order !== "oldest" && order !== "hottest") { order = "newest"; } - renderLoadingState(); - + var authorOnly = getCurrentAuthorOnly(); var url = new URL(window.location.href); url.searchParams.set("aksrt_comment_order", order); url.searchParams.delete("cpage"); - var fetchUrl = new URL(url.toString()); - fetchUrl.searchParams.set("_cache_bust", Date.now().toString()); + if (authorOnly) url.searchParams.set("aksrt_author_only", "1"); + else url.searchParams.delete("aksrt_author_only"); - fetch(fetchUrl.toString(), { - method: "GET", - credentials: "same-origin" - }) - .then(function (response) { return response.text(); }) - .then(function (html) { - var parser = new DOMParser(); - var doc = parser.parseFromString(html, "text/html"); - var newComments = doc.getElementById("comments"); - if (!newComments) { - throw new Error("new comments area not found"); + fetchCommentFragment({ order: order, authorOnly: authorOnly, page: 1 }) + .then(function (json) { + if (json && json.success && json.data) { + applyFragmentPayload(commentsArea, json.data, url.toString()); + return; } - - var currentFilter = commentsArea.querySelector(".comment-filter"); - var newFilter = newComments.querySelector(".comment-filter"); - if (currentFilter && newFilter) { - currentFilter.replaceWith(newFilter.cloneNode(true)); - } - - var currentList = commentsArea.querySelector("#comment-list"); - var newList = newComments.querySelector("#comment-list"); - if (currentList && newList) { - currentList.replaceWith(newList.cloneNode(true)); - } - - var currentPagination = getPaginationEl(commentsArea); - var newPagination = getPaginationEl(newComments); - if (currentPagination && newPagination) { - currentPagination.replaceWith(newPagination.cloneNode(true)); - } else if (currentPagination && !newPagination) { - currentPagination.remove(); - } else if (!currentPagination && newPagination) { - var list = commentsArea.querySelector("#comment-list"); - if (list && list.parentNode) { - list.parentNode.insertBefore(newPagination.cloneNode(true), list.nextSibling); - } - } - var currentCount = commentsArea.querySelector(".comments-count"); - var newCount = newComments.querySelector(".comments-count"); - if (currentCount && newCount) { - currentCount.textContent = newCount.textContent; - } - - if (window.history && typeof window.history.replaceState === "function") { - window.history.replaceState(null, "", url.toString()); - } - - if (window.AksrtFrontend) AksrtFrontend.rehydrate(commentsArea, 'ajax'); + throw new Error("order fragment failed"); }) .catch(function (error) { console.error("[AKSRT Comment] 排序刷新失败:", error); @@ -550,99 +602,23 @@ }); } - commentsArea.addEventListener("click", function (e) { - var link = e.target.closest(".comment-orderby"); - if (link && commentsArea.contains(link)) { - e.preventDefault(); - var order = (link.dataset && link.dataset.order) ? link.dataset.order : "newest"; - applyCommentOrder(order); - return; - } - - var authorLink = e.target.closest(".comment-author-filter"); - if (authorLink && commentsArea.contains(authorLink)) { - e.preventDefault(); - var authorOnly = (authorLink.dataset && authorLink.dataset.authorOnly) ? authorLink.dataset.authorOnly : "1"; - applyAuthorFilter(authorOnly); - } - }); - - function applyAuthorFilter(authorOnly) { + function applyAuthorFilter(authorOnlyFlag) { renderLoadingState(); - + var authorOnly = authorOnlyFlag === "1" || authorOnlyFlag === 1 || authorOnlyFlag === true; + var order = getCurrentCommentOrder(); var url = new URL(window.location.href); - if (authorOnly === "1") { - url.searchParams.set("aksrt_author_only", "1"); - } else { - url.searchParams.delete("aksrt_author_only"); - } + if (authorOnly) url.searchParams.set("aksrt_author_only", "1"); + else url.searchParams.delete("aksrt_author_only"); url.searchParams.delete("cpage"); - var fetchUrl = new URL(url.toString()); - fetchUrl.searchParams.set("_cache_bust", Date.now().toString()); - fetch(fetchUrl.toString(), { - method: "GET", - credentials: "same-origin" - }) - .then(function (response) { return response.text(); }) - .then(function (html) { - var parser = new DOMParser(); - var doc = parser.parseFromString(html, "text/html"); - var newComments = doc.getElementById("comments"); - if (!newComments) { - throw new Error("new comments area not found"); - } + if (order) url.searchParams.set("aksrt_comment_order", order); - var currentFilter = commentsArea.querySelector(".comment-filter"); - var newFilter = newComments.querySelector(".comment-filter"); - if (currentFilter && newFilter) { - currentFilter.replaceWith(newFilter.cloneNode(true)); - } else if (!currentFilter && newFilter) { - var listAnchor = commentsArea.querySelector("#postcomments, #comment-list"); - if (listAnchor && listAnchor.parentNode) { - listAnchor.parentNode.insertBefore(newFilter.cloneNode(true), listAnchor); - } + fetchCommentFragment({ order: order, authorOnly: authorOnly, page: 1 }) + .then(function (json) { + if (json && json.success && json.data) { + applyFragmentPayload(commentsArea, json.data, url.toString()); + return; } - - var currentListWrap = commentsArea.querySelector("#postcomments"); - var newListWrap = newComments.querySelector("#postcomments"); - var currentList = commentsArea.querySelector("#comment-list"); - var newList = newComments.querySelector("#comment-list"); - if (currentListWrap && newListWrap) { - currentListWrap.replaceWith(newListWrap.cloneNode(true)); - } else if (currentList && newList) { - currentList.replaceWith(newList.cloneNode(true)); - } - - var currentEmpty = commentsArea.querySelector(".comments-empty"); - var newEmpty = newComments.querySelector(".comments-empty"); - if (currentEmpty && newEmpty) { - currentEmpty.replaceWith(newEmpty.cloneNode(true)); - } else if (currentEmpty && !newEmpty) { - currentEmpty.remove(); - } else if (!currentEmpty && newEmpty) { - var insertAt = commentsArea.querySelector("#postcomments") || commentsArea; - insertAt.appendChild(newEmpty.cloneNode(true)); - } - - var currentPagination = getPaginationEl(commentsArea); - var newPagination = getPaginationEl(newComments); - if (currentPagination && newPagination) { - currentPagination.replaceWith(newPagination.cloneNode(true)); - } else if (currentPagination && !newPagination) { - currentPagination.remove(); - } - - var currentCount = commentsArea.querySelector(".comments-count"); - var newCount = newComments.querySelector(".comments-count"); - if (currentCount && newCount) { - currentCount.textContent = newCount.textContent; - } - - if (window.history && typeof window.history.replaceState === "function") { - window.history.replaceState(null, "", url.toString()); - } - - if (window.AksrtFrontend) AksrtFrontend.rehydrate(commentsArea, 'ajax'); + throw new Error("author fragment failed"); }) .catch(function (error) { console.error("[AKSRT Comment] Author filter failed:", error); diff --git a/assets/js/core/pagination.js b/assets/js/core/pagination.js index 431c090a..8b77df71 100644 --- a/assets/js/core/pagination.js +++ b/assets/js/core/pagination.js @@ -29,8 +29,13 @@ if (target && target.classList && target.classList.contains("content-stream__footer")) { target = target.previousElementSibling; } + if (isComment) { + var commentList = document.getElementById("comment-list") || + (container.parentElement && container.parentElement.querySelector("#comment-list, .commentlist, ol.commentlist")); + if (commentList) target = commentList; + } if (!target && container.parentElement) { - target = container.parentElement.querySelector(".post-grid, .comment-list, .content-stream__list"); + target = container.parentElement.querySelector(".post-grid, .comment-list, .commentlist, .content-stream__list"); } function showSkeleton() { @@ -79,6 +84,8 @@ if (isComment && postId) { body.set("post_id", postId); body.set("comment_order", commentOrder); + var authorOnly = container.dataset.authorOnly || "0"; + if (authorOnly === "1") body.set("author_only", "1"); } fetch(aksrtData.ajaxUrl, { @@ -98,6 +105,9 @@ while (newItems.length > 0) { target.appendChild(newItems[0]); } + if (isComment && window.AksrtFrontend) { + AksrtFrontend.rehydrate(target, "ajax"); + } nextPage++; container.dataset.nextPage = String(nextPage); diff --git a/inc/classes/class-aksrt-admin.php b/inc/classes/class-aksrt-admin.php index 25dcf8cb..2549d3c5 100644 --- a/inc/classes/class-aksrt-admin.php +++ b/inc/classes/class-aksrt-admin.php @@ -268,41 +268,28 @@ class AKSRT_Admin { wp_send_json_error(['message' => __('无效的文章 ID', 'aksrt')]); } - $settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_comment_display_settings() : []; - $per_page = max(1, (int) get_option('comments_per_page', 10)); - $comment_query = new WP_Comment_Query([ - 'post_id' => $post_id, - 'status' => 'approve', - 'number' => $per_page, - 'offset' => ($page - 1) * $per_page, - 'orderby' => 'comment_date_gmt', - 'order' => !empty($settings['order']) && strtoupper((string) $settings['order']) === 'ASC' ? 'ASC' : 'DESC', - ]); - $comments = $comment_query->comments; + $order = sanitize_key((string) ($_POST['comment_order'] ?? $_POST['order'] ?? 'newest')); + $author_only = !empty($_POST['author_only']) || (isset($_POST['aksrt_author_only']) && (int) $_POST['aksrt_author_only'] === 1); - ob_start(); - if (!empty($comments) && function_exists('aksrt_comment_callback')) { - set_query_var('aksrt_comment_settings', $settings); - wp_list_comments([ - 'callback' => 'aksrt_comment_callback', - 'style' => 'ul', - 'avatar_size' => max(24, (int) ($settings['avatar_size'] ?? 48)), - 'max_depth' => max(1, (int) ($settings['nested_depth'] ?? 3)), - ], $comments); + if (!function_exists('aksrt_comments_query_for_post') || !function_exists('aksrt_comments_render_items_html')) { + wp_send_json_error(['message' => __('评论加载不可用', 'aksrt')]); } - $html = ob_get_clean(); - $total = get_comments([ - 'post_id' => $post_id, - 'status' => 'approve', - 'count' => true, + $result = aksrt_comments_query_for_post($post_id, [ + 'page' => $page, + 'order' => $order, + 'author_only' => $author_only, ]); - $max_pages = (int) ceil(((int) $total) / $per_page); + + $html = aksrt_comments_render_items_html($result['comments'], $result['settings'], $result['order']); wp_send_json_success([ 'html' => $html, - 'has_more' => $page < $max_pages, + 'has_more' => $page < (int) $result['max_pages'], 'page' => $page, + 'order' => $result['order'], + 'total' => (int) $result['total'], + 'max_pages'=> (int) $result['max_pages'], ]); } } diff --git a/inc/classes/class-aksrt-comment-actions.php b/inc/classes/class-aksrt-comment-actions.php index 10c43f45..4f874eb5 100644 --- a/inc/classes/class-aksrt-comment-actions.php +++ b/inc/classes/class-aksrt-comment-actions.php @@ -27,6 +27,24 @@ class AKSRT_Comment_Actions { } public function register_routes(): void { + register_rest_route('aksrt/v1', '/comment/list', [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [$this, 'list_comments'], + 'permission_callback' => '__return_true', + 'args' => [ + 'post_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'], + 'page' => ['required' => false, 'type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint'], + 'order' => ['required' => false, 'type' => 'string', 'default' => 'newest', 'sanitize_callback' => 'sanitize_key'], + 'author_only' => ['required' => false, 'type' => 'boolean', 'default' => false], + ], + ]); + + register_rest_route('aksrt/v1', '/comment/submit', [ + 'methods' => 'POST', + 'callback' => [$this, 'submit_comment'], + 'permission_callback' => '__return_true', + ]); + register_rest_route('aksrt/v1', '/comment/edit', [ 'methods' => 'POST', 'callback' => [$this, 'edit_comment'], @@ -127,6 +145,172 @@ class AKSRT_Comment_Actions { return $this->permissions((int) $request->get_param('comment_id'))['can_pin']; } + + /** + * Fragment list for order / author / page without full HTML scrape. + */ + public function list_comments(\WP_REST_Request $request): \WP_REST_Response { + $post_id = (int) $request->get_param('post_id'); + $post = get_post($post_id); + if (!$post) { + return new \WP_REST_Response([ + 'success' => false, + 'code' => 'post_not_found', + 'message' => __('文章不存在。', 'aksrt'), + ], 404); + } + + if (!function_exists('aksrt_comments_render_list_fragment')) { + return new \WP_REST_Response([ + 'success' => false, + 'code' => 'fragment_unavailable', + 'message' => __('评论片段渲染不可用。', 'aksrt'), + ], 500); + } + + $order = sanitize_key((string) ($request->get_param('order') ?: 'newest')); + $author_only = (bool) $request->get_param('author_only'); + $page = max(1, (int) ($request->get_param('page') ?: 1)); + + $fragment = aksrt_comments_render_list_fragment($post_id, [ + 'order' => $order, + 'author_only' => $author_only, + 'page' => $page, + ]); + + return new \WP_REST_Response([ + 'success' => true, + 'data' => $fragment, + ]); + } + + /** + * AJAX submit with HTML item for local insert. + * Runs through native WP comment pipeline (preprocess_comment / moderation). + */ + public function submit_comment(\WP_REST_Request $request): \WP_REST_Response { + $params = $request->get_json_params(); + if (!is_array($params) || empty($params)) { + $params = $request->get_params(); + } + if (!is_array($params)) { + $params = []; + } + + $post_id = absint($params['comment_post_ID'] ?? $params['post_id'] ?? 0); + $content = isset($params['comment']) ? (string) $params['comment'] : ''; + $parent = absint($params['comment_parent'] ?? 0); + + if ($post_id <= 0 || trim(wp_strip_all_tags($content)) === '') { + return new \WP_REST_Response([ + 'success' => false, + 'code' => 'invalid_payload', + 'message' => __('请输入评论内容。', 'aksrt'), + ], 400); + } + + $post = get_post($post_id); + if (!$post || !comments_open($post_id)) { + return new \WP_REST_Response([ + 'success' => false, + 'code' => 'comments_closed', + 'message' => __('评论已关闭。', 'aksrt'), + ], 403); + } + + $nonce = (string) ($params['_aksrt_comment_nonce'] ?? $params['_wpnonce'] ?? ''); + if ($nonce !== '' && !wp_verify_nonce($nonce, 'aksrt_comment_nonce') && !wp_verify_nonce($nonce, 'wp_rest')) { + return new \WP_REST_Response([ + 'success' => false, + 'code' => 'invalid_nonce', + 'message' => __('安全校验失败,请刷新后重试。', 'aksrt'), + ], 403); + } + + $user = wp_get_current_user(); + $commentdata = [ + 'comment_post_ID' => $post_id, + 'comment_content' => $content, + 'comment_parent' => $parent, + 'comment_type' => '', + 'comment_author_IP' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash((string) $_SERVER['REMOTE_ADDR'])) : '', + 'comment_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash((string) $_SERVER['HTTP_USER_AGENT'])) : '', + 'comment_date' => current_time('mysql'), + 'comment_date_gmt' => current_time('mysql', true), + ]; + + if ($user && $user->ID > 0) { + $commentdata['user_id'] = (int) $user->ID; + $commentdata['comment_author'] = $user->display_name ?: $user->user_login; + $commentdata['comment_author_email'] = $user->user_email; + $commentdata['comment_author_url'] = $user->user_url; + } else { + $commentdata['user_id'] = 0; + $commentdata['comment_author'] = sanitize_text_field((string) ($params['author'] ?? '')); + $commentdata['comment_author_email'] = sanitize_email((string) ($params['email'] ?? '')); + $commentdata['comment_author_url'] = esc_url_raw((string) ($params['url'] ?? '')); + if ($commentdata['comment_author'] === '' || $commentdata['comment_author_email'] === '') { + return new \WP_REST_Response([ + 'success' => false, + 'code' => 'guest_fields_required', + 'message' => __('请填写昵称和邮箱。', 'aksrt'), + ], 400); + } + } + + foreach (['author', 'email', 'url', 'comment', 'comment_post_ID', 'comment_parent', '_aksrt_comment_nonce'] as $k) { + if (isset($params[$k])) { + $_POST[$k] = $params[$k]; + } + } + foreach ($params as $k => $v) { + if (is_string($k) && (str_contains($k, 'captcha') || str_starts_with($k, 'g-recaptcha') || str_starts_with($k, 'cf-turnstile') || str_starts_with($k, 'h-captcha'))) { + $_POST[$k] = $v; + } + } + + $comment_id = wp_new_comment(wp_slash($commentdata), true); + if (is_wp_error($comment_id)) { + return new \WP_REST_Response([ + 'success' => false, + 'code' => $comment_id->get_error_code() ?: 'submit_failed', + 'message' => $comment_id->get_error_message() ?: __('评论提交失败。', 'aksrt'), + ], 400); + } + + $comment = get_comment((int) $comment_id); + if (!$comment) { + return new \WP_REST_Response([ + 'success' => false, + 'code' => 'submit_failed', + 'message' => __('评论提交失败。', 'aksrt'), + ], 500); + } + + $settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_comment_display_settings() : []; + $html = function_exists('aksrt_comments_render_single_html') + ? aksrt_comments_render_single_html($comment, $settings) + : ''; + + $needs_moderation = '0' === (string) $comment->comment_approved; + $wait_time = 0; + + return new \WP_REST_Response([ + 'success' => true, + 'code' => 'submitted', + 'message' => $needs_moderation ? __('评论已提交,等待管理员审核', 'aksrt') : __('评论发布成功', 'aksrt'), + 'html' => $html, + 'is_reply'=> $parent > 0, + 'parent_id' => $parent, + 'needs_moderation' => $needs_moderation, + 'wait_time' => $wait_time, + 'data' => [ + 'comment_id' => (int) $comment_id, + 'approved' => (string) $comment->comment_approved, + ], + ]); + } + /** * Edit comment content */ @@ -168,12 +352,16 @@ class AKSRT_Comment_Actions { ], 500); } + $rendered = function_exists('aksrt_render_comment_content') + ? aksrt_render_comment_content($clean_content) + : wpautop($clean_content); + return new \WP_REST_Response([ 'success' => true, 'code' => 'updated', 'message' => __('评论已更新。', 'aksrt'), 'data' => [ - 'content' => $clean_content, + 'content' => $rendered, ], ]); } diff --git a/inc/helpers/aksrt-comments.php b/inc/helpers/aksrt-comments.php index ccbe1dd0..de3de7eb 100644 --- a/inc/helpers/aksrt-comments.php +++ b/inc/helpers/aksrt-comments.php @@ -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,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 $comments + * @param array $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 '
      '; + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- rendered comment markup. + echo $items_html; + echo '
    '; + } + $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. diff --git a/inc/helpers/aksrt-frontend-tags.php b/inc/helpers/aksrt-frontend-tags.php index 8e3f03eb..d3e780a3 100644 --- a/inc/helpers/aksrt-frontend-tags.php +++ b/inc/helpers/aksrt-frontend-tags.php @@ -1063,7 +1063,7 @@ if (!function_exists('aksrt_comment_callback')) :
  • id="comment-" data-comment-id="" data-comment-status="comment_approved); ?>">
    • -
      +
      'avatar-img comt-avatar']); ?> @@ -1074,16 +1074,16 @@ if (!function_exists('aksrt_comment_callback')) :
      - +
      -
      +

      -