From 56bbb63afd93bb24c452ca411b7acb05af73b30d Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 4 Oct 2026 19:11:27 +0800 Subject: [PATCH] fix(user-home): align profile comments with reference theme --- assets/css/routes/profile/lists.css | 163 ++++++++---------- assets/js/author.js | 48 +++++- inc/helpers/aksrt-user-dashboard.php | 8 +- .../profile-comments.php | 2 + .../parts/content-profile-comment-item.php | 63 +++++-- templates/parts/user-home/panel-comments.php | 49 ++++-- .../post-visual-contract.test.cjs | 2 +- .../profile-comment-contract.test.cjs | 57 ++++++ 8 files changed, 261 insertions(+), 131 deletions(-) create mode 100644 tests/widget-system/profile-comment-contract.test.cjs diff --git a/assets/css/routes/profile/lists.css b/assets/css/routes/profile/lists.css index 80b2c853..aeb87b60 100644 --- a/assets/css/routes/profile/lists.css +++ b/assets/css/routes/profile/lists.css @@ -240,91 +240,82 @@ padding: 0; display: flex; flex-direction: column; - gap: 14px; + gap: 0; } .profile-comment-item { - position: relative; - padding: 16px 18px; - border: 1px solid var(--color-line); - border-radius: var(--radius-lg); - background: var(--surface-1); - box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04); - transition: background-color var(--dur-fast) var(--ease), - border-color var(--dur-fast) var(--ease), - box-shadow var(--dur-fast) var(--ease); -} -.profile-comment-item::before { - content: none; -} -.profile-comment-item:hover { - background: var(--surface-1); - border-color: var(--color-line-strong, var(--color-line)); - box-shadow: 0 2px 6px rgba(15, 23, 42, 0.05); -} - -.profile-comment-item__quote-link { display: flex; align-items: flex-start; - gap: 6px; - text-decoration: none; - color: var(--color-ink-2); - margin-bottom: 8px; + margin: 0; + padding: 20px 0; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; } -.profile-comment-item__quote-link:hover .profile-comment-item__quote-text { - color: var(--color-ink-2); -} -.profile-comment-item__quote-mark { - flex-shrink: 0; - font-family: var(--font-display); - font-size: 18px; - line-height: 1; +.profile-comment-item__time { + flex: 0 0 20%; + margin-right: 20px; + padding-top: 6px; color: var(--color-ink-4); - opacity: 0.45; - margin-top: 2px; + text-align: right; } -.profile-comment-item__quote-text { - flex: 1; - font-family: var(--font-body); - font-size: var(--text-sm, 0.875rem); - line-height: 1.65; - color: var(--color-ink-2); - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; +.profile-comment-item__body { + flex: 0 1 calc(80% - 25px); + width: calc(80% - 25px); + min-width: 0; overflow: hidden; - transition: color var(--dur-fast) var(--ease); } - -.profile-comment-item__meta { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 6px; - margin-left: 0; - font-size: 12px; - color: var(--color-ink-4); +.profile-comment-item__content-wrap { + margin-bottom: 10px; } -.profile-comment-item__meta-item { - display: inline-flex; - align-items: center; - gap: 4px; +.profile-comment-item__content { + display: -webkit-box; + max-height: 7em; + overflow: hidden; + color: var(--color-ink-2); + line-height: 1.4em; + text-decoration: none; + white-space: normal; + -webkit-box-orient: vertical; + -webkit-line-clamp: 5; } -.profile-comment-item__meta-item i { - font-size: 11px; - opacity: 0.75; +.profile-comment-item__content:hover { + color: var(--color-ink-2); } -.profile-comment-item__meta-sep { - opacity: 0.5; +.profile-comment-item__pending { + margin-right: 6px; +} +.profile-comment-item__source { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.profile-comment-item__parent { + margin-right: 10px; } .profile-comment-item__post-link { - color: var(--color-ink-2); - font-weight: 500; text-decoration: none; - transition: color var(--dur-fast) var(--ease); } -.profile-comment-item__post-link:hover { - color: var(--color-ink); +.profile-comment-more { + margin: 20px 0; + text-align: center; +} +.profile-comment-more button { + padding: 5px 15px; + border: 0; + border-radius: 30px; + color: var(--color-ink-3); + background: transparent; + cursor: pointer; + opacity: .65; +} +.profile-comment-more button:hover { + background: var(--color-line); + opacity: 1; +} +.profile-comment-more button:disabled { + cursor: wait; } /* ==================== 分页 ==================== */ @@ -656,19 +647,15 @@ /* legacy-style user tabs: one panel outside, flat rows inside. */ .dashboard--public .profile-list, -.dashboard--public .profile-comment-list, .uc-content #panel-posts .profile-list, -.uc-content #panel-favorites .profile-list, -.uc-content #panel-comments .profile-comment-list { +.uc-content #panel-favorites .profile-list { gap: 0; } .dashboard--public .profile-list-item, -.dashboard--public .profile-comment-item, .uc-content #panel-posts .profile-list-item, .uc-content #panel-favorites .profile-list-item, -.uc-content #panel-favorites .profile-list[data-list="favorites"] .profile-list-item, -.uc-content #panel-comments .profile-comment-item { +.uc-content #panel-favorites .profile-list[data-list="favorites"] .profile-list-item { padding: 16px 0; background: transparent; border: 0; @@ -678,29 +665,23 @@ } .dashboard--public .profile-list-item:hover, -.dashboard--public .profile-comment-item:hover, .uc-content #panel-posts .profile-list-item:hover, .uc-content #panel-favorites .profile-list-item:hover, -.uc-content #panel-favorites .profile-list[data-list="favorites"] .profile-list-item:hover, -.uc-content #panel-comments .profile-comment-item:hover { +.uc-content #panel-favorites .profile-list[data-list="favorites"] .profile-list-item:hover { background: transparent; border-color: var(--color-line-soft, rgba(0,0,0,0.06)); box-shadow: none; } .dashboard--public .profile-list-item:first-child, -.dashboard--public .profile-comment-item:first-child, .uc-content #panel-posts .profile-list-item:first-child, -.uc-content #panel-favorites .profile-list-item:first-child, -.uc-content #panel-comments .profile-comment-item:first-child { +.uc-content #panel-favorites .profile-list-item:first-child { padding-top: 0; } .dashboard--public .profile-list-item:last-child, -.dashboard--public .profile-comment-item:last-child, .uc-content #panel-posts .profile-list-item:last-child, -.uc-content #panel-favorites .profile-list-item:last-child, -.uc-content #panel-comments .profile-comment-item:last-child { +.uc-content #panel-favorites .profile-list-item:last-child { padding-bottom: 0; border-bottom: 0; } @@ -779,26 +760,24 @@ @media (max-width: 640px) { .dashboard--public .profile-list-item, - .dashboard--public .profile-comment-item, .uc-content #panel-posts .profile-list-item, - .uc-content #panel-favorites .profile-list-item, - .uc-content #panel-comments .profile-comment-item { + .uc-content #panel-favorites .profile-list-item { padding: 14px 0; } .dashboard--public .profile-list-item:first-child, - .dashboard--public .profile-comment-item:first-child, .uc-content #panel-posts .profile-list-item:first-child, - .uc-content #panel-favorites .profile-list-item:first-child, - .uc-content #panel-comments .profile-comment-item:first-child { + .uc-content #panel-favorites .profile-list-item:first-child { padding-top: 0; } .dashboard--public .profile-list-item:last-child, - .dashboard--public .profile-comment-item:last-child, .uc-content #panel-posts .profile-list-item:last-child, - .uc-content #panel-favorites .profile-list-item:last-child, - .uc-content #panel-comments .profile-comment-item:last-child { + .uc-content #panel-favorites .profile-list-item:last-child { padding-bottom: 0; } + + .profile-comment-item { + padding: 10px 0; + } } diff --git a/assets/js/author.js b/assets/js/author.js index 450031bd..226a65e0 100644 --- a/assets/js/author.js +++ b/assets/js/author.js @@ -60,7 +60,25 @@ }); } - function loadPanelBody(panel, name, page, sub) { + function appendCommentPage(panel, html) { + var fragment = document.createElement('div'); + fragment.innerHTML = html; + + var currentList = panel.querySelector('.profile-comment-list'); + var incomingList = fragment.querySelector('.profile-comment-list'); + if (currentList && incomingList) { + Array.prototype.slice.call(incomingList.children).forEach(function (item) { + currentList.appendChild(item); + }); + } + + var currentMore = panel.querySelector('.profile-comment-more'); + var incomingMore = fragment.querySelector('.profile-comment-more'); + if (currentMore) currentMore.remove(); + if (incomingMore && currentList) currentList.insertAdjacentElement('afterend', incomingMore); + } + + function loadPanelBody(panel, name, page, sub, appendComments, trigger) { var cfg = window.aksrtData || {}; var restBase = cfg.apiUrl || cfg.restUrl || ''; if (!userId || !restBase) return Promise.reject(new Error('rest-unavailable')); @@ -74,6 +92,11 @@ panel.dataset.uhPage = String(page); panel.dataset.uhSub = followSub; } + if (appendComments && trigger) { + trigger.disabled = true; + trigger.setAttribute('aria-busy', 'true'); + trigger.textContent = '加载中…'; + } panel.classList.add('is-loading'); var url = restBase.replace(/\/$/, '') + '/user-home/panel/' + userId + '/' + encodeURIComponent(name) @@ -89,10 +112,21 @@ if (!response.ok || !payload || !payload.success || !payload.data || typeof payload.data.html !== 'string') { throw new Error('panel-load-failed'); } - panel.innerHTML = payload.data.html; + if (appendComments && name === 'comments') { + appendCommentPage(panel, payload.data.html); + } else { + panel.innerHTML = payload.data.html; + } if (window.jQuery) jQuery(document).trigger('post_ajax.ed', [panel]); if (typeof window.auto_fun === 'function') window.auto_fun(); }); + }).catch(function (error) { + if (appendComments && trigger && trigger.isConnected) { + trigger.disabled = false; + trigger.removeAttribute('aria-busy'); + trigger.textContent = '加载更多'; + } + throw error; }).finally(function () { panel._aksrtUhRequest = null; panel.classList.remove('is-loading'); @@ -127,6 +161,16 @@ return; } + var commentsNext = target.closest('[data-uh-comments-next]'); + if (commentsNext) { + event.preventDefault(); + var commentsPanel = commentsNext.closest('.dashboard-panel'); + if (!commentsPanel) return; + var commentsPage = Math.max(2, parseInt(commentsNext.getAttribute('data-uh-comments-next') || '2', 10) || 2); + loadPanelBody(commentsPanel, 'comments', commentsPage, 'followers', true, commentsNext).catch(notifyLoadError); + return; + } + var pageBtn = target.closest('[data-uh-page]'); if (!pageBtn) return; event.preventDefault(); diff --git a/inc/helpers/aksrt-user-dashboard.php b/inc/helpers/aksrt-user-dashboard.php index 01cf16a3..729e08fe 100644 --- a/inc/helpers/aksrt-user-dashboard.php +++ b/inc/helpers/aksrt-user-dashboard.php @@ -41,9 +41,11 @@ function aksrt_get_user_dashboard_view(int $user_id, array $args = []): array { $post_count = (int) count_user_posts($user_id, 'post', true); $comment_count = (int) get_comments([ - 'user_id' => $user_id, - 'count' => true, - 'status' => 'approve', + 'user_id' => $user_id, + 'count' => true, + 'status' => 'approve', + 'type' => 'comment', + 'post_type' => ['post'], ]); $favorite_post_ids = function_exists('aksrt_get_user_interaction_posts') diff --git a/inc/helpers/user-center-renderers/profile-comments.php b/inc/helpers/user-center-renderers/profile-comments.php index 3f9b124f..cf4bbd58 100644 --- a/inc/helpers/user-center-renderers/profile-comments.php +++ b/inc/helpers/user-center-renderers/profile-comments.php @@ -22,6 +22,8 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_ 'number' => $per_page, 'offset' => ($comments_page - 1) * $per_page, 'status' => 'approve', + 'type' => 'comment', + 'post_type' => ['post'], 'update_comment_meta_cache' => false, ]); if ($cs) : diff --git a/templates/parts/content-profile-comment-item.php b/templates/parts/content-profile-comment-item.php index 2c9cbdc4..73a5f287 100644 --- a/templates/parts/content-profile-comment-item.php +++ b/templates/parts/content-profile-comment-item.php @@ -1,6 +1,6 @@ comment_content), 30, '…'); +$content = get_comment_text($comment); +$content = preg_replace_callback( + '/\[link=([^\]\s]+)(?:\s+target=_blank)?\](.*?)\[\/link\]/is', + static function (array $matches): string { + $url = esc_url(trim($matches[1])); + $label = wp_kses_post($matches[2]); + if ($url === '') { + return $label; + } + + $target = stripos($matches[0], 'target=_blank') !== false ? ' target="_blank" rel="noopener noreferrer"' : ''; + return '' . $label . ''; + }, + (string) $content +); +$content = preg_replace('/]*>/i', '[图片]', (string) $content); +$content = preg_replace('/\[img=[^\]]*\]/i', '[图片]', (string) $content); +$content = preg_replace('/\[code\][\s\S]*?\[\/code\]/i', '[代码]', (string) $content); +$content = wp_kses_post((string) $content); + $post_title = get_the_title($post); $comment_link = get_comment_link($comment); $post_link = get_permalink($post); $date_iso = get_comment_date('c', $comment); -$date_label = get_comment_date('Y-m-d', $comment); -$time_diff = human_time_diff(strtotime($comment->comment_date), current_time('timestamp')); +$date_label = get_comment_date('Y-m-d H:i:s', $comment); +$time_diff = human_time_diff((int) get_comment_time('U', false, false, $comment), current_time('timestamp')); +$parent = $comment->comment_parent ? get_comment((int) $comment->comment_parent) : null; ?> - + + diff --git a/templates/parts/user-home/panel-comments.php b/templates/parts/user-home/panel-comments.php index 20b84ef2..71b1ffde 100644 --- a/templates/parts/user-home/panel-comments.php +++ b/templates/parts/user-home/panel-comments.php @@ -3,8 +3,8 @@ * Public user home — comments tab panel body. * * Rendered inside the #panel-comments wrapper on the public user home and via - * the aksrt/v1 user-home panel REST fragment. Pagination is button-driven - * (aksrt_render_user_home_pagination) and never touches the URL. + * the aksrt/v1 user-home panel REST fragment. Later pages append through the + * existing REST route and never touch the URL. * * @package AKSRT */ @@ -14,26 +14,47 @@ defined('ABSPATH') || exit; $args = is_array($args ?? null) ? $args : []; extract($args, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract -$user_comments = get_comments([ - 'user_id' => $user_id, - 'status' => 'approve', - 'number' => $per_page, - 'offset' => ($comments_page - 1) * $per_page, +$is_own_profile = !empty($is_own_profile); +$comments_page = max(1, (int) $comments_page); +$per_page = max(1, (int) $per_page); +$comment_offset = ($comments_page - 1) * $per_page; +$comment_status = $is_own_profile ? 'all' : 'approve'; +$comment_args = [ + 'user_id' => (int) $user_id, + 'status' => $comment_status, 'type' => 'comment', + 'post_type' => ['post'], + 'orderby' => 'comment_date_gmt', + 'order' => 'DESC', 'update_comment_meta_cache' => false, -]); +]; +$user_comments = get_comments(array_merge($comment_args, [ + 'number' => $per_page, + 'offset' => $comment_offset, +])); +$comments_total = (int) get_comments(array_merge($comment_args, ['count' => true])); +$has_more = $comments_total > ($comment_offset + count($user_comments)); if ($user_comments) : ?>
$c]); + get_template_part('templates/parts/content', 'profile-comment-item', [ + 'comment' => $c, + ]); ?>
- +
+
+ +
+
+ + + + diff --git a/tests/widget-system/post-visual-contract.test.cjs b/tests/widget-system/post-visual-contract.test.cjs index f10e6d06..92ba9e7b 100644 --- a/tests/widget-system/post-visual-contract.test.cjs +++ b/tests/widget-system/post-visual-contract.test.cjs @@ -578,7 +578,7 @@ test('public user home stays param-free and swaps panels via REST fragments', () assert.match(follow, /aksrt_render_user_home_pagination\(\$total_pages, \$follow_page\)/); assert.doesNotMatch(follow, /add_query_arg|\$_GET|paginate_links/); - assert.match(comments, /aksrt_render_user_home_pagination\(/); + assert.match(comments, /data-uh-comments-next=/); assert.match(favorites, /aksrt_render_user_home_pagination\(/); assert.doesNotMatch(comments + favorites, /add_query_arg|\$_GET/); diff --git a/tests/widget-system/profile-comment-contract.test.cjs b/tests/widget-system/profile-comment-contract.test.cjs new file mode 100644 index 00000000..dbc4a9f8 --- /dev/null +++ b/tests/widget-system/profile-comment-contract.test.cjs @@ -0,0 +1,57 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); + +const root = path.resolve(__dirname, '..', '..'); +const read = (file) => fs.readFileSync(path.join(root, file), 'utf8'); + +test('profile comments follow the author-page row and moderation contract', () => { + const panel = read('templates/parts/user-home/panel-comments.php'); + const item = read('templates/parts/content-profile-comment-item.php'); + const dashboard = read('inc/helpers/aksrt-user-dashboard.php'); + const userCenter = read('inc/helpers/user-center-renderers/profile-comments.php'); + const styles = read('assets/css/routes/profile/lists.css'); + + assert.match(panel, /\$comment_status\s*=\s*\$is_own_profile\s*\?\s*'all'\s*:\s*'approve'/); + assert.match(panel, /'type'\s*=>\s*'comment'/); + assert.match(panel, /'post_type'\s*=>\s*\['post'\]/); + assert.match(panel, /'count'\s*=>\s*true/); + assert.match(panel, /data-uh-comments-next=/); + assert.match(panel, /<\?php else : \?>\s*<\?php aksrt_render_profile_empty\(/); + assert.doesNotMatch(panel, /aksrt_render_user_home_pagination/); + assert.match(dashboard, /'type'\s*=>\s*'comment'[\s\S]*?'post_type'\s*=>\s*\['post'\]/); + assert.match(userCenter, /'type'\s*=>\s*'comment'[\s\S]*?'post_type'\s*=>\s*\['post'\]/); + + assert.match(item, /profile-comment-item__time/); + assert.match(item, /profile-comment-item__content text-ellipsis-5/); + assert.match(item, /comment_approved\s*===\s*'0'[\s\S]*?待审核/); + assert.match(item, /get_comment\(\(int\) \$comment->comment_parent\)/); + assert.match(item, /preg_replace_callback\([\s\S]*?target=_blank[\s\S]*?rel="noopener noreferrer"/); + assert.match(item, /评论于:/); + assert.match(item, /esc_url\(\$comment_link\)/); + assert.match(item, /esc_url\(\$post_link\)/); + + assert.match(styles, /\.profile-comment-item__time\s*\{[^}]*flex:\s*0 0 20%;[^}]*margin-right:\s*20px;[^}]*text-align:\s*right;/s); + assert.match(styles, /\.profile-comment-item__body\s*\{[^}]*width:\s*calc\(80% - 25px\)/s); + assert.match(styles, /\.profile-comment-item__content\s*\{[^}]*-webkit-line-clamp:\s*5;/s); + assert.match(styles, /@media \(max-width: 640px\)[\s\S]*?\.profile-comment-item\s*\{\s*padding:\s*10px 0;/); + assert.doesNotMatch(styles, /profile-comment-item__(?:quote|meta)/); +}); + +test('public comment load-more appends REST fragments and retains retry on errors', () => { + const runtime = read('assets/js/author.js'); + const rest = read('src/User/Home.php'); + + assert.match(runtime, /function appendCommentPage\(panel, html\)/); + assert.match(runtime, /currentList\.appendChild\(item\)/); + assert.match(runtime, /incomingMore && currentList/); + assert.match(runtime, /data-uh-comments-next/); + assert.match(runtime, /trigger\.disabled = true/); + assert.match(runtime, /trigger\.disabled = false/); + assert.match(runtime, /trigger\.textContent = '加载更多'/); + assert.match(runtime, /panel\.innerHTML = payload\.data\.html/); + assert.ok(rest.includes("'/user-home/panel/(?P\\d+)/(?P[a-z0-9_-]+)'")); +});