xhs_get_comments → xhs_get_sub_comments:针对单条评论加载子评论

getFeedDetail 已返回首屏一级评论(含 1-2 条子评论预览),不再需要独立的
评论加载工具。新增 xhs_get_sub_comments 针对指定一级评论加载完整子评论,
支持 max_count 参数(默认 20)控制加载量,避免超时和上下文溢出。

后端:
- schemas: GetFeedCommentsSchema → GetSubCommentsSchema (feed_id, xsec_token, comment_id, max_count)
- types: 删除 CommentsResult
- feed-detail: 删除 getFeedComments/scrapeComments/CommentSort/parseCommentElement,
  新增 getSubComments(导航→store就绪→定位评论→点击展开→读store)
- selectors: 删除 commentSort* 选择器
- index/routes: 注册新工具和路由,超时改用 feed_detail(60s)

前端:
- types/endpoints: 删除 CommentsResult,新增 getSubComments API
- FeedDetail: 删除独立评论加载逻辑,评论随详情显示,新增 handleLoadSubComments
- CommentTree: "还有 X 条回复" 改为可点击按钮,带加载状态
This commit is contained in:
2026-03-02 17:52:35 +08:00
parent a0f3a3cbac
commit 54a3d9708a
10 changed files with 273 additions and 375 deletions
+17 -47
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import type { FeedDetail as FeedDetailType, Comment } from '@/api/types';
import { getFeedDetail, getFeedComments, toggleLike, toggleFavorite, postComment, replyComment } from '@/api/endpoints';
import { getFeedDetail, getSubComments, toggleLike, toggleFavorite, postComment, replyComment } from '@/api/endpoints';
import { Badge } from '@/components/ui/Badge';
import { Spinner } from '@/components/ui/Spinner';
import { Button } from '@/components/ui/Button';
@@ -24,10 +24,7 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
const [currentImage, setCurrentImage] = useState(0);
const [comments, setComments] = useState<Comment[]>([]);
const [commentsLoading, setCommentsLoading] = useState(false);
const [commentsHasMore, setCommentsHasMore] = useState(false);
const [commentsTotalCount, setCommentsTotalCount] = useState(0);
const [commentsMaxCount, setCommentsMaxCount] = useState(20);
const [subCommentsLoading, setSubCommentsLoading] = useState<string | null>(null);
const [liked, setLiked] = useState(false);
const [favorited, setFavorited] = useState(false);
@@ -49,8 +46,6 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
// Use comments from __INITIAL_STATE__ (first page, ~10-20).
if (res.data.comments.length > 0) {
setComments(res.data.comments);
setCommentsTotalCount(res.data.commentCount);
setCommentsHasMore(res.data.commentCount > res.data.comments.length);
}
} else {
setError(res.error?.message || 'Failed to load detail');
@@ -61,28 +56,26 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
}, [feedId, xsecToken]);
const handleLoadComments = async (maxCount = commentsMaxCount) => {
setCommentsLoading(true);
const handleLoadSubComments = async (commentId: string) => {
setSubCommentsLoading(commentId);
try {
const res = await getFeedComments(feedId, xsecToken, 'default', maxCount);
const res = await getSubComments(feedId, xsecToken, commentId);
if (res.success && res.data) {
setComments(res.data.comments);
setCommentsHasMore(res.data.hasMore);
setCommentsTotalCount(res.data.totalCount);
setComments((prev) =>
prev.map((c) =>
c.id === commentId
? { ...c, subComments: res.data!, subCommentCount: res.data!.length }
: c,
),
);
}
} catch {
toast('error', '加载评论失败');
toast('error', '加载评论失败');
} finally {
setCommentsLoading(false);
setSubCommentsLoading(null);
}
};
const handleLoadMore = () => {
const nextCount = commentsMaxCount * 2;
setCommentsMaxCount(nextCount);
void handleLoadComments(nextCount);
};
const handleToggleLike = async () => {
setActionLoading('like');
try {
@@ -323,25 +316,10 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
</div>
{/* Comments */}
{comments.length === 0 && !commentsLoading && (
<Button
size="sm"
variant="secondary"
onClick={() => void handleLoadComments()}
>
</Button>
)}
{commentsLoading && (
<div className="flex items-center gap-2 py-4 text-dark-muted text-sm">
<Spinner size="sm" />
<span>...</span>
</div>
)}
{comments.length > 0 && (
<div>
<h3 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">
({commentsTotalCount > 0 ? commentsTotalCount : comments.length})
({detail.commentCount > 0 ? detail.commentCount : comments.length})
</h3>
<CommentTree
comments={comments}
@@ -349,17 +327,9 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
setReplyTarget({ commentId, userId, nickname });
setReplyText('');
}}
onLoadSubComments={(commentId) => void handleLoadSubComments(commentId)}
subCommentsLoadingId={subCommentsLoading}
/>
{commentsHasMore && !commentsLoading && (
<Button
size="sm"
variant="secondary"
onClick={handleLoadMore}
className="mt-3 w-full"
>
</Button>
)}
</div>
)}
</div>