fix: 修复评论区用户态异常

This commit is contained in:
2025-09-15 16:56:46 +08:00
parent 6bae6947f5
commit 16d8eae61f
5 changed files with 168 additions and 33 deletions

View File

@ -17,7 +17,7 @@ import { formatDateTime } from "@/utils/common/datetime";
export function CommentItem( export function CommentItem(
{ {
user, loginUser,
comment, comment,
parentComment, parentComment,
onCommentDelete, onCommentDelete,
@ -25,7 +25,7 @@ export function CommentItem(
setActiveInputId, setActiveInputId,
onReplySubmitted // 评论区计数更新用 onReplySubmitted // 评论区计数更新用
}: { }: {
user: User | null, loginUser: User | null,
comment: Comment, comment: Comment,
parentComment: Comment | null, parentComment: Comment | null,
onCommentDelete: ({ commentId }: { commentId: number }) => void, onCommentDelete: ({ commentId }: { commentId: number }) => void,
@ -55,17 +55,13 @@ export function CommentItem(
return; return;
} }
setCanClickLike(false); setCanClickLike(false);
if (!user) { if (!loginUser) {
toast.error(t("login_required"), { toast.error(t("login_required"), {
action: <div className="flex justify-end"> action: {
<button label: commonT("login"),
onClick={clickToLogin} onClick: clickToLogin,
className="ml-0 text-left bg-red-400 text-white dark:text-black px-3 py-1 rounded font-semibold hover:bg-red-600 transition-colors" },
> })
{commonT("login")}
</button>
</div>,
});
return; return;
} }
// 提前转换状态,让用户觉得响应很快 // 提前转换状态,让用户觉得响应很快
@ -202,6 +198,11 @@ export function CommentItem(
{commentState.os && <span title={commentState.os}>{commentState.os}</span>} {commentState.os && <span title={commentState.os}>{commentState.os}</span>}
</div> </div>
<div className="flex items-center gap-4 w-full md:w-auto"> <div className="flex items-center gap-4 w-full md:w-auto">
{replyCount > 0 && (
<button onClick={toggleReplies} className="fade-in-up">
{!showReplies ? t("expand_replies", { count: replyCount }) : t("collapse_replies")}
</button>
)}
{/* 回复按钮 */} {/* 回复按钮 */}
<button <button
title={t("reply")} title={t("reply")}
@ -230,7 +231,7 @@ export function CommentItem(
</button> </button>
{/* 编辑和删除按钮 仅自己的评论可见 */} {/* 编辑和删除按钮 仅自己的评论可见 */}
{user?.id === commentState.user.id && ( {loginUser?.id === commentState.user.id && (
<> <>
<button <button
title={t("edit")} title={t("edit")}
@ -265,22 +266,18 @@ export function CommentItem(
</> </>
)} )}
{replyCount > 0 && (
<button onClick={toggleReplies} className="fade-in-up">
{!showReplies ? t("expand_replies", { count: replyCount }) : t("collapse_replies")}
</button>
)}
</div> </div>
</div> </div>
{/* 这俩输入框一次只能显示一个 */} {/* 这俩输入框一次只能显示一个 */}
{activeInput && activeInput.type === 'reply' && activeInput.id === commentState.id && <CommentInput {activeInput && activeInput.type === 'reply' && activeInput.id === commentState.id && <CommentInput
user={user} user={loginUser}
onCommentSubmitted={onReply} onCommentSubmitted={onReply}
initIsPrivate={commentState.isPrivate} initIsPrivate={commentState.isPrivate}
placeholder={`${t("reply")} ${commentState.user.nickname || commentState.user.username} :`} placeholder={`${t("reply")} ${commentState.user.nickname || commentState.user.username} :`}
/>} />}
{activeInput && activeInput.type === 'edit' && activeInput.id === commentState.id && <CommentInput {activeInput && activeInput.type === 'edit' && activeInput.id === commentState.id && <CommentInput
user={user} user={loginUser}
initContent={commentState.content} initContent={commentState.content}
initIsPrivate={commentState.isPrivate} initIsPrivate={commentState.isPrivate}
onCommentSubmitted={onCommentEdit} onCommentSubmitted={onCommentEdit}
@ -295,7 +292,7 @@ export function CommentItem(
{replies.map((reply) => ( {replies.map((reply) => (
<CommentItem <CommentItem
key={reply.id} key={reply.id}
user={reply.user} loginUser={loginUser}
comment={reply} comment={reply}
parentComment={commentState} parentComment={commentState}
onCommentDelete={onReplyDelete} onCommentDelete={onReplyDelete}

View File

@ -29,7 +29,7 @@ export function CommentSection(
} }
) { ) {
const t = useTranslations('Comment') const t = useTranslations('Comment')
const [user, setUser] = useState<User | null>(null); const [loginUser, setLoginUser] = useState<User | null>(null);
const [comments, setComments] = useState<Comment[]>([]); const [comments, setComments] = useState<Comment[]>([]);
const [activeInput, setActiveInput] = useState<{ id: number; type: 'reply' | 'edit' } | null>(null); const [activeInput, setActiveInput] = useState<{ id: number; type: 'reply' | 'edit' } | null>(null);
const [page, setPage] = useState(1); // 当前页码 const [page, setPage] = useState(1); // 当前页码
@ -39,9 +39,10 @@ export function CommentSection(
// 获取登录用户信息 // 获取登录用户信息
useEffect(() => { useEffect(() => {
getLoginUser().then(res => { getLoginUser().then(res => {
setUser(res.data); setLoginUser(res.data);
console.log("login user:", res.data);
}).catch(() => { }).catch(() => {
setUser(null); setLoginUser(null);
}); });
}, []); }, []);
// 加载0/顶层评论 // 加载0/顶层评论
@ -117,7 +118,7 @@ export function CommentSection(
<Separator className="my-16" /> <Separator className="my-16" />
<div className="font-bold text-2xl">{t("comment")} ({totalCommentCount})</div> <div className="font-bold text-2xl">{t("comment")} ({totalCommentCount})</div>
<CommentInput <CommentInput
user={user} user={loginUser}
onCommentSubmitted={onCommentSubmitted} onCommentSubmitted={onCommentSubmitted}
/> />
<div className="mt-4"> <div className="mt-4">
@ -126,7 +127,7 @@ export function CommentSection(
<div key={comment.id} className="" style={{ animationDelay: `${idx * 60}ms` }}> <div key={comment.id} className="" style={{ animationDelay: `${idx * 60}ms` }}>
<Separator className="my-2" /> <Separator className="my-2" />
<CommentItem <CommentItem
user={user} loginUser={loginUser}
comment={comment} comment={comment}
parentComment={null} parentComment={null}
onCommentDelete={onCommentDelete} onCommentDelete={onCommentDelete}

View File

@ -20,6 +20,7 @@ import { getLoginUser, userLogout } from "@/api/user";
import Link from "next/link"; import Link from "next/link";
import { toast } from "sonner"; import { toast } from "sonner";
import { useToLogin } from "@/hooks/use-route"; import { useToLogin } from "@/hooks/use-route";
import { CircleUser } from "lucide-react";
export function AvatarWithDropdownMenu() { export function AvatarWithDropdownMenu() {
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<User | null>(null);
@ -43,19 +44,16 @@ export function AvatarWithDropdownMenu() {
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button className="rounded-full overflow-hidden"> <button className="rounded-full overflow-hidden">
<GravatarAvatar className="w-9 h-9" email={user?.email || ""} url={user?.avatarUrl || ""} /> {user ? <GravatarAvatar className="w-8 h-8" email={user?.email || ""} url={user?.avatarUrl || ""} /> : <CircleUser className="w-9 h-9" />}
</button> </button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start"> <DropdownMenuContent className="w-56" align="start">
<DropdownMenuLabel>My Account</DropdownMenuLabel> <DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem asChild> {user && <DropdownMenuItem asChild>
<Link href={`/u/${user?.username}`}>Profile</Link> <Link href={`/u/${user?.username}`}>Profile</Link>
</DropdownMenuItem> </DropdownMenuItem>}
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link href="/billing">Billing</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link href="/console">Console</Link> <Link href="/console">Console</Link>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>

View File

@ -0,0 +1,104 @@
import React, { useEffect, useRef, useState } from "react";
import styles from "./overlay-scrollbar.module.css";
export default function OverlayScrollbar({
children,
className,
style,
}: {
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
}) {
const scrollRef = useRef<HTMLDivElement | null>(null);
const thumbRef = useRef<HTMLDivElement | null>(null);
const [thumbHeight, setThumbHeight] = useState(0);
const [thumbTop, setThumbTop] = useState(0);
const dragging = useRef(false);
const startY = useRef(0);
const startScrollTop = useRef(0);
useEffect(() => {
const el = scrollRef.current;
if (!el) return;
const update = () => {
const visible = el.clientHeight;
const total = el.scrollHeight;
const ratio = visible / Math.max(total, 1);
const h = Math.max(ratio * visible, 24);
const top = total > visible ? (el.scrollTop / (total - visible)) * (visible - h) : 0;
setThumbHeight(h);
setThumbTop(isFinite(top) ? top : 0);
if (thumbRef.current) {
const percent = total > visible ? Math.round((el.scrollTop / (total - visible)) * 100) : 0;
thumbRef.current.setAttribute("aria-valuenow", String(percent));
}
};
update();
el.addEventListener("scroll", update, { passive: true });
window.addEventListener("resize", update);
const obs = new MutationObserver(update);
obs.observe(el, { childList: true, subtree: true });
return () => {
el.removeEventListener("scroll", update);
window.removeEventListener("resize", update);
obs.disconnect();
};
}, []);
useEffect(() => {
const onMove = (e: MouseEvent) => {
if (!dragging.current || !scrollRef.current) return;
const el = scrollRef.current;
const visible = el.clientHeight;
const total = el.scrollHeight;
const h = thumbHeight;
const delta = e.clientY - startY.current;
const proportion = delta / Math.max(visible - h, 1);
el.scrollTop = Math.min(Math.max(startScrollTop.current + proportion * (total - visible), 0), total - visible);
};
const onUp = () => {
dragging.current = false;
document.body.style.userSelect = "";
};
window.addEventListener("mousemove", onMove);
window.addEventListener("mouseup", onUp);
return () => {
window.removeEventListener("mousemove", onMove);
window.removeEventListener("mouseup", onUp);
};
}, [thumbHeight]);
const onThumbMouseDown = (e: React.MouseEvent) => {
dragging.current = true;
startY.current = e.clientY;
if (scrollRef.current) startScrollTop.current = scrollRef.current.scrollTop;
document.body.style.userSelect = "none";
};
return (
<div className={`${styles.container} ${className || ""}`} style={{ position: "relative", ...style }}>
<div ref={scrollRef} className={styles.content} tabIndex={0}>
{children}
</div>
<div className={styles.track} aria-hidden={false}>
<div
ref={thumbRef}
role="scrollbar"
aria-orientation="vertical"
aria-valuemin={0}
aria-valuemax={100}
className={styles.thumb}
style={{ height: thumbHeight, transform: `translateY(${thumbTop}px)` }}
onMouseDown={onThumbMouseDown}
/>
</div>
</div>
);
}

View File

@ -0,0 +1,35 @@
.container {
position: relative;
}
.content {
overflow: auto;
max-height: 100%;
/* hide native scrollbars but keep scrolling */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.content::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
.track {
pointer-events: none; /* track itself not interactive */
position: absolute;
right: 8px;
top: 0;
bottom: 0;
width: 12px;
display: flex;
align-items: flex-start;
justify-content: center;
}
.thumb {
pointer-events: auto; /* thumb is interactive */
width: 8px;
margin-top: 4px;
background: rgba(0, 0, 0, 0.32);
border-radius: 9999px;
transition: background 0.12s ease;
}
.thumb:hover {
background: rgba(0, 0, 0, 0.6);
}