feat: 添加评论功能的客户端信息显示选项,更新相关接口和组件

This commit is contained in:
2025-09-13 16:04:09 +08:00
parent 011dc298c2
commit 2d0e1a46e2
10 changed files with 279 additions and 234 deletions

View File

@ -1,47 +1,48 @@
package dto package dto
type CommentDto struct { type CommentDto struct {
ID uint `json:"id"` ID uint `json:"id"`
TargetID uint `json:"target_id"` TargetID uint `json:"target_id"`
TargetType string `json:"target_type"` // 目标类型,如 "post", "page" TargetType string `json:"target_type"` // 目标类型,如 "post", "page"
Content string `json:"content"` Content string `json:"content"`
ReplyID uint `json:"reply_id"` // 回复的评论ID ReplyID uint `json:"reply_id"` // 回复的评论ID
Depth int `json:"depth"` // 评论的层级深度 Depth int `json:"depth"` // 评论的层级深度
CreatedAt string `json:"created_at"` CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"` UpdatedAt string `json:"updated_at"`
User UserDto `json:"user"` // 评论的 User UserDto `json:"user"` // 评论的
ReplyCount uint64 `json:"reply_count"` // 回复数量 ReplyCount uint64 `json:"reply_count"` // 回复数量
LikeCount uint64 `json:"like_count"` // 点赞数量 LikeCount uint64 `json:"like_count"` // 点赞数量
IsLiked bool `json:"is_liked"` // 当前用户是否点赞 IsLiked bool `json:"is_liked"` // 当前用户是否点赞
IsPrivate bool `json:"is_private"` IsPrivate bool `json:"is_private"`
Location string `json:"location"` // 用户位置基于IP Location string `json:"location"` // 用户位置基于IP
OS string `json:"os"` // 用户操作系统基于User-Agent OS string `json:"os"` // 用户操作系统基于User-Agent
Browser string `json:"browser"` // 用户浏览器基于User-Agent Browser string `json:"browser"` // 用户浏览器基于User-Agent
} }
type CreateCommentReq struct { type CreateCommentReq struct {
TargetID uint `json:"target_id" binding:"required"` // 目标ID TargetID uint `json:"target_id" binding:"required"` // 目标ID
TargetType string `json:"target_type" binding:"required"` // 目标类型,如 "post", "page" TargetType string `json:"target_type" binding:"required"` // 目标类型,如 "post", "page"
Content string `json:"content" binding:"required"` // 评论内容 Content string `json:"content" binding:"required"` // 评论内容
ReplyID uint `json:"reply_id"` // 回复的评论ID ReplyID uint `json:"reply_id"` // 回复的评论ID
IsPrivate bool `json:"is_private"` // 是否私密评论默认false IsPrivate bool `json:"is_private"` // 是否私密评论默认false
RemoteAddr string `json:"remote_addr"` // 远程地址 RemoteAddr string `json:"remote_addr"` // 远程地址
UserAgent string `json:"user_agent"` // 用户代理 UserAgent string `json:"user_agent"` // 用户代理
ShowClientInfo bool `json:"show_client_info"` // 是否显示客户端信息
} }
type UpdateCommentReq struct { type UpdateCommentReq struct {
CommentID uint `json:"comment_id" binding:"required"` // 评论ID CommentID uint `json:"comment_id" binding:"required"` // 评论ID
Content string `json:"content" binding:"required"` // 评论内容 Content string `json:"content" binding:"required"` // 评论内容
IsPrivate bool `json:"is_private"` // 是否私密 IsPrivate bool `json:"is_private"` // 是否私密
} }
type GetCommentListReq struct { type GetCommentListReq struct {
TargetID uint `json:"target_id" binding:"required"` TargetID uint `json:"target_id" binding:"required"`
TargetType string `json:"target_type" binding:"required"` TargetType string `json:"target_type" binding:"required"`
CommentID uint `json:"comment_id"` // 获取某条评论的所有子评论 CommentID uint `json:"comment_id"` // 获取某条评论的所有子评论
OrderBy string `json:"order_by"` // 排序方式 OrderBy string `json:"order_by"` // 排序方式
Page uint64 `json:"page"` // 页码 Page uint64 `json:"page"` // 页码
Size uint64 `json:"size"` Size uint64 `json:"size"`
Desc bool `json:"desc"` Desc bool `json:"desc"`
Depth int `json:"depth"` // 评论的层级深度 Depth int `json:"depth"` // 评论的层级深度
} }

