From dd7641bf6e0b7780c8345f90f76ce8272261b8a5 Mon Sep 17 00:00:00 2001 From: Snowykami Date: Tue, 9 Sep 2025 00:24:25 +0800 Subject: [PATCH] feat: implement advanced comment features including reply and like functionality - Added support for nested comments with reply functionality. - Implemented like/unlike feature for comments and posts. - Enhanced comment DTO to include reply count, like count, and like status. - Updated comment and like services to handle new functionalities. - Created new API endpoints for toggling likes and listing comments. - Improved UI components for comments to support replies and likes with animations. - Added localization for new comment-related messages. - Introduced a TODO list for future enhancements in the comment module. --- README.md | 13 +- TODO.md | 12 ++ internal/controller/v1/comment.go | 11 ++ internal/controller/v1/like.go | 30 +++- internal/controller/v1/user.go | 3 +- internal/dto/comment.go | 10 +- internal/dto/like.go | 5 + internal/model/like.go | 5 +- internal/repo/comment.go | 24 ++- internal/repo/like.go | 22 ++- internal/repo/post.go | 15 +- internal/service/comment.go | 12 +- internal/service/like.go | 22 +++ internal/service/post.go | 22 +-- pkg/constant/constant.go | 14 +- pkg/utils/env.go | 5 +- web/src/api/comment.ts | 5 +- web/src/api/like.ts | 11 ++ web/src/app/globals.css | 4 + .../components/comment/comment-animations.css | 47 +++++ web/src/components/comment/comment-input.tsx | 24 ++- web/src/components/comment/comment-item.tsx | 163 +++++++++++++++--- web/src/components/comment/index.tsx | 19 +- web/src/components/login/login-form.tsx | 3 +- web/src/locales/zh-CN.json | 13 +- web/src/models/comment.ts | 7 +- web/src/models/like.tsx | 0 web/src/models/types.ts | 2 + 28 files changed, 422 insertions(+), 101 deletions(-) create mode 100644 TODO.md create mode 100644 web/src/api/like.ts create mode 100644 web/src/components/comment/comment-animations.css create mode 100644 web/src/models/like.tsx diff --git a/README.md b/README.md index c9bb942..56945a0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ - [ ] 支持文章置顶 - [x] OIDC认证和注册 - [x] 支持多用户 -- [ ] 高级评论功能(后端已实装) +- [x] 高级评论功能(后端已实装) - [ ] 支持多语言 - [x] 移动端适配 - [ ] 后台管理 @@ -24,7 +24,7 @@ services: frontend: container_name: neo-blog-frontend environment: - - BACKEND_URL=http://neo-blog-backend:8888 # 此处请保证和后端服务的名称一致 + - BACKEND_URL=http://neo-blog-backend:8888 # 此处的后端地址用于前端服务器访问后端服务 image: snowykami/neo-blog-frontend:latest networks: - internal-network @@ -38,7 +38,7 @@ services: container_name: neo-blog-backend image: snowykami/neo-blog-backend:latest environment: - - BASE_URL=https://neo-blog-dev.sfkm.me # 此处是外部用户访问端点,也许你使用了nginx等反向代理 + - BASE_URL=https://neo-blog-dev.sfkm.me # 此处是外部用户访问端点,用于在某些情况下后端可以生成正确的URL networks: - internal-network restart: always @@ -72,7 +72,7 @@ helm repo update helm install neo-blog git.liteyuki.org/neo-blog ``` -### 使用源码构建部署(除开发场景外不推荐) +### 使用源码构建部署(不推荐) 需要准备:go、nodejs、pnpm @@ -132,5 +132,10 @@ pnpm install pnpm dev ``` +### 联合调试 + +默认情况下,本机启动后端和前端服务器无须额外配置即可互联,若后端在不同的主机上,需要在.env.development(自己创建)中配置`BACKEND_URL`变量 + ## 环境变量配置 + 后端所有环境变量及其示例在[`.env.example`](./.env.example)文件中 \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..5f92b28 --- /dev/null +++ b/TODO.md @@ -0,0 +1,12 @@ +# TODO List + +## 主页模块 + +- [ ]主页文章列表 +- [ ]主页侧边栏卡片列表 + +## 评论模块 + +- [ ]评论列表 +- [ ]评论输入框 +- [ ]评论回复功能 \ No newline at end of file diff --git a/internal/controller/v1/comment.go b/internal/controller/v1/comment.go index b7caecf..b4eba19 100644 --- a/internal/controller/v1/comment.go +++ b/internal/controller/v1/comment.go @@ -107,6 +107,16 @@ func (cc *CommentController) GetCommentList(ctx context.Context, c *app.RequestC resps.BadRequest(c, "无效的 target_id") return } + commentIDStr := c.Query("comment_id") + var commentID uint + if commentIDStr != "" { + commentIDInt, err := strconv.Atoi(commentIDStr) + if err != nil { + resps.BadRequest(c, "无效的 comment_id") + return + } + commentID = uint(commentIDInt) + } req := dto.GetCommentListReq{ Desc: pagination.Desc, OrderBy: pagination.OrderBy, @@ -115,6 +125,7 @@ func (cc *CommentController) GetCommentList(ctx context.Context, c *app.RequestC Depth: depthInt, TargetID: uint(targetID), TargetType: c.Query("target_type"), + CommentID: commentID, } resp, err := cc.service.GetCommentList(ctx, &req) if err != nil { diff --git a/internal/controller/v1/like.go b/internal/controller/v1/like.go index 4822a6f..d67ddd3 100644 --- a/internal/controller/v1/like.go +++ b/internal/controller/v1/like.go @@ -2,15 +2,39 @@ package v1 import ( "context" + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/common/utils" + "github.com/sirupsen/logrus" + "github.com/snowykami/neo-blog/internal/dto" + "github.com/snowykami/neo-blog/internal/service" + "github.com/snowykami/neo-blog/pkg/errs" + "github.com/snowykami/neo-blog/pkg/resps" ) -type LikeController struct{} +type LikeController struct { + service *service.LikeService +} func NewLikeController() *LikeController { - return &LikeController{} + return &LikeController{ + service: service.NewLikeService(), + } } func (lc *LikeController) ToggleLike(ctx context.Context, c *app.RequestContext) { - // Implementation for creating a like + var toggleLikeReq dto.ToggleLikeReq + if err := c.BindAndValidate(&toggleLikeReq); err != nil { + logrus.Error(err) + resps.BadRequest(c, resps.ErrParamInvalid) + return + } + liked, err := lc.service.ToggleLike(ctx, toggleLikeReq.TargetID, toggleLikeReq.TargetType) + if err != nil { + serviceErr := errs.AsServiceError(err) + logrus.Error(serviceErr.Error()) + resps.Custom(c, serviceErr.Code, serviceErr.Message, nil) + return + } + resps.Ok(c, resps.Success, utils.H{"status": liked}) } diff --git a/internal/controller/v1/user.go b/internal/controller/v1/user.go index cde5e6a..60a79f7 100644 --- a/internal/controller/v1/user.go +++ b/internal/controller/v1/user.go @@ -3,6 +3,8 @@ package v1 import ( "context" "fmt" + "strconv" + "github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/common/utils" "github.com/snowykami/neo-blog/internal/ctxutils" @@ -10,7 +12,6 @@ import ( "github.com/snowykami/neo-blog/internal/service" "github.com/snowykami/neo-blog/pkg/errs" "github.com/snowykami/neo-blog/pkg/resps" - "strconv" ) type UserController struct { diff --git a/internal/dto/comment.go b/internal/dto/comment.go index 6d6fbb8..a7ccc79 100644 --- a/internal/dto/comment.go +++ b/internal/dto/comment.go @@ -9,7 +9,10 @@ type CommentDto struct { Depth int `json:"depth"` // 评论的层级深度 CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` - User UserDto `json:"user"` // 评论的 + User UserDto `json:"user"` // 评论的 + ReplyCount int64 `json:"reply_count"` // 回复数量 + LikeCount uint64 `json:"like_count"` // 点赞数量 + IsLiked bool `json:"is_liked"` // 当前用户是否点赞 } type CreateCommentReq struct { @@ -29,8 +32,9 @@ type UpdateCommentReq struct { type GetCommentListReq struct { TargetID uint `json:"target_id" binding:"required"` TargetType string `json:"target_type" binding:"required"` - OrderBy string `json:"order_by"` // 排序方式 - Page uint64 `json:"page"` // 页码 + CommentID uint `json:"comment_id"` // 获取某条评论的所有子评论 + OrderBy string `json:"order_by"` // 排序方式 + Page uint64 `json:"page"` // 页码 Size uint64 `json:"size"` Desc bool `json:"desc"` Depth int `json:"depth"` // 评论的层级深度 diff --git a/internal/dto/like.go b/internal/dto/like.go index 76d3a17..13664d3 100644 --- a/internal/dto/like.go +++ b/internal/dto/like.go @@ -1 +1,6 @@ package dto + +type ToggleLikeReq struct { + TargetID uint `json:"target_id" binding:"required"` + TargetType string `json:"target_type" binding:"required"` // 目标类型,如 "post", "comment" +} diff --git a/internal/model/like.go b/internal/model/like.go index 1983818..6947dae 100644 --- a/internal/model/like.go +++ b/internal/model/like.go @@ -2,6 +2,7 @@ package model import ( "fmt" + "github.com/snowykami/neo-blog/pkg/constant" "gorm.io/gorm" ) @@ -32,10 +33,10 @@ func (l *Like) AfterDelete(tx *gorm.DB) (err error) { switch l.TargetType { case constant.TargetTypePost: return tx.Model(&Post{}).Where("id = ?", l.TargetID). - UpdateColumn("like_count", gorm.Expr("GREATEST(like_count - ?, 0)", 1)).Error + UpdateColumn("like_count", gorm.Expr("like_count - ?", 1)).Error case constant.TargetTypeComment: return tx.Model(&Comment{}).Where("id = ?", l.TargetID). - UpdateColumn("like_count", gorm.Expr("GREATEST(like_count - ?, 0)", 1)).Error + UpdateColumn("like_count", gorm.Expr("like_count - ?", 1)).Error default: return fmt.Errorf("不支持的目标类型: %s", l.TargetType) } diff --git a/internal/repo/comment.go b/internal/repo/comment.go index 04b56ec..c6dfc82 100644 --- a/internal/repo/comment.go +++ b/internal/repo/comment.go @@ -9,6 +9,7 @@ import ( "github.com/snowykami/neo-blog/internal/model" "github.com/snowykami/neo-blog/pkg/constant" "github.com/snowykami/neo-blog/pkg/errs" + "github.com/snowykami/neo-blog/pkg/utils" "gorm.io/gorm" ) @@ -92,16 +93,15 @@ func (cr *CommentRepo) CreateComment(comment *model.Comment) error { } depth = parentComment.Depth + 1 } - + if depth > utils.Env.GetAsInt(constant.EnvKeyMaxReplyDepth, constant.MaxReplyDepthDefault) { + return errs.New(http.StatusBadRequest, "exceeded maximum reply depth", nil) + } comment.Depth = depth - if err := tx.Create(comment).Error; err != nil { return err } - return nil }) - return err } @@ -172,7 +172,7 @@ func (cr *CommentRepo) GetComment(commentID string) (*model.Comment, error) { return &comment, nil } -func (cr *CommentRepo) ListComments(currentUserID uint, targetID uint, targetType string, page, size uint64, orderBy string, desc bool, depth int) ([]model.Comment, error) { +func (cr *CommentRepo) ListComments(currentUserID, targetID, commentID uint, targetType string, page, size uint64, orderBy string, desc bool, depth int) ([]model.Comment, error) { if !slices.Contains(constant.OrderByEnumComment, orderBy) { return nil, errs.New(http.StatusBadRequest, "invalid order_by parameter", nil) } @@ -189,13 +189,17 @@ func (cr *CommentRepo) ListComments(currentUserID uint, targetID uint, targetTyp query := GetDB().Model(&model.Comment{}).Preload("User") + if commentID > 0 { + query = query.Where("reply_id = ?", commentID) + } + if currentUserID > 0 { query = query.Where("(is_private = ? OR (is_private = ? AND (user_id = ? OR user_id = ?)))", false, true, currentUserID, masterID) } else { query = query.Where("is_private = ?", false) } - if depth > 0 { + if depth >= 0 { query = query.Where("target_id = ? AND target_type = ? AND depth = ?", targetID, targetType, depth) } else { query = query.Where("target_id = ? AND target_type = ?", targetID, targetType) @@ -208,3 +212,11 @@ func (cr *CommentRepo) ListComments(currentUserID uint, targetID uint, targetTyp return items, nil } + +func (cr *CommentRepo) CountReplyComments(commentID uint) (int64, error) { + var count int64 + if err := GetDB().Model(&model.Comment{}).Where("reply_id = ?", commentID).Count(&count).Error; err != nil { + return 0, err + } + return count, nil +} diff --git a/internal/repo/like.go b/internal/repo/like.go index 4ced224..91ce0bc 100644 --- a/internal/repo/like.go +++ b/internal/repo/like.go @@ -2,6 +2,8 @@ package repo import ( "errors" + + "github.com/sirupsen/logrus" "github.com/snowykami/neo-blog/internal/model" "github.com/snowykami/neo-blog/pkg/constant" "gorm.io/gorm" @@ -11,24 +13,28 @@ type likeRepo struct{} var Like = &likeRepo{} -func (l *likeRepo) ToggleLike(userID, targetID uint, targetType string) error { +func (l *likeRepo) ToggleLike(userID, targetID uint, targetType string) (bool, error) { err := l.checkTargetType(targetType) if err != nil { - return err + return false, err } - return GetDB().Transaction(func(tx *gorm.DB) error { - // 判断是否已点赞 + var finalStatus bool + err = GetDB().Transaction(func(tx *gorm.DB) error { isLiked, err := l.IsLiked(userID, targetID, targetType) if err != nil { + logrus.Error(err) return err } if isLiked { - // 已点赞,执行取消点赞逻辑 - if err := tx.Where("target_type = ? AND target_id = ? AND user_id = ?", targetType, targetID, userID).Delete(&model.Like{}).Error; err != nil { + if err := + tx.Where("target_type = ? AND target_id = ? AND user_id = ?", targetType, targetID, userID). + Delete(&model.Like{TargetType: targetType, TargetID: targetID, UserID: userID}). + Error; err != nil { + logrus.Error(err) return err } + finalStatus = false } else { - // 未点赞,执行新增点赞逻辑 like := &model.Like{ TargetType: targetType, TargetID: targetID, @@ -37,6 +43,7 @@ func (l *likeRepo) ToggleLike(userID, targetID uint, targetType string) error { if err := tx.Create(like).Error; err != nil { return err } + finalStatus = true } // 重新计算点赞数量 var count int64 @@ -58,6 +65,7 @@ func (l *likeRepo) ToggleLike(userID, targetID uint, targetType string) error { } return nil }) + return finalStatus, err } // IsLiked 检查是否点赞 diff --git a/internal/repo/post.go b/internal/repo/post.go index 3e6b27f..76327fe 100644 --- a/internal/repo/post.go +++ b/internal/repo/post.go @@ -1,11 +1,12 @@ package repo import ( + "net/http" + "slices" + "github.com/snowykami/neo-blog/internal/model" "github.com/snowykami/neo-blog/pkg/constant" "github.com/snowykami/neo-blog/pkg/errs" - "net/http" - "slices" ) type postRepo struct{} @@ -73,13 +74,13 @@ func (p *postRepo) ListPosts(currentUserID uint, keywords []string, page, size u return items, nil } -func (p *postRepo) ToggleLikePost(postID uint, userID uint) error { +func (p *postRepo) ToggleLikePost(postID uint, userID uint) (bool, error) { if postID == 0 || userID == 0 { - return errs.New(http.StatusBadRequest, "invalid post ID or user ID", nil) + return false, errs.New(http.StatusBadRequest, "invalid post ID or user ID", nil) } - err := Like.ToggleLike(userID, postID, constant.TargetTypePost) + liked, err := Like.ToggleLike(userID, postID, constant.TargetTypePost) if err != nil { - return err + return false, err } - return nil + return liked, nil } diff --git a/internal/service/comment.go b/internal/service/comment.go index e8c3e61..5bfb864 100644 --- a/internal/service/comment.go +++ b/internal/service/comment.go @@ -130,7 +130,7 @@ func (cs *CommentService) GetCommentList(ctx context.Context, req *dto.GetCommen currentUserID = currentUser.ID } - comments, err := repo.Comment.ListComments(currentUserID, req.TargetID, 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 { return nil, errs.New(errs.ErrInternalServer.Code, "failed to list comments", err) } @@ -138,15 +138,25 @@ func (cs *CommentService) GetCommentList(ctx context.Context, req *dto.GetCommen commentDtos := make([]dto.CommentDto, 0) for _, comment := range comments { + replyCount, _ := repo.Comment.CountReplyComments(comment.ID) + isLiked := false + if currentUserID != 0 { + isLiked, _ = repo.Like.IsLiked(currentUserID, comment.ID, constant.TargetTypeComment) + } + 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: replyCount, + LikeCount: comment.LikeCount, + IsLiked: isLiked, } commentDtos = append(commentDtos, commentDto) } diff --git a/internal/service/like.go b/internal/service/like.go index 6d43c33..1603464 100644 --- a/internal/service/like.go +++ b/internal/service/like.go @@ -1 +1,23 @@ package service + +import ( + "context" + + "github.com/snowykami/neo-blog/internal/ctxutils" + "github.com/snowykami/neo-blog/internal/repo" + "github.com/snowykami/neo-blog/pkg/errs" +) + +type LikeService struct{} + +func NewLikeService() *LikeService { + return &LikeService{} +} + +func (ls *LikeService) ToggleLike(ctx context.Context, targetID uint, targetType string) (bool, error) { + currentUser, ok := ctxutils.GetCurrentUser(ctx) + if !ok { + return false, errs.ErrUnauthorized + } + return repo.Like.ToggleLike(currentUser.ID, targetID, targetType) +} diff --git a/internal/service/post.go b/internal/service/post.go index 44465fb..2e272da 100644 --- a/internal/service/post.go +++ b/internal/service/post.go @@ -2,12 +2,13 @@ package service import ( "context" + "strconv" + "github.com/snowykami/neo-blog/internal/ctxutils" "github.com/snowykami/neo-blog/internal/dto" "github.com/snowykami/neo-blog/internal/model" "github.com/snowykami/neo-blog/internal/repo" "github.com/snowykami/neo-blog/pkg/errs" - "strconv" ) type PostService struct{} @@ -126,27 +127,28 @@ func (p *PostService) ListPosts(ctx context.Context, req *dto.ListPostReq) ([]*d return postDtos, nil } -func (p *PostService) ToggleLikePost(ctx context.Context, id string) error { +func (p *PostService) ToggleLikePost(ctx context.Context, id string) (bool, error) { currentUser, ok := ctxutils.GetCurrentUser(ctx) if !ok { - return errs.ErrUnauthorized + return false, errs.ErrUnauthorized } if id == "" { - return errs.ErrBadRequest + return false, errs.ErrBadRequest } post, err := repo.Post.GetPostByID(id) if err != nil { - return errs.New(errs.ErrNotFound.Code, "post not found", err) + return false, errs.New(errs.ErrNotFound.Code, "post not found", err) } if post.UserID == currentUser.ID { - return errs.ErrForbidden + return false, errs.ErrForbidden } idInt, err := strconv.ParseUint(id, 10, 64) if err != nil { - return errs.New(errs.ErrBadRequest.Code, "invalid post ID", err) + return false, errs.New(errs.ErrBadRequest.Code, "invalid post ID", err) } - if err := repo.Post.ToggleLikePost(uint(idInt), currentUser.ID); err != nil { - return errs.ErrInternalServer + liked, err := repo.Post.ToggleLikePost(uint(idInt), currentUser.ID) + if err != nil { + return false, errs.ErrInternalServer } - return nil + return liked, nil } diff --git a/pkg/constant/constant.go b/pkg/constant/constant.go index e49e665..e9086ca 100644 --- a/pkg/constant/constant.go +++ b/pkg/constant/constant.go @@ -10,12 +10,13 @@ const ( ModeProd = "prod" RoleUser = "user" RoleAdmin = "admin" - EnvKeyBaseUrl = "BASE_URL" // 环境变量:基础URL - EnvKeyLogLevel = "LOG_LEVEL" // 环境变量:日志级别 - EnvKeyMode = "MODE" // 环境变量:运行模式 - EnvKeyJwtSecrete = "JWT_SECRET" // 环境变量:JWT密钥 - EnvKeyPasswordSalt = "PASSWORD_SALT" // 环境变量:密码盐 - EnvKeyTokenDuration = "TOKEN_DURATION" // 环境变量:令牌有效期 + EnvKeyBaseUrl = "BASE_URL" // 环境变量:基础URL + EnvKeyLogLevel = "LOG_LEVEL" // 环境变量:日志级别 + EnvKeyMode = "MODE" // 环境变量:运行模式 + EnvKeyJwtSecrete = "JWT_SECRET" // 环境变量:JWT密钥 + EnvKeyPasswordSalt = "PASSWORD_SALT" // 环境变量:密码盐 + EnvKeyTokenDuration = "TOKEN_DURATION" // 环境变量:令牌有效期 + EnvKeyMaxReplyDepth = "MAX_REPLY_DEPTH" // 环境变量:最大回复深度 EnvKeyTokenDurationDefault = 300 EnvKeyRefreshTokenDurationDefault = 604800 EnvKeyRefreshTokenDuration = "REFRESH_TOKEN_DURATION" // 环境变量:刷新令牌有效期 @@ -35,6 +36,7 @@ const ( OrderByCommentCount = "comment_count" // 按评论数排序 OrderByViewCount = "view_count" // 按浏览量排序 OrderByHeat = "heat" + MaxReplyDepthDefault = 3 // 默认最大回复深度 HeatFactorViewWeight = 1 // 热度因子:浏览量权重 HeatFactorLikeWeight = 5 // 热度因子:点赞权重 HeatFactorCommentWeight = 10 // 热度因子:评论权重 diff --git a/pkg/utils/env.go b/pkg/utils/env.go index d69abf7..8a0081f 100644 --- a/pkg/utils/env.go +++ b/pkg/utils/env.go @@ -2,11 +2,12 @@ package utils import ( "fmt" + "os" + "strconv" + "github.com/joho/godotenv" "github.com/sirupsen/logrus" "github.com/snowykami/neo-blog/pkg/constant" - "os" - "strconv" ) var ( diff --git a/web/src/api/comment.ts b/web/src/api/comment.ts index a0e9a87..91e78b9 100644 --- a/web/src/api/comment.ts +++ b/web/src/api/comment.ts @@ -32,6 +32,7 @@ export interface ListCommentsParams { desc?: boolean page?: number size?: number + commentId?: number } export async function listComments(params: ListCommentsParams): Promise> { @@ -43,6 +44,7 @@ export async function listComments(params: ListCommentsParams): Promise>(`/comment/list`, { params: { @@ -52,7 +54,8 @@ export async function listComments(params: ListCommentsParams): Promise> { + const res = await axiosClient.put>('/like/toggle', { targetType, targetId }) + return res.data +} \ No newline at end of file diff --git a/web/src/app/globals.css b/web/src/app/globals.css index ae9755e..32b7048 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -112,6 +112,10 @@ --sidebar-ring: oklch(0.488 0.243 264.376); } +:root { + --animation-duration: 0.6s; +} + @layer base { * { @apply border-border outline-ring/50; diff --git a/web/src/components/comment/comment-animations.css b/web/src/components/comment/comment-animations.css new file mode 100644 index 0000000..6a84174 --- /dev/null +++ b/web/src/components/comment/comment-animations.css @@ -0,0 +1,47 @@ +/* 评论区原生动画:淡入、上移 */ +.fade-in { + opacity: 0; + animation: fadeIn 0.5s ease forwards; +} + +.fade-in-up { + opacity: 0; + transform: translateY(16px); + animation: fadeInUp 0.5s cubic-bezier(.33,1,.68,1) forwards; +} + +@keyframes fadeIn { + to { + opacity: 1; + } +} + +@keyframes fadeInUp { + to { + opacity: 1; + transform: translateY(0); + } +}/* 评论区原生动画:淡入、上移 */ +.fade-in { + opacity: 0; + animation: fadeIn 0.5s ease forwards; +} + +.fade-in-up { + opacity: 0; + transform: translateY(16px); + animation: fadeInUp 0.5s cubic-bezier(.33,1,.68,1) forwards; +} + +@keyframes fadeIn { + to { + opacity: 1; + } +} + +@keyframes fadeInUp { + to { + opacity: 1; + transform: translateY(0); + } +} \ No newline at end of file diff --git a/web/src/components/comment/comment-input.tsx b/web/src/components/comment/comment-input.tsx index 8b657a1..fba7f13 100644 --- a/web/src/components/comment/comment-input.tsx +++ b/web/src/components/comment/comment-input.tsx @@ -13,9 +13,13 @@ import { TargetType } from "@/models/types"; import { useToLogin } from "@/hooks/use-to-login"; import NeedLogin from "../common/need-login"; + +import "./comment-animations.css"; + export function CommentInput( - { targetId, targetType, onCommentSubmitted }: { targetId: number, targetType: TargetType, onCommentSubmitted: () => void } + { targetId, targetType, replyId, onCommentSubmitted }: { targetId: number, targetType: TargetType, replyId: number | null, onCommentSubmitted: () => void } ) { + const t = useTranslations('Comment') const toLogin = useToLogin() const [user, setUser] = useState(null); @@ -41,6 +45,8 @@ export function CommentInput( targetType: targetType, targetId: targetId, content: commentContent, + replyId: replyId, + isPrivate: false, }).then(response => { setCommentContent(""); toast.success(t("comment_success")); @@ -52,25 +58,25 @@ export function CommentInput( }); }; return ( -
-
+
+
{/* Avatar */} -
+
{user && getGravatarByUser(user)} - {!user && } + {!user && }
{/* Input Area */} -
+