mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-26 11:06:23 +00:00
fix: 修复评论区用户态异常
This commit is contained in:
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
104
web/src/components/overlay/OverlayScrollbar.tsx
Normal file
104
web/src/components/overlay/OverlayScrollbar.tsx
Normal 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>
|
||||
);
|
||||
}
|
35
web/src/components/overlay/overlay-scrollbar.module.css
Normal file
35
web/src/components/overlay/overlay-scrollbar.module.css
Normal 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);
|
||||
}
|
Reference in New Issue
Block a user