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 && (
<>
)}
- {replyCount > 0 && (
-
- )}
+
{/* 这俩输入框一次只能显示一个 */}
{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 (
+
+ );
+}
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);
+}