View File

@ -7,19 +7,20 @@ import (
type Comment struct { type Comment struct {
gorm.Model gorm.Model
UserID uint `gorm:"index"` // 评论的用户ID UserID uint `gorm:"index"` // 评论的用户ID
User User `gorm:"foreignKey:UserID;references:ID"` // 关联的用户 User User `gorm:"foreignKey:UserID;references:ID"` // 关联的用户
TargetID uint `gorm:"index"` // 目标ID TargetID uint `gorm:"index"` // 目标ID
TargetType string `gorm:"index"` // 目标类型,如 "post", "page" TargetType string `gorm:"index"` // 目标类型,如 "post", "page"
ReplyID uint `gorm:"index"` // 回复的评论ID ReplyID uint `gorm:"index"` // 回复的评论ID
Content string `gorm:"type:text"` // 评论内容 Content string `gorm:"type:text"` // 评论内容
Depth int `gorm:"default:0"` // 评论的层级深度,从0开始计数 Depth int `gorm:"default:0"` // 评论的层级深度,从0开始计数
IsPrivate bool `gorm:"default:false"` // 是否为私密评论,私密评论只有评论者和被评论对象所有者可见 IsPrivate bool `gorm:"default:false"` // 是否为私密评论,私密评论只有评论者和被评论对象所有者可见
RemoteAddr string `gorm:"type:text"` // 远程地址 RemoteAddr string `gorm:"type:text"` // 远程地址
UserAgent string `gorm:"type:text"` UserAgent string `gorm:"type:text"`
Location string `gorm:"type:text"` // 用户位置基于IP Location string `gorm:"type:text"` // 用户位置基于IP
LikeCount uint64 LikeCount uint64
CommentCount uint64 CommentCount uint64
ShowClientInfo bool `gorm:"default:false"` // 是否显示客户端信息
} }
func (c *Comment) AfterCreate(tx *gorm.DB) (err error) { func (c *Comment) AfterCreate(tx *gorm.DB) (err error) {

View File

@ -121,6 +121,7 @@ func (cr *CommentRepo) CreateComment(comment *model.Comment) (uint, error) {
}) })
return commentID, err return commentID, err
} }
func (cr *CommentRepo) UpdateComment(comment *model.Comment) error { func (cr *CommentRepo) UpdateComment(comment *model.Comment) error {
if comment.ID == 0 { if comment.ID == 0 {
return errs.New(http.StatusBadRequest, "invalid comment ID", nil) return errs.New(http.StatusBadRequest, "invalid comment ID", nil)

View File

@ -1,184 +1,213 @@
package service package service
import ( import (
"context" "context"
"strconv" "strconv"
"github.com/snowykami/neo-blog/pkg/constant" "github.com/snowykami/neo-blog/pkg/constant"
"github.com/snowykami/neo-blog/pkg/utils" "github.com/snowykami/neo-blog/pkg/utils"
"github.com/snowykami/neo-blog/internal/ctxutils" "github.com/snowykami/neo-blog/internal/ctxutils"
"github.com/snowykami/neo-blog/internal/dto" "github.com/snowykami/neo-blog/internal/dto"
"github.com/snowykami/neo-blog/internal/model" "github.com/snowykami/neo-blog/internal/model"
"github.com/snowykami/neo-blog/internal/repo" "github.com/snowykami/neo-blog/internal/repo"
"github.com/snowykami/neo-blog/pkg/errs" "github.com/snowykami/neo-blog/pkg/errs"
) )
type CommentService struct{} type CommentService struct{}
func NewCommentService() *CommentService { func NewCommentService() *CommentService {
return &CommentService{} return &CommentService{}
} }
func (cs *CommentService) CreateComment(ctx context.Context, req *dto.CreateCommentReq) (uint, error) { func (cs *CommentService) CreateComment(ctx context.Context, req *dto.CreateCommentReq) (uint, error) {
currentUser, ok := ctxutils.GetCurrentUser(ctx) currentUser, ok := ctxutils.GetCurrentUser(ctx)
if !ok { if !ok {
return 0, errs.ErrUnauthorized return 0, errs.ErrUnauthorized
} }
if ok, err := cs.checkTargetExists(req.TargetID, req.TargetType); !ok { if ok, err := cs.checkTargetExists(req.TargetID, req.TargetType); !ok {
if err != nil { if err != nil {
return 0, errs.New(errs.ErrBadRequest.Code, "target not found", err) return 0, errs.New(errs.ErrBadRequest.Code, "target not found", err)
} }
return 0, errs.ErrBadRequest return 0, errs.ErrBadRequest
} }
comment := &model.Comment{ comment := &model.Comment{
Content: req.Content, Content: req.Content,
ReplyID: req.ReplyID, ReplyID: req.ReplyID,
TargetID: req.TargetID, TargetID: req.TargetID,
TargetType: req.TargetType, TargetType: req.TargetType,
UserID: currentUser.ID, UserID: currentUser.ID,
IsPrivate: req.IsPrivate, IsPrivate: req.IsPrivate,
RemoteAddr: req.RemoteAddr, RemoteAddr: req.RemoteAddr,
UserAgent: req.UserAgent, UserAgent: req.UserAgent,
} ShowClientInfo: req.ShowClientInfo,
}
commentID, err := repo.Comment.CreateComment(comment) commentID, err := repo.Comment.CreateComment(comment)
if err != nil { if err != nil {
return 0, err return 0, err
} }
return commentID, nil return commentID, nil
} }
func (cs *CommentService) UpdateComment(ctx context.Context, req *dto.UpdateCommentReq) error { func (cs *CommentService) UpdateComment(ctx context.Context, req *dto.UpdateCommentReq) error {
currentUser, ok := ctxutils.GetCurrentUser(ctx) currentUser, ok := ctxutils.GetCurrentUser(ctx)
if !ok { if !ok {
return errs.ErrUnauthorized return errs.ErrUnauthorized
} }
comment, err := repo.Comment.GetComment(strconv.Itoa(int(req.CommentID))) comment, err := repo.Comment.GetComment(strconv.Itoa(int(req.CommentID)))
if err != nil { if err != nil {
return err return err
} }
if currentUser.ID != comment.UserID { if currentUser.ID != comment.UserID {
return errs.ErrForbidden return errs.ErrForbidden
} }
comment.Content = req.Content comment.Content = req.Content
comment.IsPrivate = req.IsPrivate comment.IsPrivate = req.IsPrivate
err = repo.Comment.UpdateComment(comment) err = repo.Comment.UpdateComment(comment)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
func (cs *CommentService) DeleteComment(ctx context.Context, commentID string) error { func (cs *CommentService) DeleteComment(ctx context.Context, commentID string) error {
currentUser, ok := ctxutils.GetCurrentUser(ctx) currentUser, ok := ctxutils.GetCurrentUser(ctx)
if !ok { if !ok {
return errs.ErrUnauthorized return errs.ErrUnauthorized
} }
if commentID == "" { if commentID == "" {
return errs.ErrBadRequest return errs.ErrBadRequest
} }
comment, err := repo.Comment.GetComment(commentID) comment, err := repo.Comment.GetComment(commentID)
if err != nil { if err != nil {
return errs.New(errs.ErrNotFound.Code, "comment not found", err) return errs.New(errs.ErrNotFound.Code, "comment not found", err)
} }
if comment.UserID != currentUser.ID { if comment.UserID != currentUser.ID {
return errs.ErrForbidden return errs.ErrForbidden
} }
if err := repo.Comment.DeleteComment(commentID); err != nil { if err := repo.Comment.DeleteComment(commentID); err != nil {
return err return err
} }
return nil return nil
} }
func (cs *CommentService) GetComment(ctx context.Context, commentID string) (*dto.CommentDto, error) { func (cs *CommentService) GetComment(ctx context.Context, commentID string) (*dto.CommentDto, error) {
comment, err := repo.Comment.GetComment(commentID) comment, err := repo.Comment.GetComment(commentID)
if err != nil { if err != nil {
return nil, errs.New(errs.ErrNotFound.Code, "comment not found", err) return nil, errs.New(errs.ErrNotFound.Code, "comment not found", err)
} }
commentDto := dto.CommentDto{ currentUserID := uint(0)
ID: comment.ID, if currentUser, ok := ctxutils.GetCurrentUser(ctx); ok {
TargetID: comment.TargetID, currentUserID = currentUser.ID
TargetType: comment.TargetType, }
Content: comment.Content, if comment.IsPrivate && currentUserID != comment.UserID {
ReplyID: comment.ReplyID, return nil, errs.ErrForbidden
Depth: comment.Depth, }
CreatedAt: comment.CreatedAt.String(), isLiked := false
UpdatedAt: comment.UpdatedAt.String(), if currentUserID != 0 {
User: comment.User.ToDto(), isLiked, _ = repo.Like.IsLiked(currentUserID, comment.ID, constant.TargetTypeComment)
} }
// TODO: 返回更多字段 ua := utils.ParseUA(comment.UserAgent)
if !comment.ShowClientInfo {
comment.Location = ""
ua.OS = ""
ua.OSVersion = ""
ua.Browser = ""
ua.BrowserVer = ""
}
return &commentDto, err 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,
}
return &commentDto, err
} }
func (cs *CommentService) GetCommentList(ctx context.Context, req *dto.GetCommentListReq) ([]dto.CommentDto, error) { func (cs *CommentService) GetCommentList(ctx context.Context, req *dto.GetCommentListReq) ([]dto.CommentDto, error) {
currentUserID := uint(0) currentUserID := uint(0)
if currentUser, ok := ctxutils.GetCurrentUser(ctx); ok { if currentUser, ok := ctxutils.GetCurrentUser(ctx); ok {
currentUserID = currentUser.ID currentUserID = currentUser.ID
} }
comments, err := repo.Comment.ListComments(currentUserID, req.TargetID, req.CommentID, req.TargetType, req.Page, req.Size, req.OrderBy, req.Desc, req.Depth)
comments, err := repo.Comment.ListComments(currentUserID, req.TargetID, req.CommentID, req.TargetType, req.Page, req.Size, req.OrderBy, req.Desc, req.Depth) if err != nil {
if err != nil { return nil, errs.New(errs.ErrInternalServer.Code, "failed to list comments", err)
return nil, errs.New(errs.ErrInternalServer.Code, "failed to list comments", err) }
} commentDtos := make([]dto.CommentDto, 0)
for _, comment := range comments {
commentDtos := make([]dto.CommentDto, 0) //replyCount, _ := repo.Comment.CountReplyComments(currentUserID, comment.ID)
isLiked := false
for _, comment := range comments { if currentUserID != 0 {
//replyCount, _ := repo.Comment.CountReplyComments(currentUserID, comment.ID) isLiked, _ = repo.Like.IsLiked(currentUserID, comment.ID, constant.TargetTypeComment)
isLiked := false }
if currentUserID != 0 { ua := utils.ParseUA(comment.UserAgent)
isLiked, _ = repo.Like.IsLiked(currentUserID, comment.ID, constant.TargetTypeComment) commentDto := dto.CommentDto{
} ID: comment.ID,
ua := utils.ParseUA(comment.UserAgent) Content: comment.Content,
commentDto := dto.CommentDto{ TargetID: comment.TargetID,
ID: comment.ID, TargetType: comment.TargetType,
Content: comment.Content, ReplyID: comment.ReplyID,
TargetID: comment.TargetID, CreatedAt: comment.CreatedAt.String(),
TargetType: comment.TargetType, UpdatedAt: comment.UpdatedAt.String(),
ReplyID: comment.ReplyID, Depth: comment.Depth,
CreatedAt: comment.CreatedAt.String(), User: comment.User.ToDto(),
UpdatedAt: comment.UpdatedAt.String(), ReplyCount: comment.CommentCount,
Depth: comment.Depth, LikeCount: comment.LikeCount,
User: comment.User.ToDto(), IsLiked: isLiked,
ReplyCount: comment.CommentCount, IsPrivate: comment.IsPrivate,
LikeCount: comment.LikeCount, OS: ua.OS + " " + ua.OSVersion,
IsLiked: isLiked, Browser: ua.Browser + " " + ua.BrowserVer,
IsPrivate: comment.IsPrivate, Location: comment.Location,
OS: ua.OS + " " + ua.OSVersion, }
Browser: ua.Browser + " " + ua.BrowserVer, if !comment.ShowClientInfo {
Location: comment.Location, commentDto.Location = ""
} commentDto.OS = ""
commentDtos = append(commentDtos, commentDto) commentDto.Browser = ""
} }
return commentDtos, nil commentDtos = append(commentDtos, commentDto)
}
return commentDtos, nil
} }
func (cs *CommentService) checkTargetExists(targetID uint, targetType string) (bool, error) { func (cs *CommentService) checkTargetExists(targetID uint, targetType string) (bool, error) {
switch targetType { switch targetType {
case constant.TargetTypePost: case constant.TargetTypePost:
if _, err := repo.Post.GetPostByID(strconv.Itoa(int(targetID))); err != nil { if _, err := repo.Post.GetPostByID(strconv.Itoa(int(targetID))); err != nil {
return false, errs.New(errs.ErrNotFound.Code, "post not found", err) return false, errs.New(errs.ErrNotFound.Code, "post not found", err)
} }
default: default:
return false, errs.New(errs.ErrBadRequest.Code, "invalid target type", nil) return false, errs.New(errs.ErrBadRequest.Code, "invalid target type", nil)
} }
return true, nil return true, nil
} }

View File

@ -1,7 +1,7 @@
package utils package utils
import ( import (
"fmt" "fmt"
) )
type oidcUtils struct{} type oidcUtils struct{}
@ -10,59 +10,59 @@ var Oidc = oidcUtils{}
// RequestToken 请求访问令牌 // RequestToken 请求访问令牌
func (u *oidcUtils) RequestToken(tokenEndpoint, clientID, clientSecret, code, redirectURI string) (*TokenResponse, error) { func (u *oidcUtils) RequestToken(tokenEndpoint, clientID, clientSecret, code, redirectURI string) (*TokenResponse, error) {
tokenResp, err := client.R(). tokenResp, err := client.R().
SetFormData(map[string]string{ SetFormData(map[string]string{
"grant_type": "authorization_code", "grant_type": "authorization_code",
"client_id": clientID, "client_id": clientID,
"client_secret": clientSecret, "client_secret": clientSecret,
"code": code, "code": code,
"redirect_uri": redirectURI, "redirect_uri": redirectURI,
}). }).
SetHeader("Accept", "application/json"). SetHeader("Accept", "application/json").
SetResult(&TokenResponse{}). SetResult(&TokenResponse{}).
Post(tokenEndpoint) Post(tokenEndpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if tokenResp.StatusCode() != 200 { if tokenResp.StatusCode() != 200 {
return nil, fmt.Errorf("状态码: %d响应: %s", tokenResp.StatusCode(), tokenResp.String()) return nil, fmt.Errorf("状态码: %d响应: %s", tokenResp.StatusCode(), tokenResp.String())
} }
return tokenResp.Result().(*TokenResponse), nil return tokenResp.Result().(*TokenResponse), nil
} }
// RequestUserInfo 请求用户信息 // RequestUserInfo 请求用户信息
func (u *oidcUtils) RequestUserInfo(userInfoEndpoint, accessToken string) (*UserInfo, error) { func (u *oidcUtils) RequestUserInfo(userInfoEndpoint, accessToken string) (*UserInfo, error) {
userInfoResp, err := client.R(). userInfoResp, err := client.R().
SetHeader("Authorization", "Bearer "+accessToken). SetHeader("Authorization", "Bearer "+accessToken).
SetHeader("Accept", "application/json"). SetHeader("Accept", "application/json").
SetResult(&UserInfo{}). SetResult(&UserInfo{}).
Get(userInfoEndpoint) Get(userInfoEndpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if userInfoResp.StatusCode() != 200 { if userInfoResp.StatusCode() != 200 {
return nil, fmt.Errorf("状态码: %d响应: %s", userInfoResp.StatusCode(), userInfoResp.String()) return nil, fmt.Errorf("状态码: %d响应: %s", userInfoResp.StatusCode(), userInfoResp.String())
} }
return userInfoResp.Result().(*UserInfo), nil return userInfoResp.Result().(*UserInfo), nil
} }
type TokenResponse struct { type TokenResponse struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
TokenType string `json:"token_type"` TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"` ExpiresIn int `json:"expires_in"`
IDToken string `json:"id_token,omitempty"` IDToken string `json:"id_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"` RefreshToken string `json:"refresh_token,omitempty"`
} }
// UserInfo 定义用户信息结构 // UserInfo 定义用户信息结构
type UserInfo struct { type UserInfo struct {
Sub string `json:"sub"` Sub string `json:"sub"`
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Email string `json:"email"`
Picture string `json:"picture,omitempty"` Picture string `json:"picture,omitempty"`
Groups []string `json:"groups,omitempty"` // 可选字段OIDC提供的用户组信息 Groups []string `json:"groups,omitempty"` // 可选字段OIDC提供的用户组信息
} }

View File

@ -12,13 +12,15 @@ export async function createComment(
targetId, targetId,
content, content,
replyId = null, replyId = null,
isPrivate = false isPrivate = false,
showClientInfo = true,
}: { }: {
targetType: TargetType targetType: TargetType
targetId: number targetId: number
content: string content: string
replyId: number | null replyId: number | null
isPrivate: boolean isPrivate: boolean
showClientInfo: boolean
} }
): Promise<BaseResponse<{ id: number }>> { ): Promise<BaseResponse<{ id: number }>> {
const res = await axiosClient.post<BaseResponse<{ id: number }>>('/comment/c', { const res = await axiosClient.post<BaseResponse<{ id: number }>>('/comment/c', {
@ -26,7 +28,8 @@ export async function createComment(
targetId, targetId,
content, content,
replyId, replyId,
isPrivate isPrivate,
showClientInfo,
}) })
return res.data return res.data
} }

View File

@ -3,7 +3,7 @@ import { User } from "@/models/user";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import GravatarAvatar, { getGravatarByUser } from "@/components/common/gravatar"; import GravatarAvatar from "@/components/common/gravatar";
import { CircleUser } from "lucide-react"; import { CircleUser } from "lucide-react";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
@ -20,7 +20,7 @@ export function CommentInput(
isUpdate = false isUpdate = false
}: { }: {
user: User | null, user: User | null,
onCommentSubmitted: ({ commentContent, isPrivate }: { commentContent: string, isPrivate: boolean }) => void, onCommentSubmitted: ({ commentContent, isPrivate, showClientInfo }: { commentContent: string, isPrivate: boolean, showClientInfo: boolean }) => void,
initContent?: string, initContent?: string,
initIsPrivate?: boolean, initIsPrivate?: boolean,
placeholder?: string, placeholder?: string,
@ -33,6 +33,7 @@ export function CommentInput(
const clickToUserProfile = useToUserProfile(); const clickToUserProfile = useToUserProfile();
const [isPrivate, setIsPrivate] = useState(initIsPrivate); const [isPrivate, setIsPrivate] = useState(initIsPrivate);
const [showClientInfo, setShowClientInfo] = useState(true);
const [commentContent, setCommentContent] = useState(initContent); const [commentContent, setCommentContent] = useState(initContent);
const handleCommentSubmit = async () => { const handleCommentSubmit = async () => {
@ -54,7 +55,7 @@ export function CommentInput(
toast.warning(t("comment_unchanged")); toast.warning(t("comment_unchanged"));
return; return;
} }
onCommentSubmitted({ commentContent, isPrivate }); onCommentSubmitted({ commentContent, isPrivate, showClientInfo });
setCommentContent(""); setCommentContent("");
}; };
@ -62,7 +63,7 @@ export function CommentInput(
<div className="fade-in-up"> <div className="fade-in-up">
<div className="flex py-4 fade-in"> <div className="flex py-4 fade-in">
<div onClick={user ? () => clickToUserProfile(user.username) : clickToLogin} className="cursor-pointer flex-shrink-0 w-10 h-10 fade-in"> <div onClick={user ? () => clickToUserProfile(user.username) : clickToLogin} className="cursor-pointer flex-shrink-0 w-10 h-10 fade-in">
{user && <GravatarAvatar className="w-full h-full" url={user.avatarUrl} email={user.email} size={100}/>} {user && <GravatarAvatar className="w-full h-full" url={user.avatarUrl} email={user.email} size={100} />}
{!user && <CircleUser className="w-full h-full fade-in" />} {!user && <CircleUser className="w-full h-full fade-in" />}
</div> </div>
<div className="flex-1 pl-2 fade-in-up"> <div className="flex-1 pl-2 fade-in-up">
@ -75,6 +76,13 @@ export function CommentInput(
</div> </div>
</div> </div>
<div className="flex justify-end fade-in-up space-x-4 items-center"> <div className="flex justify-end fade-in-up space-x-4 items-center">
<div className="flex items-center space-x-2">
<Checkbox
checked={showClientInfo}
onCheckedChange={checked => setShowClientInfo(checked === true)}
/>
<Label onClick={() => setShowClientInfo(prev => !prev)}>{t("show_client_info")}</Label>
</div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Checkbox <Checkbox
checked={isPrivate} checked={isPrivate}

View File

@ -123,13 +123,14 @@ export function CommentItem(
}); });
} }
const onReply = ({ commentContent, isPrivate }: { commentContent: string, isPrivate: boolean }) => { const onReply = ({ commentContent, isPrivate, showClientInfo }: { commentContent: string, isPrivate: boolean, showClientInfo: boolean }) => {
createComment({ createComment({
targetType: comment.targetType, targetType: comment.targetType,
targetId: comment.targetId, targetId: comment.targetId,
content: commentContent, content: commentContent,
replyId: comment.id, replyId: comment.id,
isPrivate, isPrivate,
showClientInfo
}).then(() => { }).then(() => {
toast.success(t("comment_success")); toast.success(t("comment_success"));
reloadReplies(); reloadReplies();

View File

@ -61,18 +61,18 @@ export function CommentSection(
}); });
}, []) }, [])
const onCommentSubmitted = ({ commentContent, isPrivate }: { commentContent: string, isPrivate: boolean }) => { const onCommentSubmitted = ({ commentContent, isPrivate, showClientInfo }: { commentContent: string, isPrivate: boolean, showClientInfo: boolean }) => {
createComment({ createComment({
targetType, targetType,
targetId, targetId,
content: commentContent, content: commentContent,
replyId: null, replyId: null,
isPrivate, isPrivate,
showClientInfo
}).then(res => { }).then(res => {
toast.success(t("comment_success")); toast.success(t("comment_success"));
setTotalCommentCount(c => c + 1); setTotalCommentCount(c => c + 1);
getComment({ id: res.data.id }).then(response => { getComment({ id: res.data.id }).then(response => {
console.log("New comment fetched:", response.data);
setComments(prevComments => [response.data, ...prevComments]); setComments(prevComments => [response.data, ...prevComments]);
}); });
setActiveInput(null); setActiveInput(null);

View File

@ -47,6 +47,7 @@
"private": "私密评论", "private": "私密评论",
"private_placeholder": "悄悄地说一句...", "private_placeholder": "悄悄地说一句...",
"reply": "回复", "reply": "回复",
"show_client_info": "展示客户端信息",
"submit": "提交", "submit": "提交",
"unlike": "取消点赞", "unlike": "取消点赞",
"unlike_success": "已取消点赞", "unlike_success": "已取消点赞",