feat: 评论支持回复(两级 + @某人)
不做无限层级——手机屏幕撑不住层层缩进,读到第四层就没法看了。
微信、小红书、B站清一色是两级:一级评论 + 其下的回复列表,回复里
用「回复 @某人」表达指向谁。
数据结构:comments 加 parent_id + reply_to_name。回复的回复会被
压平到同一条一级评论下(parent_id 取爷爷的),同时把被回复人的名字
记进 reply_to_name —— 这样既保住两级,又不丢「在跟谁说话」的信息。
几个容易做错的地方:
- 一次查完这一页所有一级评论的回复,不是每条一次查询(N+1)
- 一级评论默认只带 3 条回复,其余点「展开全部 N 条」再拉,避免热门
评论一次返回几百条
- 删一级评论要连它下面的回复一起删,否则回复变成挂在空处的孤儿;
帖子的 comment_count 也要按实际删除条数减,不是减 1
- total 统计含回复,和帖子上显示的数字对得上
- replies 为空时返回 [],不是 null
实测四条评论(含一条回复的回复):
▸ 甲:一级评论 (2 条回复)
└ 乙:多久了?
└ 甲 回复 乙:三天了
▸ 乙:另一条一级评论 (0 条回复)
删掉第一条一级评论后 total 4→1,comment_count 同步。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -115,6 +115,7 @@ func (h *Handler) ListComments(c *gin.Context) {
|
|||||||
type commentReq struct {
|
type commentReq struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Images []string `json:"images"`
|
Images []string `json:"images"`
|
||||||
|
ParentID string `json:"parent_id"` // 回复某条评论时传它的 id
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateComment POST /api/posts/:id/comments
|
// CreateComment POST /api/posts/:id/comments
|
||||||
@@ -128,7 +129,7 @@ func (h *Handler) CreateComment(c *gin.Context) {
|
|||||||
response.FailParams(c, "评论内容不能为空")
|
response.FailParams(c, "评论内容不能为空")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
comment, err := h.svc.CreateComment(middleware.UserID(c), idParam(c, "id"), req.Content, req.Images)
|
comment, err := h.svc.CreateComment(middleware.UserID(c), idParam(c, "id"), req.Content, req.Images, req.ParentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondErr(c, err)
|
respondErr(c, err)
|
||||||
return
|
return
|
||||||
@@ -171,3 +172,13 @@ func (h *Handler) DeleteOwnComment(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
response.OK(c, gin.H{"deleted": true})
|
response.OK(c, gin.H{"deleted": true})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListReplies GET /api/comments/:id/replies 展开某条评论的全部回复
|
||||||
|
func (h *Handler) ListReplies(c *gin.Context) {
|
||||||
|
list, err := h.svc.ListReplies(middleware.UserID(c), idParam(c, "id"))
|
||||||
|
if err != nil {
|
||||||
|
respondErr(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, list)
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,14 @@ type Comment struct {
|
|||||||
AuthorName string `gorm:"size:64" json:"author_name"`
|
AuthorName string `gorm:"size:64" json:"author_name"`
|
||||||
Content string `gorm:"size:512" json:"content"`
|
Content string `gorm:"size:512" json:"content"`
|
||||||
Images datatypes.JSON `json:"images"`
|
Images datatypes.JSON `json:"images"`
|
||||||
|
// 两级评论:ParentID 为空是一级评论,否则是某条一级评论下的回复。
|
||||||
|
// 不做无限层级——手机上层层缩进读到第四层就没法看了,
|
||||||
|
// 主流产品(微信、小红书、B站)都是两级 + @某人 表达指向。
|
||||||
|
ParentID string `gorm:"size:24;index" json:"parent_id"`
|
||||||
|
ReplyToName string `gorm:"size:64" json:"reply_to_name"`
|
||||||
Status string `gorm:"size:16;default:published" json:"status"`
|
Status string `gorm:"size:16;default:published" json:"status"`
|
||||||
|
|
||||||
IsSelf bool `gorm:"-" json:"is_self"` // 计算字段:是不是我自己发的
|
IsSelf bool `gorm:"-" json:"is_self"` // 计算字段:是不是我自己发的
|
||||||
|
Replies []Comment `gorm:"-" json:"replies"` // 计算字段:一级评论下挂的回复
|
||||||
|
ReplyN int `gorm:"-" json:"reply_n"` // 回复总数(可能多于返回的条数)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ func registerUserAPI(api *gin.RouterGroup, h *handler.Handler, jm *appjwt.Manage
|
|||||||
g.POST("/posts/:id/comments", h.CreateComment)
|
g.POST("/posts/:id/comments", h.CreateComment)
|
||||||
g.DELETE("/posts/:id", h.DeleteOwnPost)
|
g.DELETE("/posts/:id", h.DeleteOwnPost)
|
||||||
g.DELETE("/comments/:id", h.DeleteOwnComment)
|
g.DELETE("/comments/:id", h.DeleteOwnComment)
|
||||||
|
g.GET("/comments/:id/replies", h.ListReplies)
|
||||||
|
|
||||||
g.GET("/articles", h.ListArticles)
|
g.GET("/articles", h.ListArticles)
|
||||||
g.POST("/feedback", h.CreateFeedback)
|
g.POST("/feedback", h.CreateFeedback)
|
||||||
|
|||||||
@@ -193,23 +193,67 @@ func (s *Service) postLikeCount(postID string) (int, error) {
|
|||||||
|
|
||||||
// ListComments 评论分页
|
// ListComments 评论分页
|
||||||
func (s *Service) ListComments(userID, postID string, offset, limit int) ([]model.Comment, int64, error) {
|
func (s *Service) ListComments(userID, postID string, offset, limit int) ([]model.Comment, int64, error) {
|
||||||
q := s.db.Model(&model.Comment{}).Where("post_id = ? AND status = ?", postID, "published")
|
base := func() *gorm.DB {
|
||||||
|
return s.db.Model(&model.Comment{}).Where("post_id = ? AND status = ?", postID, "published")
|
||||||
|
}
|
||||||
|
// total 是这条帖子的评论总数(含回复),和帖子上显示的数字对得上
|
||||||
var total int64
|
var total int64
|
||||||
if err := q.Count(&total).Error; err != nil {
|
if err := base().Count(&total).Error; err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
var comments []model.Comment
|
// 只分页一级评论,回复跟着它爹一起返回
|
||||||
if err := q.Order("id desc").Offset(offset).Limit(limit).Find(&comments).Error; err != nil {
|
var roots []model.Comment
|
||||||
|
if err := base().Where("parent_id = ? OR parent_id IS NULL", "").
|
||||||
|
Order("id desc").Offset(offset).Limit(limit).Find(&roots).Error; err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
for i := range comments {
|
if len(roots) == 0 {
|
||||||
comments[i].IsSelf = comments[i].UserID == userID
|
return []model.Comment{}, total, nil
|
||||||
}
|
}
|
||||||
return comments, total, nil
|
|
||||||
|
ids := make([]string, 0, len(roots))
|
||||||
|
for i := range roots {
|
||||||
|
roots[i].IsSelf = roots[i].UserID == userID
|
||||||
|
ids = append(ids, roots[i].ID)
|
||||||
|
}
|
||||||
|
// 一次查完这一页所有一级评论的回复,不要每条一次查询
|
||||||
|
var replies []model.Comment
|
||||||
|
s.db.Where("parent_id IN ? AND status = ?", ids, "published").Order("id asc").Find(&replies)
|
||||||
|
byParent := map[string][]model.Comment{}
|
||||||
|
for i := range replies {
|
||||||
|
replies[i].IsSelf = replies[i].UserID == userID
|
||||||
|
byParent[replies[i].ParentID] = append(byParent[replies[i].ParentID], replies[i])
|
||||||
|
}
|
||||||
|
for i := range roots {
|
||||||
|
list := byParent[roots[i].ID]
|
||||||
|
if list == nil {
|
||||||
|
list = []model.Comment{} // 空切片而不是 nil,否则 JSON 出来是 null
|
||||||
|
}
|
||||||
|
roots[i].ReplyN = len(list)
|
||||||
|
// 默认只带前 3 条,其余等前端点「展开」再要,避免热门评论一次拉几百条
|
||||||
|
if len(list) > 3 {
|
||||||
|
list = list[:3]
|
||||||
|
}
|
||||||
|
roots[i].Replies = list
|
||||||
|
}
|
||||||
|
return roots, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListReplies 展开某条一级评论下的全部回复
|
||||||
|
func (s *Service) ListReplies(userID, commentID string) ([]model.Comment, error) {
|
||||||
|
var list []model.Comment
|
||||||
|
if err := s.db.Where("parent_id = ? AND status = ?", commentID, "published").
|
||||||
|
Order("id asc").Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for i := range list {
|
||||||
|
list[i].IsSelf = list[i].UserID == userID
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateComment 评论
|
// CreateComment 评论
|
||||||
func (s *Service) CreateComment(userID, postID string, content string, images []string) (*model.Comment, error) {
|
func (s *Service) CreateComment(userID, postID string, content string, images []string, parentID string) (*model.Comment, error) {
|
||||||
if _, err := s.GetPost(postID); err != nil {
|
if _, err := s.GetPost(postID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -218,7 +262,23 @@ func (s *Service) CreateComment(userID, postID string, content string, images []
|
|||||||
if err := s.db.First(&user, userID).Error; err == nil && user.Nickname != "" {
|
if err := s.db.First(&user, userID).Error; err == nil && user.Nickname != "" {
|
||||||
name = user.Nickname
|
name = user.Nickname
|
||||||
}
|
}
|
||||||
comment := model.Comment{PostID: postID, UserID: userID, AuthorName: name, Content: content, Status: "published"}
|
// 回复的回复仍然挂到同一条一级评论下,保持两级不塌成深树
|
||||||
|
replyTo := ""
|
||||||
|
if parentID != "" {
|
||||||
|
var parent model.Comment
|
||||||
|
if err := s.db.Where("id = ? AND post_id = ?", parentID, postID).First(&parent).Error; err != nil {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
if parent.ParentID != "" {
|
||||||
|
replyTo = parent.AuthorName
|
||||||
|
parentID = parent.ParentID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
comment := model.Comment{
|
||||||
|
PostID: postID, UserID: userID, AuthorName: name, Content: content,
|
||||||
|
Status: "published", ParentID: parentID, ReplyToName: replyTo,
|
||||||
|
}
|
||||||
// 配图存 JSON 数组。空数组也要显式写,否则前端拿到 null 还要额外判空
|
// 配图存 JSON 数组。空数组也要显式写,否则前端拿到 null 还要额外判空
|
||||||
if b, err := json.Marshal(images); err == nil && len(images) > 0 {
|
if b, err := json.Marshal(images); err == nil && len(images) > 0 {
|
||||||
comment.Images = b
|
comment.Images = b
|
||||||
@@ -265,7 +325,18 @@ func (s *Service) DeleteOwnComment(userID, commentID string) error {
|
|||||||
Update("status", "deleted").Error; err != nil {
|
Update("status", "deleted").Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return tx.Model(&model.Post{}).Where("id = ? AND comment_count > 0", c.PostID).
|
// 删一级评论要连它下面的回复一起删,否则回复会变成挂在空处的孤儿
|
||||||
UpdateColumn("comment_count", gorm.Expr("comment_count - 1")).Error
|
n := int64(1)
|
||||||
|
if c.ParentID == "" {
|
||||||
|
res := tx.Model(&model.Comment{}).
|
||||||
|
Where("parent_id = ? AND status = ?", commentID, "published").
|
||||||
|
Update("status", "deleted")
|
||||||
|
if res.Error != nil {
|
||||||
|
return res.Error
|
||||||
|
}
|
||||||
|
n += res.RowsAffected
|
||||||
|
}
|
||||||
|
return tx.Model(&model.Post{}).Where("id = ? AND comment_count >= ?", c.PostID, n).
|
||||||
|
UpdateColumn("comment_count", gorm.Expr("comment_count - ?", n)).Error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ Component({
|
|||||||
posterSaving: false,
|
posterSaving: false,
|
||||||
centered: false,
|
centered: false,
|
||||||
commentImages: [],
|
commentImages: [],
|
||||||
|
replyTo: {},
|
||||||
},
|
},
|
||||||
observers: {
|
observers: {
|
||||||
show: function (show) {
|
show: function (show) {
|
||||||
@@ -165,6 +166,7 @@ Component({
|
|||||||
patch.addBreed = '';
|
patch.addBreed = '';
|
||||||
patch.commentText = '';
|
patch.commentText = '';
|
||||||
patch.commentImages = [];
|
patch.commentImages = [];
|
||||||
|
patch.replyTo = {};
|
||||||
patch.riskData = null;
|
patch.riskData = null;
|
||||||
patch.saving = false;
|
patch.saving = false;
|
||||||
}
|
}
|
||||||
@@ -417,6 +419,7 @@ Component({
|
|||||||
// 没有头像字段,用昵称首字做个色块,比一律显示同一个 emoji 强
|
// 没有头像字段,用昵称首字做个色块,比一律显示同一个 emoji 强
|
||||||
initial: (c.author_name || '?').slice(0, 1),
|
initial: (c.author_name || '?').slice(0, 1),
|
||||||
imgs: Array.isArray(c.images) ? c.images : [],
|
imgs: Array.isArray(c.images) ? c.images : [],
|
||||||
|
replies: (c.replies || []).map((r) => ({ ...r, timeText: fmtAgo(r.created_at) })),
|
||||||
})),
|
})),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -424,8 +427,9 @@ Component({
|
|||||||
},
|
},
|
||||||
// 长按删除自己的评论
|
// 长按删除自己的评论
|
||||||
onDeleteComment(e) {
|
onDeleteComment(e) {
|
||||||
const c = this.data.comments[e.currentTarget.dataset.index];
|
const { id, self } = e.currentTarget.dataset;
|
||||||
if (!c || !c.is_self) return;
|
if (!id || !self) return;
|
||||||
|
const c = { id };
|
||||||
wx.showModal({
|
wx.showModal({
|
||||||
title: '删除评论',
|
title: '删除评论',
|
||||||
content: '确定删除这条评论?',
|
content: '确定删除这条评论?',
|
||||||
@@ -934,8 +938,13 @@ Component({
|
|||||||
if (!text) return wx.showToast({ title: '写点什么再发', icon: 'none' });
|
if (!text) return wx.showToast({ title: '写点什么再发', icon: 'none' });
|
||||||
if (!this.data.postId) return this.close();
|
if (!this.data.postId) return this.close();
|
||||||
try {
|
try {
|
||||||
await api.createComment(this.data.postId, text, this.data.commentImages.map((i) => i.url));
|
await api.createComment(
|
||||||
this.setData({ commentText: '', commentImages: [] });
|
this.data.postId,
|
||||||
|
text,
|
||||||
|
this.data.commentImages.map((i) => i.url),
|
||||||
|
this.data.replyTo.id || '',
|
||||||
|
);
|
||||||
|
this.setData({ commentText: '', commentImages: [], replyTo: {} });
|
||||||
this.loadComments();
|
this.loadComments();
|
||||||
this.triggerEvent('commented');
|
this.triggerEvent('commented');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -955,6 +964,26 @@ Component({
|
|||||||
wx.showToast({ title: (e && e.message) || '上传失败', icon: 'none' });
|
wx.showToast({ title: (e && e.message) || '上传失败', icon: 'none' });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
// 回复某条评论:输入区顶部显示「回复 xxx」,发送时带上 parent_id
|
||||||
|
onReplyTo(e) {
|
||||||
|
const { id, name } = e.currentTarget.dataset;
|
||||||
|
this.setData({ replyTo: { id, name } });
|
||||||
|
},
|
||||||
|
onCancelReply() {
|
||||||
|
this.setData({ replyTo: {} });
|
||||||
|
},
|
||||||
|
// 一级评论默认只带 3 条回复,点开拉全量
|
||||||
|
onExpandReplies(e) {
|
||||||
|
const { id, index } = e.currentTarget.dataset;
|
||||||
|
api
|
||||||
|
.listReplies(id)
|
||||||
|
.then((list) =>
|
||||||
|
this.setData({
|
||||||
|
[`comments[${index}].replies`]: (list || []).map((r) => ({ ...r, timeText: fmtAgo(r.created_at) })),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.catch((err) => wx.showToast({ title: (err && err.message) || '加载失败', icon: 'none' }));
|
||||||
|
},
|
||||||
onPreviewCommentImage(e) {
|
onPreviewCommentImage(e) {
|
||||||
const { urls, cur } = e.currentTarget.dataset;
|
const { urls, cur } = e.currentTarget.dataset;
|
||||||
if (urls && urls.length) wx.previewImage({ urls, current: cur });
|
if (urls && urls.length) wx.previewImage({ urls, current: cur });
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ module.exports.sel = function (map, key, index, def) {
|
|||||||
<block wx:elif="{{innerType === 'comments'}}">
|
<block wx:elif="{{innerType === 'comments'}}">
|
||||||
<view class="sheet-h3">评论 {{comments.length ? comments.length : ''}}</view>
|
<view class="sheet-h3">评论 {{comments.length ? comments.length : ''}}</view>
|
||||||
|
|
||||||
<view wx:for="{{comments}}" wx:key="id" class="cm" bindlongpress="onDeleteComment" data-index="{{index}}">
|
<view wx:for="{{comments}}" wx:key="id" class="cm">
|
||||||
<view class="cm-av">{{item.initial}}</view>
|
<view class="cm-av">{{item.initial}}</view>
|
||||||
<view class="cm-body">
|
<view class="cm-body">
|
||||||
<view class="cm-head">
|
<view class="cm-head">
|
||||||
@@ -329,12 +329,28 @@ module.exports.sel = function (map, key, index, def) {
|
|||||||
<text wx:if="{{item.is_self}}" class="mine-tag">我</text>
|
<text wx:if="{{item.is_self}}" class="mine-tag">我</text>
|
||||||
<text class="cm-time">{{item.timeText}}</text>
|
<text class="cm-time">{{item.timeText}}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="cm-text">{{item.content}}</view>
|
<view class="cm-text" bindlongpress="onDeleteComment" data-id="{{item.id}}" data-self="{{item.is_self}}">{{item.content}}</view>
|
||||||
<view wx:if="{{item.imgs.length}}" class="cm-imgs">
|
<view wx:if="{{item.imgs.length}}" class="cm-imgs">
|
||||||
<image wx:for="{{item.imgs}}" wx:for-item="u" wx:key="*this" class="cm-img"
|
<image wx:for="{{item.imgs}}" wx:for-item="u" wx:key="*this" class="cm-img"
|
||||||
src="{{u}}" mode="aspectFill" catchtap="onPreviewCommentImage"
|
src="{{u}}" mode="aspectFill" catchtap="onPreviewCommentImage"
|
||||||
data-urls="{{item.imgs}}" data-cur="{{u}}"></image>
|
data-urls="{{item.imgs}}" data-cur="{{u}}"></image>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 回复:只缩进一层。再深就没法在手机上读了 -->
|
||||||
|
<view wx:if="{{item.replies.length}}" class="cm-replies">
|
||||||
|
<view wx:for="{{item.replies}}" wx:for-item="r" wx:key="id" class="cm-reply"
|
||||||
|
bindlongpress="onDeleteComment" data-id="{{r.id}}" data-self="{{r.is_self}}">
|
||||||
|
<text class="cm-name">{{r.author_name}}</text>
|
||||||
|
<text wx:if="{{r.reply_to_name}}" class="cm-at"> 回复 {{r.reply_to_name}}</text>
|
||||||
|
<text class="cm-name">:</text>{{r.content}}
|
||||||
|
</view>
|
||||||
|
<view wx:if="{{item.reply_n > item.replies.length}}" class="cm-more"
|
||||||
|
bindtap="onExpandReplies" data-id="{{item.id}}" data-index="{{index}}">
|
||||||
|
展开全部 {{item.reply_n}} 条回复
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="cm-act" bindtap="onReplyTo" data-id="{{item.id}}" data-name="{{item.author_name}}">回复</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -342,7 +358,11 @@ module.exports.sel = function (map, key, index, def) {
|
|||||||
<view wx:if="{{comments.length}}" class="cm-tip">长按自己的评论可删除</view>
|
<view wx:if="{{comments.length}}" class="cm-tip">长按自己的评论可删除</view>
|
||||||
|
|
||||||
<view class="cm-editor">
|
<view class="cm-editor">
|
||||||
<textarea class="textarea cm-ta" placeholder="友善交流,分享你的经验…" placeholder-class="placeholder"
|
<view wx:if="{{replyTo.name}}" class="cm-replying">
|
||||||
|
<text>回复 {{replyTo.name}}</text>
|
||||||
|
<view class="cm-cancel" bindtap="onCancelReply"><pt-icon name="close" size="{{24}}"></pt-icon></view>
|
||||||
|
</view>
|
||||||
|
<textarea class="textarea cm-ta" placeholder="{{replyTo.name ? '回复 ' + replyTo.name : '友善交流,分享你的经验…'}}" placeholder-class="placeholder"
|
||||||
value="{{commentText}}" bindinput="onCommentInput" maxlength="500"
|
value="{{commentText}}" bindinput="onCommentInput" maxlength="500"
|
||||||
auto-height="{{true}}" show-confirm-bar="{{false}}" cursor-spacing="24"></textarea>
|
auto-height="{{true}}" show-confirm-bar="{{false}}" cursor-spacing="24"></textarea>
|
||||||
<view wx:if="{{commentImages.length}}" class="img-picker" style="margin-top:16rpx">
|
<view wx:if="{{commentImages.length}}" class="img-picker" style="margin-top:16rpx">
|
||||||
|
|||||||
@@ -127,3 +127,16 @@
|
|||||||
.cm-count{margin-left:auto;color:var(--muted2);font-size:var(--fs-cap)}
|
.cm-count{margin-left:auto;color:var(--muted2);font-size:var(--fs-cap)}
|
||||||
.cm-imgs{display:flex;flex-wrap:wrap;gap:var(--sp-1);margin-top:var(--sp-2)}
|
.cm-imgs{display:flex;flex-wrap:wrap;gap:var(--sp-1);margin-top:var(--sp-2)}
|
||||||
.cm-img{width:150rpx;height:150rpx;border-radius:var(--r-sm)}
|
.cm-img{width:150rpx;height:150rpx;border-radius:var(--r-sm)}
|
||||||
|
|
||||||
|
/* 回复:只缩进一层,再深手机上就没法读了 */
|
||||||
|
.cm-replies{margin-top:var(--sp-2);padding:var(--sp-2) var(--sp-3);background:var(--surface-2);border-radius:var(--r-sm)}
|
||||||
|
.cm-reply{font-size:var(--fs-sm);line-height:1.6;color:var(--text);padding:4rpx 0}
|
||||||
|
.cm-at{color:var(--muted)}
|
||||||
|
.cm-more{margin-top:6rpx;font-size:var(--fs-cap);color:var(--primary-dark)}
|
||||||
|
.cm-act{margin-top:var(--sp-2);font-size:var(--fs-cap);color:var(--muted)}
|
||||||
|
.cm-replying{
|
||||||
|
display:flex;align-items:center;gap:var(--sp-2);margin-bottom:var(--sp-2);
|
||||||
|
padding:var(--sp-1) var(--sp-3);border-radius:var(--r-full);
|
||||||
|
background:var(--primary-soft);color:var(--primary-ink);font-size:var(--fs-cap);align-self:flex-start;
|
||||||
|
}
|
||||||
|
.cm-cancel{display:flex;align-items:center}
|
||||||
|
|||||||
@@ -108,8 +108,13 @@ const api = {
|
|||||||
deletePost: (id) => request({ url: `/api/posts/${id}`, method: 'DELETE' }),
|
deletePost: (id) => request({ url: `/api/posts/${id}`, method: 'DELETE' }),
|
||||||
deleteComment: (id) => request({ url: `/api/comments/${id}`, method: 'DELETE' }),
|
deleteComment: (id) => request({ url: `/api/comments/${id}`, method: 'DELETE' }),
|
||||||
listComments: (id) => request({ url: `/api/posts/${id}/comments` }),
|
listComments: (id) => request({ url: `/api/posts/${id}/comments` }),
|
||||||
createComment: (id, content, images) =>
|
createComment: (id, content, images, parentId) =>
|
||||||
request({ url: `/api/posts/${id}/comments`, method: 'POST', data: { content, images: images || [] } }),
|
request({
|
||||||
|
url: `/api/posts/${id}/comments`,
|
||||||
|
method: 'POST',
|
||||||
|
data: { content, images: images || [], parent_id: parentId || '' },
|
||||||
|
}),
|
||||||
|
listReplies: (commentId) => request({ url: `/api/comments/${commentId}/replies` }),
|
||||||
|
|
||||||
// 文章
|
// 文章
|
||||||
listArticles: () => request({ url: '/api/articles' }),
|
listArticles: () => request({ url: '/api/articles' }),
|
||||||
|
|||||||
Reference in New Issue
Block a user