From 16d8eae61f39b96c384b35bf95c1e94d91101e18 Mon Sep 17 00:00:00 2001 From: Snowykami Date: Mon, 15 Sep 2025 16:56:46 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=AF=84=E8=AE=BA?= =?UTF-8?q?=E5=8C=BA=E7=94=A8=E6=88=B7=E6=80=81=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/comment/comment-item.tsx | 41 ++++--- web/src/components/comment/index.tsx | 11 +- .../layout/avatar-with-dropdown-menu.tsx | 10 +- .../components/overlay/OverlayScrollbar.tsx | 104 ++++++++++++++++++ .../overlay/overlay-scrollbar.module.css | 35 ++++++ 5 files changed, 168 insertions(+), 33 deletions(-) create mode 100644 web/src/components/overlay/OverlayScrollbar.tsx create mode 100644 web/src/components/overlay/overlay-scrollbar.module.css diff --git a/web/src/components/comment/comment-item.tsx b/web/src/components/comment/comment-item.tsx index 8eb5c4a..b9d8926 100644 --- a/web/src/components/comment/comment-item.tsx +++ b/web/src/components/comment/comment-item.tsx @@ -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:
- -
, - }); + action: { + label: commonT("login"), + onClick: clickToLogin, + }, + }) return; } // 提前转换状态,让用户觉得响应很快 @@ -202,6 +198,11 @@ export function CommentItem( {commentState.os && {commentState.os}}
+ {replyCount > 0 && ( + + )} {/* 回复按钮 */} - + {/* 编辑和删除按钮 仅自己的评论可见 */} - {user?.id === commentState.user.id && ( + {loginUser?.id === commentState.user.id && ( <> - )} +
{/* 这俩输入框一次只能显示一个 */} {activeInput && activeInput.type === 'reply' && activeInput.id === commentState.id && } {activeInput && activeInput.type === 'edit' && activeInput.id === commentState.id && ( (null); + const [loginUser, setLoginUser] = useState(null); const [comments, setComments] = useState([]); 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(
{t("comment")} ({totalCommentCount})
@@ -126,7 +127,7 @@ export function CommentSection(
(null); @@ -43,19 +44,16 @@ export function AvatarWithDropdownMenu() { My Account - + {user && Profile - + } - Billing - - Console diff --git a/web/src/components/overlay/OverlayScrollbar.tsx b/web/src/components/overlay/OverlayScrollbar.tsx new file mode 100644 index 0000000..611f9e8 --- /dev/null +++ b/web/src/components/overlay/OverlayScrollbar.tsx @@ -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(null); + const thumbRef = useRef(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 ( +
+
+ {children} +
+ +
+
+
+
+ ); +} diff --git a/web/src/components/overlay/overlay-scrollbar.module.css b/web/src/components/overlay/overlay-scrollbar.module.css new file mode 100644 index 0000000..45fc5d0 --- /dev/null +++ b/web/src/components/overlay/overlay-scrollbar.module.css @@ -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); +}