mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-26 02:56:22 +00:00
✨ feat: 优化评论功能,添加登录提示和国际化支持,重构相关组件
All checks were successful
Push to Helm Chart Repository / build (push) Successful in 10s
All checks were successful
Push to Helm Chart Repository / build (push) Successful in 10s
This commit is contained in:
@ -1,117 +1,117 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/snowykami/neo-blog/internal/dto"
|
"github.com/snowykami/neo-blog/internal/dto"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"resty.dev/v3"
|
"resty.dev/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OidcConfig struct {
|
type OidcConfig struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Name string `gorm:"uniqueIndex"` // OIDC配置名称,唯一
|
Name string `gorm:"uniqueIndex"` // OIDC配置名称,唯一
|
||||||
ClientID string // 客户端ID
|
ClientID string // 客户端ID
|
||||||
ClientSecret string // 客户端密钥
|
ClientSecret string // 客户端密钥
|
||||||
DisplayName string // 显示名称,例如:轻雪通行证
|
DisplayName string // 显示名称,例如:轻雪通行证
|
||||||
Icon string // 图标url,为空则使用内置默认图标
|
Icon string // 图标url,为空则使用内置默认图标
|
||||||
OidcDiscoveryUrl string // OpenID自动发现URL,例如 :https://pass.liteyuki.org/.well-known/openid-configuration
|
OidcDiscoveryUrl string // OpenID自动发现URL,例如 :https://pass.liteyuki.org/.well-known/openid-configuration
|
||||||
Enabled bool `gorm:"default:true"` // 是否启用
|
Enabled bool `gorm:"default:true"` // 是否启用
|
||||||
Type string `gorm:"oauth2"` // OIDC类型,默认为oauth2,也可以为misskey
|
Type string `gorm:"oauth2"` // OIDC类型,默认为oauth2,也可以为misskey
|
||||||
// 以下字段为自动获取字段,每次更新配置时自动填充
|
// 以下字段为自动获取字段,每次更新配置时自动填充
|
||||||
Issuer string
|
Issuer string
|
||||||
AuthorizationEndpoint string
|
AuthorizationEndpoint string
|
||||||
TokenEndpoint string
|
TokenEndpoint string
|
||||||
UserInfoEndpoint string
|
UserInfoEndpoint string
|
||||||
JwksUri string
|
JwksUri string
|
||||||
}
|
}
|
||||||
|
|
||||||
type oidcDiscoveryResp struct {
|
type oidcDiscoveryResp struct {
|
||||||
Issuer string `json:"issuer" validate:"required"`
|
Issuer string `json:"issuer" validate:"required"`
|
||||||
AuthorizationEndpoint string `json:"authorization_endpoint" validate:"required"`
|
AuthorizationEndpoint string `json:"authorization_endpoint" validate:"required"`
|
||||||
TokenEndpoint string `json:"token_endpoint" validate:"required"`
|
TokenEndpoint string `json:"token_endpoint" validate:"required"`
|
||||||
UserInfoEndpoint string `json:"userinfo_endpoint" validate:"required"`
|
UserInfoEndpoint string `json:"userinfo_endpoint" validate:"required"`
|
||||||
JwksUri string `json:"jwks_uri" validate:"required"`
|
JwksUri string `json:"jwks_uri" validate:"required"`
|
||||||
// 可选字段
|
// 可选字段
|
||||||
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
|
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
|
||||||
ScopesSupported []string `json:"scopes_supported,omitempty"`
|
ScopesSupported []string `json:"scopes_supported,omitempty"`
|
||||||
ResponseTypesSupported []string `json:"response_types_supported,omitempty"`
|
ResponseTypesSupported []string `json:"response_types_supported,omitempty"`
|
||||||
GrantTypesSupported []string `json:"grant_types_supported,omitempty"`
|
GrantTypesSupported []string `json:"grant_types_supported,omitempty"`
|
||||||
SubjectTypesSupported []string `json:"subject_types_supported,omitempty"`
|
SubjectTypesSupported []string `json:"subject_types_supported,omitempty"`
|
||||||
IdTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"`
|
IdTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"`
|
||||||
ClaimsSupported []string `json:"claims_supported,omitempty"`
|
ClaimsSupported []string `json:"claims_supported,omitempty"`
|
||||||
EndSessionEndpoint string `json:"end_session_endpoint,omitempty"`
|
EndSessionEndpoint string `json:"end_session_endpoint,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateOidcConfigFromUrl(url string, typ string) (*oidcDiscoveryResp, error) {
|
func updateOidcConfigFromUrl(url string, typ string) (*oidcDiscoveryResp, error) {
|
||||||
client := resty.New()
|
client := resty.New()
|
||||||
client.SetTimeout(10 * time.Second) // 设置超时时间
|
client.SetTimeout(10 * time.Second) // 设置超时时间
|
||||||
var discovery oidcDiscoveryResp
|
var discovery oidcDiscoveryResp
|
||||||
resp, err := client.R().
|
resp, err := client.R().
|
||||||
SetHeader("Accept", "application/json").
|
SetHeader("Accept", "application/json").
|
||||||
SetResult(&discovery).
|
SetResult(&discovery).
|
||||||
Get(url)
|
Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("请求OIDC发现端点失败: %w", err)
|
return nil, fmt.Errorf("请求OIDC发现端点失败: %w", err)
|
||||||
}
|
}
|
||||||
if resp.StatusCode() != 200 {
|
if resp.StatusCode() != 200 {
|
||||||
return nil, fmt.Errorf("请求OIDC发现端点失败,状态码: %d", resp.StatusCode())
|
return nil, fmt.Errorf("请求OIDC发现端点失败,状态码: %d", resp.StatusCode())
|
||||||
}
|
}
|
||||||
// 验证必要字段
|
// 验证必要字段
|
||||||
if typ == "misskey" {
|
if typ == "misskey" {
|
||||||
discovery.UserInfoEndpoint = discovery.Issuer + "/api/users/me" // Misskey的用户信息端点
|
discovery.UserInfoEndpoint = discovery.Issuer + "/api/users/me" // Misskey的用户信息端点
|
||||||
discovery.JwksUri = discovery.Issuer + "/api/jwks"
|
discovery.JwksUri = discovery.Issuer + "/api/jwks"
|
||||||
}
|
}
|
||||||
fmt.Println(discovery)
|
fmt.Println(discovery)
|
||||||
if discovery.Issuer == "" ||
|
if discovery.Issuer == "" ||
|
||||||
discovery.AuthorizationEndpoint == "" ||
|
discovery.AuthorizationEndpoint == "" ||
|
||||||
discovery.TokenEndpoint == "" ||
|
discovery.TokenEndpoint == "" ||
|
||||||
discovery.UserInfoEndpoint == "" ||
|
discovery.UserInfoEndpoint == "" ||
|
||||||
discovery.JwksUri == "" {
|
discovery.JwksUri == "" {
|
||||||
return nil, fmt.Errorf("OIDC发现端点响应缺少必要字段")
|
return nil, fmt.Errorf("OIDC发现端点响应缺少必要字段")
|
||||||
}
|
}
|
||||||
return &discovery, nil
|
return &discovery, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OidcConfig) BeforeSave(tx *gorm.DB) (err error) {
|
func (o *OidcConfig) BeforeSave(tx *gorm.DB) (err error) {
|
||||||
// 只有在创建新记录或更新 OidcDiscoveryUrl 字段时才更新端点信息
|
// 只有在创建新记录或更新 OidcDiscoveryUrl 字段时才更新端点信息
|
||||||
if tx.Statement.Changed("OidcDiscoveryUrl") || o.ID == 0 {
|
if tx.Statement.Changed("OidcDiscoveryUrl") || o.ID == 0 {
|
||||||
logrus.Infof("Updating OIDC config for %s, OidcDiscoveryUrl: %s", o.Name, o.OidcDiscoveryUrl)
|
logrus.Infof("Updating OIDC config for %s, OidcDiscoveryUrl: %s", o.Name, o.OidcDiscoveryUrl)
|
||||||
discoveryResp, err := updateOidcConfigFromUrl(o.OidcDiscoveryUrl, o.Type)
|
discoveryResp, err := updateOidcConfigFromUrl(o.OidcDiscoveryUrl, o.Type)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Error("Updating OIDC config failed: ", err)
|
logrus.Error("Updating OIDC config failed: ", err)
|
||||||
return fmt.Errorf("updating OIDC config failed: %w", err)
|
return fmt.Errorf("updating OIDC config failed: %w", err)
|
||||||
}
|
}
|
||||||
o.Issuer = discoveryResp.Issuer
|
o.Issuer = discoveryResp.Issuer
|
||||||
o.AuthorizationEndpoint = discoveryResp.AuthorizationEndpoint
|
o.AuthorizationEndpoint = discoveryResp.AuthorizationEndpoint
|
||||||
o.TokenEndpoint = discoveryResp.TokenEndpoint
|
o.TokenEndpoint = discoveryResp.TokenEndpoint
|
||||||
o.UserInfoEndpoint = discoveryResp.UserInfoEndpoint
|
o.UserInfoEndpoint = discoveryResp.UserInfoEndpoint
|
||||||
o.JwksUri = discoveryResp.JwksUri
|
o.JwksUri = discoveryResp.JwksUri
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToUserDto 返回给用户侧
|
// ToUserDto 返回给用户侧
|
||||||
func (o *OidcConfig) ToUserDto() *dto.UserOidcConfigDto {
|
func (o *OidcConfig) ToUserDto() *dto.UserOidcConfigDto {
|
||||||
return &dto.UserOidcConfigDto{
|
return &dto.UserOidcConfigDto{
|
||||||
Name: o.Name,
|
Name: o.Name,
|
||||||
DisplayName: o.DisplayName,
|
DisplayName: o.DisplayName,
|
||||||
Icon: o.Icon,
|
Icon: o.Icon,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToAdminDto 返回给管理员侧
|
// ToAdminDto 返回给管理员侧
|
||||||
func (o *OidcConfig) ToAdminDto() *dto.AdminOidcConfigDto {
|
func (o *OidcConfig) ToAdminDto() *dto.AdminOidcConfigDto {
|
||||||
return &dto.AdminOidcConfigDto{
|
return &dto.AdminOidcConfigDto{
|
||||||
ID: o.ID,
|
ID: o.ID,
|
||||||
Name: o.Name,
|
Name: o.Name,
|
||||||
ClientID: o.ClientID,
|
ClientID: o.ClientID,
|
||||||
ClientSecret: o.ClientSecret,
|
ClientSecret: o.ClientSecret,
|
||||||
DisplayName: o.DisplayName,
|
DisplayName: o.DisplayName,
|
||||||
Icon: o.Icon,
|
Icon: o.Icon,
|
||||||
OidcDiscoveryUrl: o.OidcDiscoveryUrl,
|
OidcDiscoveryUrl: o.OidcDiscoveryUrl,
|
||||||
Enabled: o.Enabled,
|
Enabled: o.Enabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -214,9 +214,33 @@ func (cr *CommentRepo) ListComments(currentUserID, targetID, commentID uint, tar
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cr *CommentRepo) CountReplyComments(commentID uint) (int64, error) {
|
func (cr *CommentRepo) CountReplyComments(currentUserID, commentID uint) (int64, error) {
|
||||||
var count int64
|
var count int64
|
||||||
if err := GetDB().Model(&model.Comment{}).Where("reply_id = ?", commentID).Count(&count).Error; err != nil {
|
var masterID uint
|
||||||
|
|
||||||
|
// 根据commentID查询所属对象的用户ID
|
||||||
|
comment, err := cr.GetComment(strconv.Itoa(int(commentID)))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if comment.TargetType == constant.TargetTypePost {
|
||||||
|
post, err := Post.GetPostByID(strconv.Itoa(int(comment.TargetID)))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
masterID = post.UserID
|
||||||
|
} else {
|
||||||
|
// 如果不是文章类型,可以根据需要添加其他类型的处理逻辑
|
||||||
|
return 0, errs.New(http.StatusBadRequest, "unsupported target type for counting replies", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := GetDB().Model(&model.Comment{}).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 err := query.Count(&count).Error; err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return count, nil
|
return count, nil
|
||||||
|
@ -1,177 +1,177 @@
|
|||||||
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/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) error {
|
func (cs *CommentService) CreateComment(ctx context.Context, req *dto.CreateCommentReq) error {
|
||||||
currentUser, ok := ctxutils.GetCurrentUser(ctx)
|
currentUser, ok := ctxutils.GetCurrentUser(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errs.ErrUnauthorized
|
return 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 errs.New(errs.ErrBadRequest.Code, "target not found", err)
|
return errs.New(errs.ErrBadRequest.Code, "target not found", err)
|
||||||
}
|
}
|
||||||
return errs.ErrBadRequest
|
return 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := repo.Comment.CreateComment(comment)
|
err := repo.Comment.CreateComment(comment)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return 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{
|
commentDto := dto.CommentDto{
|
||||||
ID: comment.ID,
|
ID: comment.ID,
|
||||||
TargetID: comment.TargetID,
|
TargetID: comment.TargetID,
|
||||||
TargetType: comment.TargetType,
|
TargetType: comment.TargetType,
|
||||||
Content: comment.Content,
|
Content: comment.Content,
|
||||||
ReplyID: comment.ReplyID,
|
ReplyID: comment.ReplyID,
|
||||||
Depth: comment.Depth,
|
Depth: comment.Depth,
|
||||||
CreatedAt: comment.CreatedAt.String(),
|
CreatedAt: comment.CreatedAt.String(),
|
||||||
UpdatedAt: comment.UpdatedAt.String(),
|
UpdatedAt: comment.UpdatedAt.String(),
|
||||||
User: comment.User.ToDto(),
|
User: comment.User.ToDto(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return &commentDto, err
|
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)
|
commentDtos := make([]dto.CommentDto, 0)
|
||||||
|
|
||||||
for _, comment := range comments {
|
for _, comment := range comments {
|
||||||
replyCount, _ := repo.Comment.CountReplyComments(comment.ID)
|
replyCount, _ := repo.Comment.CountReplyComments(currentUserID, comment.ID)
|
||||||
isLiked := false
|
isLiked := false
|
||||||
if currentUserID != 0 {
|
if currentUserID != 0 {
|
||||||
isLiked, _ = repo.Like.IsLiked(currentUserID, comment.ID, constant.TargetTypeComment)
|
isLiked, _ = repo.Like.IsLiked(currentUserID, comment.ID, constant.TargetTypeComment)
|
||||||
}
|
}
|
||||||
|
|
||||||
commentDto := dto.CommentDto{
|
commentDto := dto.CommentDto{
|
||||||
ID: comment.ID,
|
ID: comment.ID,
|
||||||
Content: comment.Content,
|
Content: comment.Content,
|
||||||
TargetID: comment.TargetID,
|
TargetID: comment.TargetID,
|
||||||
TargetType: comment.TargetType,
|
TargetType: comment.TargetType,
|
||||||
ReplyID: comment.ReplyID,
|
ReplyID: comment.ReplyID,
|
||||||
CreatedAt: comment.CreatedAt.String(),
|
CreatedAt: comment.CreatedAt.String(),
|
||||||
UpdatedAt: comment.UpdatedAt.String(),
|
UpdatedAt: comment.UpdatedAt.String(),
|
||||||
Depth: comment.Depth,
|
Depth: comment.Depth,
|
||||||
User: comment.User.ToDto(),
|
User: comment.User.ToDto(),
|
||||||
ReplyCount: replyCount,
|
ReplyCount: replyCount,
|
||||||
LikeCount: comment.LikeCount,
|
LikeCount: comment.LikeCount,
|
||||||
IsLiked: isLiked,
|
IsLiked: isLiked,
|
||||||
IsPrivate: comment.IsPrivate,
|
IsPrivate: comment.IsPrivate,
|
||||||
}
|
}
|
||||||
commentDtos = append(commentDtos, commentDto)
|
commentDtos = append(commentDtos, commentDto)
|
||||||
}
|
}
|
||||||
return commentDtos, nil
|
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
|
||||||
}
|
}
|
||||||
|
@ -27,15 +27,22 @@ export function CommentInput(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const t = useTranslations('Comment')
|
const t = useTranslations('Comment')
|
||||||
const handleToLogin = useToLogin()
|
const commonT = useTranslations('Common')
|
||||||
const toUserProfile = useToUserProfile();
|
const clickToLogin = useToLogin()
|
||||||
|
const clickToUserProfile = useToUserProfile();
|
||||||
|
|
||||||
const [isPrivate, setIsPrivate] = useState(initIsPrivate);
|
const [isPrivate, setIsPrivate] = useState(initIsPrivate);
|
||||||
const [commentContent, setCommentContent] = useState(initContent);
|
const [commentContent, setCommentContent] = useState(initContent);
|
||||||
|
|
||||||
const handleCommentSubmit = async () => {
|
const handleCommentSubmit = async () => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
toast.error(<NeedLogin>{t("login_required")}</NeedLogin>);
|
// 通知
|
||||||
|
toast.error(t("login_required"), {
|
||||||
|
action: {
|
||||||
|
label: commonT("login"),
|
||||||
|
onClick: clickToLogin,
|
||||||
|
},
|
||||||
|
})
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!commentContent.trim()) {
|
if (!commentContent.trim()) {
|
||||||
@ -49,13 +56,13 @@ export function CommentInput(
|
|||||||
return (
|
return (
|
||||||
<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 ? () => toUserProfile(user.username) : handleToLogin} className="flex-shrink-0 w-10 h-10 fade-in">
|
<div onClick={user ? () => clickToUserProfile(user.username) : clickToLogin} className="flex-shrink-0 w-10 h-10 fade-in">
|
||||||
{user ? getGravatarByUser(user) : null}
|
{user ? getGravatarByUser(user) : null}
|
||||||
{!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">
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder={user?t("placeholder"):t("login_required")}
|
placeholder={user ? t("placeholder") : t("login_required", { loginButton: "登录" })}
|
||||||
className="w-full p-2 border border-gray-300 rounded-md fade-in-up"
|
className="w-full p-2 border border-gray-300 rounded-md fade-in-up"
|
||||||
value={commentContent}
|
value={commentContent}
|
||||||
onChange={(e) => setCommentContent(e.target.value)}
|
onChange={(e) => setCommentContent(e.target.value)}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useToUserProfile } from "@/hooks/use-route";
|
import { useToLogin, useToUserProfile } from "@/hooks/use-route";
|
||||||
import { User } from "@/models/user";
|
import { User } from "@/models/user";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@ -29,7 +29,9 @@ export function CommentItem(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const t = useTranslations("Comment")
|
const t = useTranslations("Comment")
|
||||||
const toUserProfile = useToUserProfile();
|
const commonT = useTranslations('Common')
|
||||||
|
const clickToUserProfile = useToUserProfile();
|
||||||
|
const clickToLogin = useToLogin();
|
||||||
const { confirming, onClick, onBlur } = useDoubleConfirm();
|
const { confirming, onClick, onBlur } = useDoubleConfirm();
|
||||||
|
|
||||||
const [likeCount, setLikeCount] = useState(comment.likeCount);
|
const [likeCount, setLikeCount] = useState(comment.likeCount);
|
||||||
@ -43,6 +45,19 @@ export function CommentItem(
|
|||||||
const [showEditInput, setShowEditInput] = useState(false);
|
const [showEditInput, setShowEditInput] = useState(false);
|
||||||
|
|
||||||
const handleToggleLike = () => {
|
const handleToggleLike = () => {
|
||||||
|
if (!user) {
|
||||||
|
toast.error(t("login_required"), {
|
||||||
|
action: <div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={clickToLogin}
|
||||||
|
className="ml-0 text-left bg-red-400 text-white dark:text-black px-3 py-1 rounded font-semibold hover:bg-red-600 transition-colors"
|
||||||
|
>
|
||||||
|
{commonT("login")}
|
||||||
|
</button>
|
||||||
|
</div>,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
toggleLike(
|
toggleLike(
|
||||||
{ targetType: TargetType.Comment, targetId: comment.id }
|
{ targetType: TargetType.Comment, targetId: comment.id }
|
||||||
).then(res => {
|
).then(res => {
|
||||||
@ -133,7 +148,7 @@ export function CommentItem(
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
parentComment &&
|
parentComment &&
|
||||||
<>{t("reply")} <button onClick={() => toUserProfile(parentComment.user.nickname)} className="text-primary">{parentComment?.user.nickname}</button>: </>
|
<>{t("reply")} <button onClick={() => clickToUserProfile(parentComment.user.nickname)} className="text-primary">{parentComment?.user.nickname}</button>: </>
|
||||||
}
|
}
|
||||||
{comment.content}
|
{comment.content}
|
||||||
</p>
|
</p>
|
||||||
|
0
web/src/components/login/login-require.tsx
Normal file
0
web/src/components/login/login-require.tsx
Normal file
@ -19,7 +19,7 @@
|
|||||||
"like": "点赞",
|
"like": "点赞",
|
||||||
"like_failed": "点赞失败",
|
"like_failed": "点赞失败",
|
||||||
"like_success": "点赞成功",
|
"like_success": "点赞成功",
|
||||||
"login_required": "请先登录后再评论。",
|
"login_required": "请先登录后再操作",
|
||||||
"placeholder": "写你的评论...",
|
"placeholder": "写你的评论...",
|
||||||
"private": "私密评论",
|
"private": "私密评论",
|
||||||
"reply": "回复",
|
"reply": "回复",
|
||||||
@ -28,6 +28,9 @@
|
|||||||
"unlike_success": "已取消点赞",
|
"unlike_success": "已取消点赞",
|
||||||
"update": "更新"
|
"update": "更新"
|
||||||
},
|
},
|
||||||
|
"Common":{
|
||||||
|
"login": "登录"
|
||||||
|
},
|
||||||
"Login": {
|
"Login": {
|
||||||
"welcome": "欢迎回来",
|
"welcome": "欢迎回来",
|
||||||
"with_oidc": "使用第三方身份提供者",
|
"with_oidc": "使用第三方身份提供者",
|
||||||
|
Reference in New Issue
Block a user