mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-26 19:16:24 +00:00
fix: 修复评论区用户态异常
This commit is contained in:
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
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