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(
{
user,
loginUser,
comment,
parentComment,
onCommentDelete,
@ -25,7 +25,7 @@ export function CommentItem(
setActiveInputId,
onReplySubmitted // 评论区计数更新用
}: {
user: User | null,
loginUser: User | null,
comment: Comment,
parentComment: Comment | null,
onCommentDelete: ({ commentId }: { commentId: number }) => void,
@ -55,17 +55,13 @@ export function CommentItem(
return;
}
setCanClickLike(false);
if (!user) {
if (!loginUser) {
toast.error(t("login_required"), {
action: <div className="flex justify-end">
<button
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>,
});
action: {
label: commonT("login"),
onClick: clickToLogin,
},
})
return;
}
// 提前转换状态,让用户觉得响应很快
@ -202,6 +198,11 @@ export function CommentItem(
{commentState.os && <span title={commentState.os}>{commentState.os}</span>}
</div>
<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
title={t("reply")}
@ -228,9 +229,9 @@ export function CommentItem(
>
<Heart className="w-3 h-3" /> <div>{likeCount}</div>
</button>
{/* 编辑和删除按钮 仅自己的评论可见 */}
{user?.id === commentState.user.id && (
{loginUser?.id === commentState.user.id && (
<>
<button
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>
{/* 这俩输入框一次只能显示一个 */}
{activeInput && activeInput.type === 'reply' && activeInput.id === commentState.id && <CommentInput
user={user}
user={loginUser}
onCommentSubmitted={onReply}
initIsPrivate={commentState.isPrivate}
placeholder={`${t("reply")} ${commentState.user.nickname || commentState.user.username} :`}
/>}
{activeInput && activeInput.type === 'edit' && activeInput.id === commentState.id && <CommentInput
user={user}
user={loginUser}
initContent={commentState.content}
initIsPrivate={commentState.isPrivate}
onCommentSubmitted={onCommentEdit}
@ -295,7 +292,7 @@ export function CommentItem(
{replies.map((reply) => (
<CommentItem
key={reply.id}
user={reply.user}
loginUser={loginUser}
comment={reply}
parentComment={commentState}
onCommentDelete={onReplyDelete}

View File

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

View File

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