mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-26 11:06:23 +00:00
feat: 添加评论功能的客户端信息显示选项,更新相关接口和组件
This commit is contained in:
@ -17,6 +17,7 @@ type CommentDto struct {
|
|||||||
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
|
||||||
|
ShowClientInfo bool `json:"show_client_info"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateCommentReq struct {
|
type CreateCommentReq struct {
|
||||||
@ -34,6 +35,7 @@ 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"` // 是否私密
|
||||||
|
ShowClientInfo bool `json:"show_client_info"` // 是否显示客户端信息
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetCommentListReq struct {
|
type GetCommentListReq struct {
|
||||||
|
@ -127,7 +127,7 @@ func (cr *CommentRepo) UpdateComment(comment *model.Comment) error {
|
|||||||
return errs.New(http.StatusBadRequest, "invalid comment ID", nil)
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"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"
|
||||||
|
|
||||||
@ -59,6 +60,7 @@ func (cs *CommentService) UpdateComment(ctx context.Context, req *dto.UpdateComm
|
|||||||
if !ok {
|
if !ok {
|
||||||
return errs.ErrUnauthorized
|
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)))
|
comment, err := repo.Comment.GetComment(strconv.Itoa(int(req.CommentID)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -71,13 +73,11 @@ func (cs *CommentService) UpdateComment(ctx context.Context, req *dto.UpdateComm
|
|||||||
|
|
||||||
comment.Content = req.Content
|
comment.Content = req.Content
|
||||||
comment.IsPrivate = req.IsPrivate
|
comment.IsPrivate = req.IsPrivate
|
||||||
|
comment.ShowClientInfo = req.ShowClientInfo
|
||||||
err = repo.Comment.UpdateComment(comment)
|
err = repo.Comment.UpdateComment(comment)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +102,6 @@ func (cs *CommentService) DeleteComment(ctx context.Context, commentID string) e
|
|||||||
if err := repo.Comment.DeleteComment(commentID); err != nil {
|
if err := repo.Comment.DeleteComment(commentID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,38 +119,7 @@ func (cs *CommentService) GetComment(ctx context.Context, commentID string) (*dt
|
|||||||
if comment.IsPrivate && currentUserID != comment.UserID {
|
if comment.IsPrivate && currentUserID != comment.UserID {
|
||||||
return nil, errs.ErrForbidden
|
return nil, errs.ErrForbidden
|
||||||
}
|
}
|
||||||
isLiked := false
|
commentDto := cs.toGetCommentDto(comment, currentUserID)
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
return &commentDto, err
|
return &commentDto, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,12 +135,27 @@ func (cs *CommentService) GetCommentList(ctx context.Context, req *dto.GetCommen
|
|||||||
commentDtos := make([]dto.CommentDto, 0)
|
commentDtos := make([]dto.CommentDto, 0)
|
||||||
for _, comment := range comments {
|
for _, comment := range comments {
|
||||||
//replyCount, _ := repo.Comment.CountReplyComments(currentUserID, comment.ID)
|
//replyCount, _ := repo.Comment.CountReplyComments(currentUserID, comment.ID)
|
||||||
|
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
|
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)
|
||||||
}
|
}
|
||||||
ua := utils.ParseUA(comment.UserAgent)
|
ua := utils.ParseUA(comment.UserAgent)
|
||||||
commentDto := dto.CommentDto{
|
if !comment.ShowClientInfo {
|
||||||
|
comment.Location = ""
|
||||||
|
ua.OS = ""
|
||||||
|
ua.OSVersion = ""
|
||||||
|
ua.Browser = ""
|
||||||
|
ua.BrowserVer = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto.CommentDto{
|
||||||
ID: comment.ID,
|
ID: comment.ID,
|
||||||
Content: comment.Content,
|
Content: comment.Content,
|
||||||
TargetID: comment.TargetID,
|
TargetID: comment.TargetID,
|
||||||
@ -189,17 +172,9 @@ func (cs *CommentService) GetCommentList(ctx context.Context, req *dto.GetCommen
|
|||||||
OS: ua.OS + " " + ua.OSVersion,
|
OS: ua.OS + " " + ua.OSVersion,
|
||||||
Browser: ua.Browser + " " + ua.BrowserVer,
|
Browser: ua.Browser + " " + ua.BrowserVer,
|
||||||
Location: comment.Location,
|
Location: comment.Location,
|
||||||
|
ShowClientInfo: comment.ShowClientInfo,
|
||||||
}
|
}
|
||||||
if !comment.ShowClientInfo {
|
|
||||||
commentDto.Location = ""
|
|
||||||
commentDto.OS = ""
|
|
||||||
commentDto.Browser = ""
|
|
||||||
}
|
}
|
||||||
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:
|
||||||
|
@ -36,17 +36,21 @@ export async function createComment(
|
|||||||
|
|
||||||
export async function updateComment(
|
export async function updateComment(
|
||||||
{
|
{
|
||||||
id, content,
|
id,
|
||||||
isPrivate = false
|
content,
|
||||||
|
isPrivate = false,
|
||||||
|
showClientInfo = true
|
||||||
}: {
|
}: {
|
||||||
id: number
|
id: number
|
||||||
content: string
|
content: string
|
||||||
isPrivate?: boolean // 可选字段,默认为 false
|
isPrivate?: boolean // 可选字段,默认为 false
|
||||||
|
showClientInfo?: boolean
|
||||||
}
|
}
|
||||||
): Promise<BaseResponse<Comment>> {
|
): Promise<BaseResponse<Comment>> {
|
||||||
const res = await axiosClient.put<BaseResponse<Comment>>(`/comment/c/${id}`, {
|
const res = await axiosClient.put<BaseResponse<Comment>>(`/comment/c/${id}`, {
|
||||||
content,
|
content,
|
||||||
isPrivate
|
isPrivate,
|
||||||
|
showClientInfo,
|
||||||
})
|
})
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,8 @@ export function CommentInput(
|
|||||||
initContent = "",
|
initContent = "",
|
||||||
initIsPrivate = false,
|
initIsPrivate = false,
|
||||||
placeholder = "",
|
placeholder = "",
|
||||||
isUpdate = false
|
isUpdate = false,
|
||||||
|
initShowClientInfo = true
|
||||||
}: {
|
}: {
|
||||||
user: User | null,
|
user: User | null,
|
||||||
onCommentSubmitted: ({ commentContent, isPrivate, showClientInfo }: { commentContent: string, isPrivate: boolean, showClientInfo: boolean }) => void,
|
onCommentSubmitted: ({ commentContent, isPrivate, showClientInfo }: { commentContent: string, isPrivate: boolean, showClientInfo: boolean }) => void,
|
||||||
@ -25,6 +26,7 @@ export function CommentInput(
|
|||||||
initIsPrivate?: boolean,
|
initIsPrivate?: boolean,
|
||||||
placeholder?: string,
|
placeholder?: string,
|
||||||
isUpdate?: boolean,
|
isUpdate?: boolean,
|
||||||
|
initShowClientInfo?: boolean
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const t = useTranslations('Comment')
|
const t = useTranslations('Comment')
|
||||||
@ -33,7 +35,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 [showClientInfo, setShowClientInfo] = useState(initShowClientInfo);
|
||||||
const [commentContent, setCommentContent] = useState(initContent);
|
const [commentContent, setCommentContent] = useState(initContent);
|
||||||
|
|
||||||
const handleCommentSubmit = async () => {
|
const handleCommentSubmit = async () => {
|
||||||
@ -51,7 +53,7 @@ export function CommentInput(
|
|||||||
toast.error(t("content_required"));
|
toast.error(t("content_required"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (initContent === commentContent.trim() && initIsPrivate === isPrivate) {
|
if (initContent === commentContent.trim() && initIsPrivate === isPrivate && initShowClientInfo === showClientInfo) {
|
||||||
toast.warning(t("comment_unchanged"));
|
toast.warning(t("comment_unchanged"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import { TargetType } from "@/models/types";
|
|||||||
import { toggleLike } from "@/api/like";
|
import { toggleLike } from "@/api/like";
|
||||||
import { useDoubleConfirm } from "@/hooks/use-double-confirm";
|
import { useDoubleConfirm } from "@/hooks/use-double-confirm";
|
||||||
import { CommentInput } from "./comment-input";
|
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 { OrderBy } from "@/models/common";
|
||||||
import { formatDateTime } from "@/utils/common/datetime";
|
import { formatDateTime } from "@/utils/common/datetime";
|
||||||
|
|
||||||
@ -41,11 +41,11 @@ export function CommentItem(
|
|||||||
const clickToLogin = useToLogin();
|
const clickToLogin = useToLogin();
|
||||||
const { confirming, onClick, onBlur } = useDoubleConfirm();
|
const { confirming, onClick, onBlur } = useDoubleConfirm();
|
||||||
|
|
||||||
const [likeCount, setLikeCount] = useState(comment.likeCount);
|
const [commentState, setCommentState] = useState<Comment>(comment); // 用于更新评论内容
|
||||||
const [liked, setLiked] = useState(comment.isLiked);
|
const [likeCount, setLikeCount] = useState(commentState.likeCount);
|
||||||
|
const [liked, setLiked] = useState(commentState.isLiked);
|
||||||
const [canClickLike, setCanClickLike] = useState(true);
|
const [canClickLike, setCanClickLike] = useState(true);
|
||||||
const [isPrivate, setIsPrivate] = useState(comment.isPrivate);
|
const [replyCount, setReplyCount] = useState(commentState.replyCount);
|
||||||
const [replyCount, setReplyCount] = useState(comment.replyCount);
|
|
||||||
const [showReplies, setShowReplies] = useState(false);
|
const [showReplies, setShowReplies] = useState(false);
|
||||||
const [replies, setReplies] = useState<Comment[]>([]);
|
const [replies, setReplies] = useState<Comment[]>([]);
|
||||||
const [repliesLoaded, setRepliesLoaded] = useState(false);
|
const [repliesLoaded, setRepliesLoaded] = useState(false);
|
||||||
@ -74,7 +74,7 @@ export function CommentItem(
|
|||||||
setLiked(prev => !prev);
|
setLiked(prev => !prev);
|
||||||
setLikeCount(prev => prev + (likedPrev ? -1 : 1));
|
setLikeCount(prev => prev + (likedPrev ? -1 : 1));
|
||||||
toggleLike(
|
toggleLike(
|
||||||
{ targetType: TargetType.Comment, targetId: comment.id }
|
{ targetType: TargetType.Comment, targetId: commentState.id }
|
||||||
).then(res => {
|
).then(res => {
|
||||||
toast.success(res.data.status ? t("like_success") : t("unlike_success"));
|
toast.success(res.data.status ? t("like_success") : t("unlike_success"));
|
||||||
setCanClickLike(true);
|
setCanClickLike(true);
|
||||||
@ -90,14 +90,14 @@ export function CommentItem(
|
|||||||
const reloadReplies = () => {
|
const reloadReplies = () => {
|
||||||
listComments(
|
listComments(
|
||||||
{
|
{
|
||||||
targetType: comment.targetType,
|
targetType: commentState.targetType,
|
||||||
targetId: comment.targetId,
|
targetId: commentState.targetId,
|
||||||
depth: comment.depth + 1,
|
depth: commentState.depth + 1,
|
||||||
orderBy: OrderBy.CreatedAt,
|
orderBy: OrderBy.CreatedAt,
|
||||||
desc: false,
|
desc: false,
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 999999,
|
size: 999999,
|
||||||
commentId: comment.id
|
commentId: commentState.id
|
||||||
}
|
}
|
||||||
).then(response => {
|
).then(response => {
|
||||||
setReplies(response.data.comments);
|
setReplies(response.data.comments);
|
||||||
@ -112,11 +112,13 @@ export function CommentItem(
|
|||||||
setShowReplies(!showReplies);
|
setShowReplies(!showReplies);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onCommentEdit = ({ commentContent: newContent, isPrivate }: { commentContent: string, isPrivate: boolean }) => {
|
const onCommentEdit = ({ commentContent: newContent, isPrivate, showClientInfo }: { commentContent: string, isPrivate: boolean, showClientInfo: boolean }) => {
|
||||||
updateComment({ id: comment.id, content: newContent, isPrivate }).then(() => {
|
updateComment({ id: commentState.id, content: newContent, isPrivate, showClientInfo }).then(() => {
|
||||||
toast.success(t("edit_success"));
|
toast.success(t("edit_success"));
|
||||||
comment.content = newContent;
|
getComment({ id: commentState.id }).then(response => {
|
||||||
setIsPrivate(isPrivate);
|
setCommentState(response.data);
|
||||||
|
console.log(response.data);
|
||||||
|
});
|
||||||
setActiveInputId(null);
|
setActiveInputId(null);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
toast.error(t("edit_failed") + ": " + error.message);
|
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 }) => {
|
const onReply = ({ commentContent, isPrivate, showClientInfo }: { commentContent: string, isPrivate: boolean, showClientInfo: boolean }) => {
|
||||||
createComment({
|
createComment({
|
||||||
targetType: comment.targetType,
|
targetType: commentState.targetType,
|
||||||
targetId: comment.targetId,
|
targetId: commentState.targetId,
|
||||||
content: commentContent,
|
content: commentContent,
|
||||||
replyId: comment.id,
|
replyId: commentState.id,
|
||||||
isPrivate,
|
isPrivate,
|
||||||
showClientInfo
|
showClientInfo
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
@ -158,24 +160,24 @@ export function CommentItem(
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div onClick={() => clickToUserProfile(comment.user.username)} className="cursor-pointer fade-in w-12 h-12">
|
<div onClick={() => clickToUserProfile(commentState.user.username)} className="cursor-pointer fade-in w-12 h-12">
|
||||||
<GravatarAvatar className="w-full h-full" url={comment.user.avatarUrl} email={comment.user.email} size={100} />
|
<GravatarAvatar className="w-full h-full" url={commentState.user.avatarUrl} email={commentState.user.email} size={100} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 pl-2 fade-in-up">
|
<div className="flex-1 pl-2 fade-in-up">
|
||||||
<div className="flex gap-2 md:gap-4 items-center">
|
<div className="flex gap-2 md:gap-4 items-center">
|
||||||
<div onClick={() => clickToUserProfile(comment.user.username)} className="font-bold text-base text-slate-800 dark:text-slate-100 cursor-pointer fade-in-up">
|
<div onClick={() => clickToUserProfile(commentState.user.username)} className="font-bold text-base text-slate-800 dark:text-slate-100 cursor-pointer fade-in-up">
|
||||||
{comment.user.nickname}
|
{commentState.user.nickname}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs">{formatDateTime({
|
<span className="text-xs">{formatDateTime({
|
||||||
dateTimeString: comment.createdAt,
|
dateTimeString: commentState.createdAt,
|
||||||
locale,
|
locale,
|
||||||
convertShortAgo: true,
|
convertShortAgo: true,
|
||||||
unitI18n: { secondsAgo: commonT("secondsAgo"), minutesAgo: commonT("minutesAgo"), hoursAgo: commonT("hoursAgo"), daysAgo: commonT("daysAgo") }
|
unitI18n: { secondsAgo: commonT("secondsAgo"), minutesAgo: commonT("minutesAgo"), hoursAgo: commonT("hoursAgo"), daysAgo: commonT("daysAgo") }
|
||||||
})}</span>
|
})}</span>
|
||||||
{comment.createdAt !== comment.updatedAt &&
|
{commentState.createdAt !== commentState.updatedAt &&
|
||||||
<span className="text-xs">{t("edit_at", {
|
<span className="text-xs">{t("edit_at", {
|
||||||
time: formatDateTime({
|
time: formatDateTime({
|
||||||
dateTimeString: comment.updatedAt,
|
dateTimeString: commentState.updatedAt,
|
||||||
locale,
|
locale,
|
||||||
convertShortAgo: true,
|
convertShortAgo: true,
|
||||||
unitI18n: { secondsAgo: commonT("secondsAgo"), minutesAgo: commonT("minutesAgo"), hoursAgo: commonT("hoursAgo"), daysAgo: commonT("daysAgo") }
|
unitI18n: { secondsAgo: commonT("secondsAgo"), minutesAgo: commonT("minutesAgo"), hoursAgo: commonT("hoursAgo"), daysAgo: commonT("daysAgo") }
|
||||||
@ -184,35 +186,35 @@ export function CommentItem(
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-lg text-slate-600 dark:text-slate-400 fade-in">
|
<p className="text-lg text-slate-600 dark:text-slate-400 fade-in">
|
||||||
{
|
{
|
||||||
isPrivate && <Lock className="inline w-4 h-4 mr-1 mb-1 text-slate-500 dark:text-slate-400" />
|
commentState.isPrivate && <Lock className="inline w-4 h-4 mr-1 mb-1 text-slate-500 dark:text-slate-400" />
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
parentComment &&
|
parentComment &&
|
||||||
<>{t("reply")} <button onClick={() => clickToUserProfile(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}
|
{commentState.content}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-1 text-xs text-slate-500 dark:text-slate-400 fade-in flex flex-col md:flex-row items-start md:items-center md:justify-between gap-2">
|
<div className="mt-1 text-xs text-slate-500 dark:text-slate-400 fade-in flex flex-col md:flex-row items-start md:items-center md:justify-between gap-2">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
{/* 用户地理,浏览器,系统信息 */}
|
{/* 用户地理,浏览器,系统信息 */}
|
||||||
{comment.location && <span title={comment.location} >{comment.location}</span>}
|
{commentState.location && <span title={commentState.location} >{commentState.location}</span>}
|
||||||
{comment.browser && <span title={comment.browser}>{comment.browser}</span>}
|
{commentState.browser && <span title={commentState.browser}>{commentState.browser}</span>}
|
||||||
{comment.os && <span title={comment.os}>{comment.os}</span>}
|
{commentState.os && <span title={commentState.os}>{commentState.os}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4 w-full md:w-auto">
|
<div className="flex items-center gap-4 w-full md:w-auto">
|
||||||
{/* 回复按钮 */}
|
{/* 回复按钮 */}
|
||||||
<button
|
<button
|
||||||
title={t("reply")}
|
title={t("reply")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (activeInput?.type === 'reply' && activeInput.id === comment.id) {
|
if (activeInput?.type === 'reply' && activeInput.id === commentState.id) {
|
||||||
setActiveInputId(null);
|
setActiveInputId(null);
|
||||||
} else {
|
} else {
|
||||||
setActiveInputId({ id: comment.id, type: 'reply' });
|
setActiveInputId({ id: commentState.id, type: 'reply' });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`flex items-center justify-center px-2 py-1 h-5
|
className={`flex items-center justify-center px-2 py-1 h-5
|
||||||
text-primary-foreground dark:text-white text-xs
|
text-primary-foreground dark:text-white text-xs
|
||||||
rounded ${activeInput?.type === 'reply' && activeInput.id === comment.id ? "bg-slate-600" : "bg-slate-400"} hover:bg-slate-600 dark:hover:bg-slate-500 fade-in-up`}
|
rounded ${activeInput?.type === 'reply' && activeInput.id === commentState.id ? "bg-slate-600" : "bg-slate-400"} hover:bg-slate-600 dark:hover:bg-slate-500 fade-in-up`}
|
||||||
>
|
>
|
||||||
<Reply className="w-3 h-3" />
|
<Reply className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
@ -228,21 +230,21 @@ export function CommentItem(
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 编辑和删除按钮 仅自己的评论可见 */}
|
{/* 编辑和删除按钮 仅自己的评论可见 */}
|
||||||
{user?.id === comment.user.id && (
|
{user?.id === commentState.user.id && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
title={t("edit")}
|
title={t("edit")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (activeInput?.type === 'edit' && activeInput.id === comment.id) {
|
if (activeInput?.type === 'edit' && activeInput.id === commentState.id) {
|
||||||
setActiveInputId(null);
|
setActiveInputId(null);
|
||||||
} else {
|
} else {
|
||||||
setActiveInputId({ id: comment.id, type: 'edit' });
|
setActiveInputId({ id: commentState.id, type: 'edit' });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`
|
className={`
|
||||||
flex items-center justify-center px-2 py-1 h-5
|
flex items-center justify-center px-2 py-1 h-5
|
||||||
text-primary-foreground dark:text-white text-xs
|
text-primary-foreground dark:text-white text-xs
|
||||||
rounded ${activeInput?.type === 'edit' && activeInput.id === comment.id ? "bg-slate-600" : "bg-slate-400"} hover:bg-slate-600 dark:hover:bg-slate-500 fade-in-up`}
|
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`}
|
||||||
>
|
>
|
||||||
<Pencil className="w-3 h-3" />
|
<Pencil className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
@ -252,7 +254,7 @@ export function CommentItem(
|
|||||||
className={`flex items-center justify-center px-2 py-1 h-5 rounded
|
className={`flex items-center justify-center px-2 py-1 h-5 rounded
|
||||||
text-primary-foreground dark:text-white text-xs
|
text-primary-foreground dark:text-white text-xs
|
||||||
${confirming ? 'bg-red-600 hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600' : 'bg-slate-400 hover:bg-slate-600 dark:hover:bg-slate-500'} fade-in`}
|
${confirming ? 'bg-red-600 hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600' : 'bg-slate-400 hover:bg-slate-600 dark:hover:bg-slate-500'} fade-in`}
|
||||||
onClick={() => onClick(() => { onCommentDelete({ commentId: comment.id }); })}
|
onClick={() => onClick(() => { onCommentDelete({ commentId: commentState.id }); })}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
>
|
>
|
||||||
<Trash className="w-3 h-3" />
|
<Trash className="w-3 h-3" />
|
||||||
@ -271,18 +273,19 @@ export function CommentItem(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 这俩输入框一次只能显示一个 */}
|
{/* 这俩输入框一次只能显示一个 */}
|
||||||
{activeInput && activeInput.type === 'reply' && activeInput.id === comment.id && <CommentInput
|
{activeInput && activeInput.type === 'reply' && activeInput.id === commentState.id && <CommentInput
|
||||||
user={user}
|
user={user}
|
||||||
onCommentSubmitted={onReply}
|
onCommentSubmitted={onReply}
|
||||||
initIsPrivate={isPrivate}
|
initIsPrivate={commentState.isPrivate}
|
||||||
placeholder={`${t("reply")} ${comment.user.nickname} :`}
|
placeholder={`${t("reply")} ${commentState.user.nickname} :`}
|
||||||
/>}
|
/>}
|
||||||
{activeInput && activeInput.type === 'edit' && activeInput.id === comment.id && <CommentInput
|
{activeInput && activeInput.type === 'edit' && activeInput.id === commentState.id && <CommentInput
|
||||||
user={user}
|
user={user}
|
||||||
initContent={comment.content}
|
initContent={commentState.content}
|
||||||
initIsPrivate={isPrivate}
|
initIsPrivate={commentState.isPrivate}
|
||||||
onCommentSubmitted={onCommentEdit}
|
onCommentSubmitted={onCommentEdit}
|
||||||
isUpdate={true}
|
isUpdate={true}
|
||||||
|
initShowClientInfo={commentState.showClientInfo}
|
||||||
/>}
|
/>}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -294,7 +297,7 @@ export function CommentItem(
|
|||||||
key={reply.id}
|
key={reply.id}
|
||||||
user={reply.user}
|
user={reply.user}
|
||||||
comment={reply}
|
comment={reply}
|
||||||
parentComment={comment}
|
parentComment={commentState}
|
||||||
onCommentDelete={onReplyDelete}
|
onCommentDelete={onReplyDelete}
|
||||||
activeInput={activeInput}
|
activeInput={activeInput}
|
||||||
setActiveInputId={setActiveInputId}
|
setActiveInputId={setActiveInputId}
|
||||||
|
@ -59,6 +59,63 @@ export const markdownComponents = {
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
table: (props: React.ComponentPropsWithoutRef<"table">) => {
|
||||||
|
const { children, className, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<div className="my-4 overflow-auto rounded-md shadow-sm">
|
||||||
|
<table
|
||||||
|
className={`min-w-full divide-y divide-gray-200 dark:divide-gray-700 ${className ?? ""}`}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
thead: (props: React.ComponentPropsWithoutRef<"thead">) => {
|
||||||
|
const { children, className, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<thead className={`bg-gray-50 dark:bg-gray-800 ${className ?? ""}`} {...rest}>
|
||||||
|
{children}
|
||||||
|
</thead>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
tbody: (props: React.ComponentPropsWithoutRef<"tbody">) => {
|
||||||
|
const { children, className, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<tbody className={`bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700 ${className ?? ""}`} {...rest}>
|
||||||
|
{children}
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
tr: (props: React.ComponentPropsWithoutRef<"tr">) => {
|
||||||
|
const { children, className, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<tr className={`odd:bg-white even:bg-gray-50 dark:odd:bg-gray-900 dark:even:bg-gray-800 ${className ?? ""}`} {...rest}>
|
||||||
|
{children}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
th: (props: React.ComponentPropsWithoutRef<"th">) => {
|
||||||
|
const { children, className, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className={`px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400 ${className ?? ""}`}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
td: (props: React.ComponentPropsWithoutRef<"td">) => {
|
||||||
|
const { children, className, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<td className={`px-3 py-2 align-top text-sm text-gray-700 dark:text-gray-300 ${className ?? ""}`} {...rest}>
|
||||||
|
{children}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RenderMarkdown(props: Omit<MDXRemoteProps, "components">) {
|
export function RenderMarkdown(props: Omit<MDXRemoteProps, "components">) {
|
||||||
|
@ -18,4 +18,5 @@ export interface Comment {
|
|||||||
os: string
|
os: string
|
||||||
browser: string
|
browser: string
|
||||||
location: string
|
location: string
|
||||||
|
showClientInfo: boolean
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user