From 4da06b931f51f1220fb5fa40b78942e3e87bffde Mon Sep 17 00:00:00 2001 From: Snowykami Date: Sat, 13 Sep 2025 16:42:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=AF=84=E8=AE=BA?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E7=9A=84=E5=AE=A2=E6=88=B7=E7=AB=AF=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E6=98=BE=E7=A4=BA=E9=80=89=E9=A1=B9=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3=E5=92=8C=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/dto/comment.go | 40 ++--- internal/repo/comment.go | 2 +- internal/service/comment.go | 103 +++++------ web/src/api/comment.ts | 10 +- web/src/components/comment/comment-input.tsx | 8 +- web/src/components/comment/comment-item.tsx | 171 ++++++++++--------- web/src/components/common/markdown.tsx | 57 +++++++ web/src/models/comment.ts | 1 + 8 files changed, 218 insertions(+), 174 deletions(-) diff --git a/internal/dto/comment.go b/internal/dto/comment.go index 5af89f5..3c9ea6f 100644 --- a/internal/dto/comment.go +++ b/internal/dto/comment.go @@ -1,22 +1,23 @@ package dto type CommentDto struct { - ID uint `json:"id"` - TargetID uint `json:"target_id"` - TargetType string `json:"target_type"` // 目标类型,如 "post", "page" - Content string `json:"content"` - ReplyID uint `json:"reply_id"` // 回复的评论ID - Depth int `json:"depth"` // 评论的层级深度 - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - User UserDto `json:"user"` // 评论的 - ReplyCount uint64 `json:"reply_count"` // 回复数量 - LikeCount uint64 `json:"like_count"` // 点赞数量 - IsLiked bool `json:"is_liked"` // 当前用户是否点赞 - IsPrivate bool `json:"is_private"` - Location string `json:"location"` // 用户位置,基于IP - OS string `json:"os"` // 用户操作系统,基于User-Agent - Browser string `json:"browser"` // 用户浏览器,基于User-Agent + ID uint `json:"id"` + TargetID uint `json:"target_id"` + TargetType string `json:"target_type"` // 目标类型,如 "post", "page" + Content string `json:"content"` + ReplyID uint `json:"reply_id"` // 回复的评论ID + Depth int `json:"depth"` // 评论的层级深度 + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + User UserDto `json:"user"` // 评论的 + ReplyCount uint64 `json:"reply_count"` // 回复数量 + LikeCount uint64 `json:"like_count"` // 点赞数量 + IsLiked bool `json:"is_liked"` // 当前用户是否点赞 + IsPrivate bool `json:"is_private"` + Location string `json:"location"` // 用户位置,基于IP + OS string `json:"os"` // 用户操作系统,基于User-Agent + Browser string `json:"browser"` // 用户浏览器,基于User-Agent + ShowClientInfo bool `json:"show_client_info"` } type CreateCommentReq struct { @@ -31,9 +32,10 @@ type CreateCommentReq struct { } type UpdateCommentReq struct { - CommentID uint `json:"comment_id" binding:"required"` // 评论ID - Content string `json:"content" binding:"required"` // 评论内容 - IsPrivate bool `json:"is_private"` // 是否私密 + CommentID uint `json:"comment_id" binding:"required"` // 评论ID + Content string `json:"content" binding:"required"` // 评论内容 + IsPrivate bool `json:"is_private"` // 是否私密 + ShowClientInfo bool `json:"show_client_info"` // 是否显示客户端信息 } type GetCommentListReq struct { diff --git a/internal/repo/comment.go b/internal/repo/comment.go index 41246ef..688c2e3 100644 --- a/internal/repo/comment.go +++ b/internal/repo/comment.go @@ -127,7 +127,7 @@ func (cr *CommentRepo) UpdateComment(comment *model.Comment) error { return errs.New(http.StatusBadRequest, "invalid comment ID", nil) } - if err := GetDB().Select("IsPrivate", "Content").Updates(comment).Error; err != nil { + if err := GetDB().Select("IsPrivate", "ShowClientInfo", "Content").Updates(comment).Error; err != nil { return err } diff --git a/internal/service/comment.go b/internal/service/comment.go index 714c62a..77d69d8 100644 --- a/internal/service/comment.go +++ b/internal/service/comment.go @@ -4,6 +4,7 @@ import ( "context" "strconv" + "github.com/sirupsen/logrus" "github.com/snowykami/neo-blog/pkg/constant" "github.com/snowykami/neo-blog/pkg/utils" @@ -59,6 +60,7 @@ func (cs *CommentService) UpdateComment(ctx context.Context, req *dto.UpdateComm if !ok { return errs.ErrUnauthorized } + logrus.Infof("UpdateComment: currentUser ID %d, req.CommentID %d", currentUser.ID, req.CommentID) comment, err := repo.Comment.GetComment(strconv.Itoa(int(req.CommentID))) if err != nil { @@ -71,13 +73,11 @@ func (cs *CommentService) UpdateComment(ctx context.Context, req *dto.UpdateComm comment.Content = req.Content comment.IsPrivate = req.IsPrivate - + comment.ShowClientInfo = req.ShowClientInfo err = repo.Comment.UpdateComment(comment) - if err != nil { return err } - return nil } @@ -102,7 +102,6 @@ func (cs *CommentService) DeleteComment(ctx context.Context, commentID string) e if err := repo.Comment.DeleteComment(commentID); err != nil { return err } - return nil } @@ -120,38 +119,7 @@ func (cs *CommentService) GetComment(ctx context.Context, commentID string) (*dt if comment.IsPrivate && currentUserID != comment.UserID { return nil, errs.ErrForbidden } - isLiked := false - if currentUserID != 0 { - isLiked, _ = repo.Like.IsLiked(currentUserID, comment.ID, constant.TargetTypeComment) - } - ua := utils.ParseUA(comment.UserAgent) - if !comment.ShowClientInfo { - comment.Location = "" - ua.OS = "" - ua.OSVersion = "" - ua.Browser = "" - ua.BrowserVer = "" - } - - commentDto := dto.CommentDto{ - ID: comment.ID, - Content: comment.Content, - TargetID: comment.TargetID, - TargetType: comment.TargetType, - ReplyID: comment.ReplyID, - CreatedAt: comment.CreatedAt.String(), - UpdatedAt: comment.UpdatedAt.String(), - Depth: comment.Depth, - User: comment.User.ToDto(), - ReplyCount: comment.CommentCount, - LikeCount: comment.LikeCount, - IsLiked: isLiked, - IsPrivate: comment.IsPrivate, - OS: ua.OS + " " + ua.OSVersion, - Browser: ua.Browser + " " + ua.BrowserVer, - Location: comment.Location, - } - + commentDto := cs.toGetCommentDto(comment, currentUserID) return &commentDto, err } @@ -167,39 +135,46 @@ func (cs *CommentService) GetCommentList(ctx context.Context, req *dto.GetCommen commentDtos := make([]dto.CommentDto, 0) for _, comment := range comments { //replyCount, _ := repo.Comment.CountReplyComments(currentUserID, comment.ID) - isLiked := false - if currentUserID != 0 { - isLiked, _ = repo.Like.IsLiked(currentUserID, comment.ID, constant.TargetTypeComment) - } - ua := utils.ParseUA(comment.UserAgent) - commentDto := dto.CommentDto{ - ID: comment.ID, - Content: comment.Content, - TargetID: comment.TargetID, - TargetType: comment.TargetType, - ReplyID: comment.ReplyID, - CreatedAt: comment.CreatedAt.String(), - UpdatedAt: comment.UpdatedAt.String(), - Depth: comment.Depth, - User: comment.User.ToDto(), - ReplyCount: comment.CommentCount, - LikeCount: comment.LikeCount, - IsLiked: isLiked, - IsPrivate: comment.IsPrivate, - OS: ua.OS + " " + ua.OSVersion, - Browser: ua.Browser + " " + ua.BrowserVer, - Location: comment.Location, - } - if !comment.ShowClientInfo { - commentDto.Location = "" - commentDto.OS = "" - commentDto.Browser = "" - } + commentDto := cs.toGetCommentDto(&comment, currentUserID) commentDtos = append(commentDtos, commentDto) } return commentDtos, nil } +func (cs *CommentService) toGetCommentDto(comment *model.Comment, currentUserID uint) dto.CommentDto { + isLiked := false + if currentUserID != 0 { + isLiked, _ = repo.Like.IsLiked(currentUserID, comment.ID, constant.TargetTypeComment) + } + ua := utils.ParseUA(comment.UserAgent) + if !comment.ShowClientInfo { + comment.Location = "" + ua.OS = "" + ua.OSVersion = "" + ua.Browser = "" + ua.BrowserVer = "" + } + + return dto.CommentDto{ + ID: comment.ID, + Content: comment.Content, + TargetID: comment.TargetID, + TargetType: comment.TargetType, + ReplyID: comment.ReplyID, + CreatedAt: comment.CreatedAt.String(), + UpdatedAt: comment.UpdatedAt.String(), + Depth: comment.Depth, + User: comment.User.ToDto(), + ReplyCount: comment.CommentCount, + LikeCount: comment.LikeCount, + IsLiked: isLiked, + IsPrivate: comment.IsPrivate, + OS: ua.OS + " " + ua.OSVersion, + Browser: ua.Browser + " " + ua.BrowserVer, + Location: comment.Location, + ShowClientInfo: comment.ShowClientInfo, + } +} func (cs *CommentService) checkTargetExists(targetID uint, targetType string) (bool, error) { switch targetType { case constant.TargetTypePost: diff --git a/web/src/api/comment.ts b/web/src/api/comment.ts index 32029b0..728f003 100644 --- a/web/src/api/comment.ts +++ b/web/src/api/comment.ts @@ -36,17 +36,21 @@ export async function createComment( export async function updateComment( { - id, content, - isPrivate = false + id, + content, + isPrivate = false, + showClientInfo = true }: { id: number content: string isPrivate?: boolean // 可选字段,默认为 false + showClientInfo?: boolean } ): Promise> { const res = await axiosClient.put>(`/comment/c/${id}`, { content, - isPrivate + isPrivate, + showClientInfo, }) return res.data } diff --git a/web/src/components/comment/comment-input.tsx b/web/src/components/comment/comment-input.tsx index cf97341..3e28573 100644 --- a/web/src/components/comment/comment-input.tsx +++ b/web/src/components/comment/comment-input.tsx @@ -17,7 +17,8 @@ export function CommentInput( initContent = "", initIsPrivate = false, placeholder = "", - isUpdate = false + isUpdate = false, + initShowClientInfo = true }: { user: User | null, onCommentSubmitted: ({ commentContent, isPrivate, showClientInfo }: { commentContent: string, isPrivate: boolean, showClientInfo: boolean }) => void, @@ -25,6 +26,7 @@ export function CommentInput( initIsPrivate?: boolean, placeholder?: string, isUpdate?: boolean, + initShowClientInfo?: boolean } ) { const t = useTranslations('Comment') @@ -33,7 +35,7 @@ export function CommentInput( const clickToUserProfile = useToUserProfile(); const [isPrivate, setIsPrivate] = useState(initIsPrivate); - const [showClientInfo, setShowClientInfo] = useState(true); + const [showClientInfo, setShowClientInfo] = useState(initShowClientInfo); const [commentContent, setCommentContent] = useState(initContent); const handleCommentSubmit = async () => { @@ -51,7 +53,7 @@ export function CommentInput( toast.error(t("content_required")); return; } - if (initContent === commentContent.trim() && initIsPrivate === isPrivate) { + if (initContent === commentContent.trim() && initIsPrivate === isPrivate && initShowClientInfo === showClientInfo) { toast.warning(t("comment_unchanged")); return; } diff --git a/web/src/components/comment/comment-item.tsx b/web/src/components/comment/comment-item.tsx index b7ef40a..30b6e0f 100644 --- a/web/src/components/comment/comment-item.tsx +++ b/web/src/components/comment/comment-item.tsx @@ -10,7 +10,7 @@ import { TargetType } from "@/models/types"; import { toggleLike } from "@/api/like"; import { useDoubleConfirm } from "@/hooks/use-double-confirm"; import { CommentInput } from "./comment-input"; -import { createComment, deleteComment, listComments, updateComment } from "@/api/comment"; +import { createComment, deleteComment, getComment, listComments, updateComment } from "@/api/comment"; import { OrderBy } from "@/models/common"; import { formatDateTime } from "@/utils/common/datetime"; @@ -41,11 +41,11 @@ export function CommentItem( const clickToLogin = useToLogin(); const { confirming, onClick, onBlur } = useDoubleConfirm(); - const [likeCount, setLikeCount] = useState(comment.likeCount); - const [liked, setLiked] = useState(comment.isLiked); + const [commentState, setCommentState] = useState(comment); // 用于更新评论内容 + const [likeCount, setLikeCount] = useState(commentState.likeCount); + const [liked, setLiked] = useState(commentState.isLiked); const [canClickLike, setCanClickLike] = useState(true); - const [isPrivate, setIsPrivate] = useState(comment.isPrivate); - const [replyCount, setReplyCount] = useState(comment.replyCount); + const [replyCount, setReplyCount] = useState(commentState.replyCount); const [showReplies, setShowReplies] = useState(false); const [replies, setReplies] = useState([]); const [repliesLoaded, setRepliesLoaded] = useState(false); @@ -74,7 +74,7 @@ export function CommentItem( setLiked(prev => !prev); setLikeCount(prev => prev + (likedPrev ? -1 : 1)); toggleLike( - { targetType: TargetType.Comment, targetId: comment.id } + { targetType: TargetType.Comment, targetId: commentState.id } ).then(res => { toast.success(res.data.status ? t("like_success") : t("unlike_success")); setCanClickLike(true); @@ -90,14 +90,14 @@ export function CommentItem( const reloadReplies = () => { listComments( { - targetType: comment.targetType, - targetId: comment.targetId, - depth: comment.depth + 1, + targetType: commentState.targetType, + targetId: commentState.targetId, + depth: commentState.depth + 1, orderBy: OrderBy.CreatedAt, desc: false, page: 1, size: 999999, - commentId: comment.id + commentId: commentState.id } ).then(response => { setReplies(response.data.comments); @@ -112,11 +112,13 @@ export function CommentItem( setShowReplies(!showReplies); } - const onCommentEdit = ({ commentContent: newContent, isPrivate }: { commentContent: string, isPrivate: boolean }) => { - updateComment({ id: comment.id, content: newContent, isPrivate }).then(() => { + const onCommentEdit = ({ commentContent: newContent, isPrivate, showClientInfo }: { commentContent: string, isPrivate: boolean, showClientInfo: boolean }) => { + updateComment({ id: commentState.id, content: newContent, isPrivate, showClientInfo }).then(() => { toast.success(t("edit_success")); - comment.content = newContent; - setIsPrivate(isPrivate); + getComment({ id: commentState.id }).then(response => { + setCommentState(response.data); + console.log(response.data); + }); setActiveInputId(null); }).catch(error => { toast.error(t("edit_failed") + ": " + error.message); @@ -125,10 +127,10 @@ export function CommentItem( const onReply = ({ commentContent, isPrivate, showClientInfo }: { commentContent: string, isPrivate: boolean, showClientInfo: boolean }) => { createComment({ - targetType: comment.targetType, - targetId: comment.targetId, + targetType: commentState.targetType, + targetId: commentState.targetId, content: commentContent, - replyId: comment.id, + replyId: commentState.id, isPrivate, showClientInfo }).then(() => { @@ -158,24 +160,24 @@ export function CommentItem( return (
-
clickToUserProfile(comment.user.username)} className="cursor-pointer fade-in w-12 h-12"> - +
clickToUserProfile(commentState.user.username)} className="cursor-pointer fade-in w-12 h-12"> +
-
clickToUserProfile(comment.user.username)} className="font-bold text-base text-slate-800 dark:text-slate-100 cursor-pointer fade-in-up"> - {comment.user.nickname} +
clickToUserProfile(commentState.user.username)} className="font-bold text-base text-slate-800 dark:text-slate-100 cursor-pointer fade-in-up"> + {commentState.user.nickname}
{formatDateTime({ - dateTimeString: comment.createdAt, + dateTimeString: commentState.createdAt, locale, convertShortAgo: true, unitI18n: { secondsAgo: commonT("secondsAgo"), minutesAgo: commonT("minutesAgo"), hoursAgo: commonT("hoursAgo"), daysAgo: commonT("daysAgo") } })} - {comment.createdAt !== comment.updatedAt && + {commentState.createdAt !== commentState.updatedAt && {t("edit_at", { time: formatDateTime({ - dateTimeString: comment.updatedAt, + dateTimeString: commentState.updatedAt, locale, convertShortAgo: true, unitI18n: { secondsAgo: commonT("secondsAgo"), minutesAgo: commonT("minutesAgo"), hoursAgo: commonT("hoursAgo"), daysAgo: commonT("daysAgo") } @@ -184,105 +186,106 @@ export function CommentItem(

{ - isPrivate && + commentState.isPrivate && } { parentComment && <>{t("reply")} : } - {comment.content} + {commentState.content}

-
+
{/* 用户地理,浏览器,系统信息 */} - {comment.location && {comment.location}} - {comment.browser && {comment.browser}} - {comment.os && {comment.os}} + {commentState.location && {commentState.location}} + {commentState.browser && {commentState.browser}} + {commentState.os && {commentState.os}}
{/* 回复按钮 */} {/* 点赞按钮 */} {/* 编辑和删除按钮 仅自己的评论可见 */} - {user?.id === comment.user.id && ( - <> - + rounded ${activeInput?.type === 'edit' && activeInput.id === commentState.id ? "bg-slate-600" : "bg-slate-400"} hover:bg-slate-600 dark:hover:bg-slate-500 fade-in-up`} + > + + - - + onClick={() => onClick(() => { onCommentDelete({ commentId: commentState.id }); })} + onBlur={onBlur} + > + + {confirming && ( + {t("confirm_delete")} + )} + + )} {replyCount > 0 && ( - + )}
-
+
{/* 这俩输入框一次只能显示一个 */} - {activeInput && activeInput.type === 'reply' && activeInput.id === comment.id && } - {activeInput && activeInput.type === 'edit' && activeInput.id === comment.id && }
@@ -294,7 +297,7 @@ export function CommentItem( key={reply.id} user={reply.user} comment={reply} - parentComment={comment} + parentComment={commentState} onCommentDelete={onReplyDelete} activeInput={activeInput} setActiveInputId={setActiveInputId} diff --git a/web/src/components/common/markdown.tsx b/web/src/components/common/markdown.tsx index b6656dd..6f272c7 100644 --- a/web/src/components/common/markdown.tsx +++ b/web/src/components/common/markdown.tsx @@ -59,6 +59,63 @@ export const markdownComponents = { {...props} /> ), + table: (props: React.ComponentPropsWithoutRef<"table">) => { + const { children, className, ...rest } = props; + return ( +
+ + {children} +
+
+ ); + }, + thead: (props: React.ComponentPropsWithoutRef<"thead">) => { + const { children, className, ...rest } = props; + return ( + + {children} + + ); + }, + tbody: (props: React.ComponentPropsWithoutRef<"tbody">) => { + const { children, className, ...rest } = props; + return ( + + {children} + + ); + }, + tr: (props: React.ComponentPropsWithoutRef<"tr">) => { + const { children, className, ...rest } = props; + return ( + + {children} + + ); + }, + th: (props: React.ComponentPropsWithoutRef<"th">) => { + const { children, className, ...rest } = props; + return ( + + {children} + + ); + }, + td: (props: React.ComponentPropsWithoutRef<"td">) => { + const { children, className, ...rest } = props; + return ( + + {children} + + ); + }, }; export function RenderMarkdown(props: Omit) { diff --git a/web/src/models/comment.ts b/web/src/models/comment.ts index c7d2c2a..146a11b 100644 --- a/web/src/models/comment.ts +++ b/web/src/models/comment.ts @@ -18,4 +18,5 @@ export interface Comment { os: string browser: string location: string + showClientInfo: boolean }