fix: 评论系统全量检查修复(7 项 Gap 全部修复)
- Delete 权限:新增 requesterID/isAdmin 参数,Service 层鉴权(仅本人/管理员可删)
- 通知内容:改用 GetUsernameByID 显示真实用户名,非数字 UID
- SearchUsers:Repository 返回 exp,Service 调用 GetLevelByExp 计算等级
- 通知高亮:Notification 新增 CommentID 字段,消息链接支持 #comment-{id}
- 已删除提示:前端 hash 检测 + 3 秒超时 Toast "该评论已不存在"
- Admin CSS:移除不存在的 comments.css 引用
- 错误哨兵:改用 common.ErrCommentNotFound/ErrCommentEmpty/PermissionDenied
This commit is contained in:
@ -23,8 +23,7 @@ func NewAdminCommentController(cs adminCommentUseCase) *AdminCommentController {
|
|||||||
// CommentsPage SSR 页面
|
// CommentsPage SSR 页面
|
||||||
func (ctrl *AdminCommentController) CommentsPage(c *gin.Context) {
|
func (ctrl *AdminCommentController) CommentsPage(c *gin.Context) {
|
||||||
c.HTML(http.StatusOK, "admin/comments/index.html", common.BuildAdminPageData(c, gin.H{
|
c.HTML(http.StatusOK, "admin/comments/index.html", common.BuildAdminPageData(c, gin.H{
|
||||||
"Title": "评论管理",
|
"Title": "评论管理",
|
||||||
"ExtraCSS": "/admin/static/css/comments.css",
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +57,7 @@ func (ctrl *AdminCommentController) Delete(c *gin.Context) {
|
|||||||
common.Error(c, http.StatusBadRequest, "无效的评论ID")
|
common.Error(c, http.StatusBadRequest, "无效的评论ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := ctrl.commentService.Delete(uint(commentID)); err != nil {
|
if err := ctrl.commentService.Delete(uint(commentID), 0, true); err != nil {
|
||||||
common.Error(c, http.StatusBadRequest, err.Error())
|
common.Error(c, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,5 +50,5 @@ type adminPostUseCase interface {
|
|||||||
// adminCommentUseCase AdminCommentController 对 CommentService 的最小依赖(ISP:2 个方法)
|
// adminCommentUseCase AdminCommentController 对 CommentService 的最小依赖(ISP:2 个方法)
|
||||||
type adminCommentUseCase interface {
|
type adminCommentUseCase interface {
|
||||||
ListAllComments(keyword string, showDeleted bool, page, pageSize int) ([]model.AdminCommentRow, int64, error)
|
ListAllComments(keyword string, showDeleted bool, page, pageSize int) ([]model.AdminCommentRow, int64, error)
|
||||||
Delete(commentID uint) error
|
Delete(commentID uint, requesterID uint, isAdmin bool) error
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"image"
|
"image"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"image/png"
|
"image/png"
|
||||||
@ -147,14 +148,13 @@ func (ctrl *CommentController) Delete(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 权限检查:仅本人或管理员可删除
|
// 权限检查由 service 层统一处理
|
||||||
_ = uid
|
isAdmin := model.HasMinRole(role, model.RoleAdmin)
|
||||||
_ = role
|
if err := ctrl.commentService.Delete(uint(commentID), uid, isAdmin); err != nil {
|
||||||
// 此处权限在 service 层不做额外判断,允许任何人请求删除
|
if errors.Is(err, common.ErrPermissionDenied) {
|
||||||
// 后端检查在之前 FindByID 之后可判断 comment.user_id vs current uid
|
common.Error(c, http.StatusForbidden, err.Error())
|
||||||
// 为简化,暂时信任前端权限,后续在 service 可增加
|
return
|
||||||
|
}
|
||||||
if err := ctrl.commentService.Delete(uint(commentID)); err != nil {
|
|
||||||
common.Error(c, http.StatusBadRequest, err.Error())
|
common.Error(c, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,7 +96,7 @@ type energyAdminUseCase interface {
|
|||||||
type commentUseCase interface {
|
type commentUseCase interface {
|
||||||
CreateRoot(userID, postID uint, body string) (*model.Comment, error)
|
CreateRoot(userID, postID uint, body string) (*model.Comment, error)
|
||||||
CreateReply(userID, parentID uint, body string) (*model.Comment, error)
|
CreateReply(userID, parentID uint, body string) (*model.Comment, error)
|
||||||
Delete(commentID uint) error
|
Delete(commentID uint, requesterID uint, isAdmin bool) error
|
||||||
ListRootComments(postID uint, page, pageSize int) ([]model.Comment, int64, error)
|
ListRootComments(postID uint, page, pageSize int) ([]model.Comment, int64, error)
|
||||||
ListReplies(rootID uint) ([]model.Comment, error)
|
ListReplies(rootID uint) ([]model.Comment, error)
|
||||||
SearchUsers(keyword string) ([]model.UserSearchResult, error)
|
SearchUsers(keyword string) ([]model.UserSearchResult, error)
|
||||||
|
|||||||
@ -20,7 +20,8 @@ type Notification struct {
|
|||||||
Title string `gorm:"type:varchar(200);not null" json:"title"`
|
Title string `gorm:"type:varchar(200);not null" json:"title"`
|
||||||
Content string `gorm:"type:text" json:"content"`
|
Content string `gorm:"type:text" json:"content"`
|
||||||
IsRead bool `gorm:"index:idx_user_read,priority:2;default:false;not null" json:"is_read"`
|
IsRead bool `gorm:"index:idx_user_read,priority:2;default:false;not null" json:"is_read"`
|
||||||
RelatedID *uint `gorm:"default:null" json:"related_id,omitempty"` // 关联业务 ID
|
RelatedID *uint `gorm:"default:null" json:"related_id,omitempty"` // 关联业务 ID(文章/评论等)
|
||||||
|
CommentID *uint `gorm:"default:null" json:"comment_id,omitempty"` // 评论 ID(用于跳转高亮目标评论)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyTypeNames 通知类型 → 中文名
|
// NotifyTypeNames 通知类型 → 中文名
|
||||||
|
|||||||
@ -185,23 +185,23 @@ func (r *CommentRepo) FindMentionsByComment(commentID uint) ([]model.CommentMent
|
|||||||
return list, err
|
return list, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchUsersByLevel 搜索 LV3+ 用户(用于@艾特)
|
// SearchUsersByLevel 搜索指定等级及以上用户(用于@艾特),返回 uid/username/exp
|
||||||
func (r *CommentRepo) SearchUsersByLevel(keyword string, minLevel int, limit int) ([]struct {
|
func (r *CommentRepo) SearchUsersByLevel(keyword string, minLevel int, limit int) ([]struct {
|
||||||
UID string
|
UID string
|
||||||
Username string
|
Username string
|
||||||
|
Exp int
|
||||||
}, error) {
|
}, error) {
|
||||||
// LV3 = 1500 exp
|
|
||||||
type result struct {
|
type result struct {
|
||||||
UID string
|
UID uint
|
||||||
Username string
|
Username string
|
||||||
|
Exp int
|
||||||
}
|
}
|
||||||
var list []result
|
var list []result
|
||||||
|
|
||||||
// level 是计算字段,用 exp >= thresholds[minLevel] 判断
|
|
||||||
threshold := model.LevelThresholds[minLevel]
|
threshold := model.LevelThresholds[minLevel]
|
||||||
|
|
||||||
err := r.db.Table("users").
|
err := r.db.Table("users").
|
||||||
Select("uid, username").
|
Select("uid, username, exp").
|
||||||
Where("username LIKE ? AND exp >= ? AND deleted_at IS NULL", "%"+keyword+"%", threshold).
|
Where("username LIKE ? AND exp >= ? AND deleted_at IS NULL", "%"+keyword+"%", threshold).
|
||||||
Order("exp DESC").
|
Order("exp DESC").
|
||||||
Limit(limit).
|
Limit(limit).
|
||||||
@ -210,12 +210,14 @@ func (r *CommentRepo) SearchUsersByLevel(keyword string, minLevel int, limit int
|
|||||||
var res []struct {
|
var res []struct {
|
||||||
UID string
|
UID string
|
||||||
Username string
|
Username string
|
||||||
|
Exp int
|
||||||
}
|
}
|
||||||
for _, u := range list {
|
for _, u := range list {
|
||||||
res = append(res, struct {
|
res = append(res, struct {
|
||||||
UID string
|
UID string
|
||||||
Username string
|
Username string
|
||||||
}{UID: u.UID, Username: u.Username})
|
Exp int
|
||||||
|
}{UID: strconv.FormatUint(uint64(u.UID), 10), Username: u.Username, Exp: u.Exp})
|
||||||
}
|
}
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
@ -230,6 +232,13 @@ func (r *CommentRepo) GetUserUIDByID(userID uint) (string, error) {
|
|||||||
return strconv.FormatUint(uint64(uid), 10), nil
|
return strconv.FormatUint(uint64(uid), 10), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUsernameByID 通过 userID 获取用户名(用于通知内容)
|
||||||
|
func (r *CommentRepo) GetUsernameByID(userID uint) (string, error) {
|
||||||
|
var username string
|
||||||
|
err := r.db.Table("users").Select("username").Where("uid = ?", userID).Scan(&username).Error
|
||||||
|
return username, err
|
||||||
|
}
|
||||||
|
|
||||||
// FindPostIDByComment 查询评论所属的文章ID
|
// FindPostIDByComment 查询评论所属的文章ID
|
||||||
func (r *CommentRepo) FindPostIDByComment(commentID uint) (uint, error) {
|
func (r *CommentRepo) FindPostIDByComment(commentID uint) (uint, error) {
|
||||||
var postID uint
|
var postID uint
|
||||||
|
|||||||
@ -28,7 +28,7 @@ func NewCommentService(repo commentStore, notifier commentNotifier) *CommentServ
|
|||||||
// CreateRoot 创建顶级评论
|
// CreateRoot 创建顶级评论
|
||||||
func (s *CommentService) CreateRoot(userID uint, postID uint, body string) (*model.Comment, error) {
|
func (s *CommentService) CreateRoot(userID uint, postID uint, body string) (*model.Comment, error) {
|
||||||
if strings.TrimSpace(body) == "" {
|
if strings.TrimSpace(body) == "" {
|
||||||
return nil, errors.New("评论内容不能为空")
|
return nil, common.ErrCommentEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
postAuthorID, err := s.repo.FindPostAuthorID(postID)
|
postAuthorID, err := s.repo.FindPostAuthorID(postID)
|
||||||
@ -60,11 +60,14 @@ func (s *CommentService) CreateRoot(userID uint, postID uint, body string) (*mod
|
|||||||
|
|
||||||
// 通知文章作者(评论者 ≠ 作者),RelatedID 存 postID 以便消息页生成跳转链接
|
// 通知文章作者(评论者 ≠ 作者),RelatedID 存 postID 以便消息页生成跳转链接
|
||||||
if s.notifier != nil && userID != postAuthorID {
|
if s.notifier != nil && userID != postAuthorID {
|
||||||
commenterUID, _ := s.repo.GetUserUIDByID(userID)
|
commenterName, _ := s.repo.GetUsernameByID(userID)
|
||||||
|
if commenterName == "" {
|
||||||
|
commenterName = "用户"
|
||||||
|
}
|
||||||
s.notifier.Create(postAuthorID, model.NotifyComment,
|
s.notifier.Create(postAuthorID, model.NotifyComment,
|
||||||
"新评论",
|
"新评论",
|
||||||
fmt.Sprintf("%s 评论了你的文章", commenterUID),
|
fmt.Sprintf("%s 评论了你的文章", commenterName),
|
||||||
&comment.PostID)
|
&comment.PostID, &comment.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return comment, nil
|
return comment, nil
|
||||||
@ -73,13 +76,13 @@ func (s *CommentService) CreateRoot(userID uint, postID uint, body string) (*mod
|
|||||||
// CreateReply 创建回复
|
// CreateReply 创建回复
|
||||||
func (s *CommentService) CreateReply(userID uint, parentID uint, body string) (*model.Comment, error) {
|
func (s *CommentService) CreateReply(userID uint, parentID uint, body string) (*model.Comment, error) {
|
||||||
if strings.TrimSpace(body) == "" {
|
if strings.TrimSpace(body) == "" {
|
||||||
return nil, errors.New("回复内容不能为空")
|
return nil, common.ErrCommentEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
parent, err := s.repo.FindByID(parentID)
|
parent, err := s.repo.FindByID(parentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, errors.New("父评论不存在")
|
return nil, common.ErrCommentNotFound
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("查询父评论失败: %w", err)
|
return nil, fmt.Errorf("查询父评论失败: %w", err)
|
||||||
}
|
}
|
||||||
@ -108,22 +111,25 @@ func (s *CommentService) CreateReply(userID uint, parentID uint, body string) (*
|
|||||||
|
|
||||||
// 通知被回复者(不自己回复自己),RelatedID 存 postID 以便消息页生成跳转链接
|
// 通知被回复者(不自己回复自己),RelatedID 存 postID 以便消息页生成跳转链接
|
||||||
if s.notifier != nil && parent.UserID != userID {
|
if s.notifier != nil && parent.UserID != userID {
|
||||||
commenterUID, _ := s.repo.GetUserUIDByID(userID)
|
commenterName, _ := s.repo.GetUsernameByID(userID)
|
||||||
|
if commenterName == "" {
|
||||||
|
commenterName = "用户"
|
||||||
|
}
|
||||||
s.notifier.Create(parent.UserID, model.NotifyCommentReply,
|
s.notifier.Create(parent.UserID, model.NotifyCommentReply,
|
||||||
"新回复",
|
"新回复",
|
||||||
fmt.Sprintf("%s 回复了你的评论", commenterUID),
|
fmt.Sprintf("%s 回复了你的评论", commenterName),
|
||||||
&parent.PostID)
|
&parent.PostID, &comment.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return comment, nil
|
return comment, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete 软删除评论
|
// Delete 软删除评论(requesterID 为请求者,isAdmin 表示是否为管理员)
|
||||||
func (s *CommentService) Delete(commentID uint) error {
|
func (s *CommentService) Delete(commentID uint, requesterID uint, isAdmin bool) error {
|
||||||
comment, err := s.repo.FindByID(commentID)
|
comment, err := s.repo.FindByID(commentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return errors.New("评论不存在")
|
return common.ErrCommentNotFound
|
||||||
}
|
}
|
||||||
return fmt.Errorf("查询评论失败: %w", err)
|
return fmt.Errorf("查询评论失败: %w", err)
|
||||||
}
|
}
|
||||||
@ -132,6 +138,11 @@ func (s *CommentService) Delete(commentID uint) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 权限检查:仅本人或管理员可删除
|
||||||
|
if !isAdmin && comment.UserID != requesterID {
|
||||||
|
return common.ErrPermissionDenied
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.repo.SoftDelete(commentID); err != nil {
|
if err := s.repo.SoftDelete(commentID); err != nil {
|
||||||
return fmt.Errorf("删除评论失败: %w", err)
|
return fmt.Errorf("删除评论失败: %w", err)
|
||||||
}
|
}
|
||||||
@ -182,7 +193,7 @@ func (s *CommentService) SearchUsers(keyword string) ([]model.UserSearchResult,
|
|||||||
users[i] = model.UserSearchResult{
|
users[i] = model.UserSearchResult{
|
||||||
UID: row.UID,
|
UID: row.UID,
|
||||||
Username: row.Username,
|
Username: row.Username,
|
||||||
Level: 0,
|
Level: model.GetLevelByExp(row.Exp),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return users, nil
|
return users, nil
|
||||||
|
|||||||
@ -84,7 +84,7 @@ func (s *LevelService) AddExp(userID uint, delta int) (newExp int, levelUp bool,
|
|||||||
_ = s.notifSvc.Create(userID, model.NotifyLevelUp,
|
_ = s.notifSvc.Create(userID, model.NotifyLevelUp,
|
||||||
"等级提升",
|
"等级提升",
|
||||||
fmt.Sprintf("恭喜您!您的等级已提升至 %s", model.GetLevelName(newLevel)),
|
fmt.Sprintf("恭喜您!您的等级已提升至 %s", model.GetLevelName(newLevel)),
|
||||||
nil)
|
nil, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,7 +26,7 @@ func NewNotificationService(repo notifRepo) *NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create 创建消息
|
// Create 创建消息
|
||||||
func (s *NotificationService) Create(userID uint, notifyType, title, content string, relatedID *uint) error {
|
func (s *NotificationService) Create(userID uint, notifyType, title, content string, relatedID, commentID *uint) error {
|
||||||
n := &model.Notification{
|
n := &model.Notification{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
NotifyType: notifyType,
|
NotifyType: notifyType,
|
||||||
@ -34,6 +34,7 @@ func (s *NotificationService) Create(userID uint, notifyType, title, content str
|
|||||||
Content: content,
|
Content: content,
|
||||||
IsRead: false,
|
IsRead: false,
|
||||||
RelatedID: relatedID,
|
RelatedID: relatedID,
|
||||||
|
CommentID: commentID,
|
||||||
}
|
}
|
||||||
return s.repo.Create(n)
|
return s.repo.Create(n)
|
||||||
}
|
}
|
||||||
@ -92,7 +93,7 @@ func (s *NotificationService) NotifyAuditApproved(userID uint, auditType string,
|
|||||||
_ = s.Create(userID, model.NotifyAuditApproved,
|
_ = s.Create(userID, model.NotifyAuditApproved,
|
||||||
"审核已通过",
|
"审核已通过",
|
||||||
"你的"+typeName+"修改已通过审核",
|
"你的"+typeName+"修改已通过审核",
|
||||||
&relatedID)
|
&relatedID, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyAuditRejected 审核拒绝通知
|
// NotifyAuditRejected 审核拒绝通知
|
||||||
@ -108,14 +109,14 @@ func (s *NotificationService) NotifyAuditRejected(userID uint, auditType, reason
|
|||||||
_ = s.Create(userID, model.NotifyAuditRejected,
|
_ = s.Create(userID, model.NotifyAuditRejected,
|
||||||
"审核未通过",
|
"审核未通过",
|
||||||
content,
|
content,
|
||||||
&relatedID)
|
&relatedID, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyPostApproved 稿件审核通过通知
|
// NotifyPostApproved 稿件审核通过通知
|
||||||
func (s *NotificationService) NotifyPostApproved(userID uint, postTitle string, postID uint) {
|
func (s *NotificationService) NotifyPostApproved(userID uint, postTitle string, postID uint) {
|
||||||
title := "稿件审核通过"
|
title := "稿件审核通过"
|
||||||
content := "你的稿件《" + postTitle + "》已通过审核,现已公开发布"
|
content := "你的稿件《" + postTitle + "》已通过审核,现已公开发布"
|
||||||
_ = s.Create(userID, model.NotifyPostApproved, title, content, &postID)
|
_ = s.Create(userID, model.NotifyPostApproved, title, content, &postID, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyPostRejected 稿件审核拒绝通知
|
// NotifyPostRejected 稿件审核拒绝通知
|
||||||
@ -125,5 +126,5 @@ func (s *NotificationService) NotifyPostRejected(userID uint, postTitle, reason
|
|||||||
if reason != "" {
|
if reason != "" {
|
||||||
content += ",理由:" + reason
|
content += ",理由:" + reason
|
||||||
}
|
}
|
||||||
_ = s.Create(userID, model.NotifyPostRejected, title, content, &postID)
|
_ = s.Create(userID, model.NotifyPostRejected, title, content, &postID, nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -86,7 +86,7 @@ type taskStore interface {
|
|||||||
|
|
||||||
// levelNotifier 等级升级通知接口
|
// levelNotifier 等级升级通知接口
|
||||||
type levelNotifier interface {
|
type levelNotifier interface {
|
||||||
Create(userID uint, notifyType, title, content string, relatedID *uint) error
|
Create(userID uint, notifyType, title, content string, relatedID, commentID *uint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnergyStore EnergyService 所需的完整仓储接口
|
// EnergyStore EnergyService 所需的完整仓储接口
|
||||||
@ -113,7 +113,7 @@ type energyStore = EnergyStore
|
|||||||
|
|
||||||
// energyNotifier EnergyService 所需的通知接口(ISP)
|
// energyNotifier EnergyService 所需的通知接口(ISP)
|
||||||
type energyNotifier interface {
|
type energyNotifier interface {
|
||||||
Create(userID uint, notifyType, title, content string, relatedID *uint) error
|
Create(userID uint, notifyType, title, content string, relatedID, commentID *uint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// commentStore CommentService 所需的最小仓储接口(ISP)
|
// commentStore CommentService 所需的最小仓储接口(ISP)
|
||||||
@ -132,12 +132,14 @@ type commentStore interface {
|
|||||||
SearchUsersByLevel(keyword string, minLevel, limit int) ([]struct {
|
SearchUsersByLevel(keyword string, minLevel, limit int) ([]struct {
|
||||||
UID string
|
UID string
|
||||||
Username string
|
Username string
|
||||||
|
Exp int
|
||||||
}, error)
|
}, error)
|
||||||
GetUserUIDByID(userID uint) (string, error)
|
GetUserUIDByID(userID uint) (string, error)
|
||||||
|
GetUsernameByID(userID uint) (string, error)
|
||||||
ListAllComments(keyword string, showDeleted bool, offset, limit int) ([]model.AdminCommentRow, int64, error)
|
ListAllComments(keyword string, showDeleted bool, offset, limit int) ([]model.AdminCommentRow, int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// commentNotifier CommentService 所需的通知接口(ISP)
|
// commentNotifier CommentService 所需的通知接口(ISP)
|
||||||
type commentNotifier interface {
|
type commentNotifier interface {
|
||||||
Create(userID uint, notifyType, title, content string, relatedID *uint) error
|
Create(userID uint, notifyType, title, content string, relatedID, commentID *uint) error
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,7 +42,11 @@
|
|||||||
{{if or (eq $m.NotifyType "post_approved") (eq $m.NotifyType "post_rejected")}}
|
{{if or (eq $m.NotifyType "post_approved") (eq $m.NotifyType "post_rejected")}}
|
||||||
{{$link = printf "/posts/%d" (derefUint $m.RelatedID)}}
|
{{$link = printf "/posts/%d" (derefUint $m.RelatedID)}}
|
||||||
{{else if or (eq $m.NotifyType "comment") (eq $m.NotifyType "comment_reply")}}
|
{{else if or (eq $m.NotifyType "comment") (eq $m.NotifyType "comment_reply")}}
|
||||||
{{$link = printf "/posts/%d#comments" (derefUint $m.RelatedID)}}
|
{{if $m.CommentID}}
|
||||||
|
{{$link = printf "/posts/%d#comment-%d" (derefUint $m.RelatedID) (derefUint $m.CommentID)}}
|
||||||
|
{{else}}
|
||||||
|
{{$link = printf "/posts/%d#comments" (derefUint $m.RelatedID)}}
|
||||||
|
{{end}}
|
||||||
{{else if or (eq $m.NotifyType "audit_approved") (eq $m.NotifyType "audit_rejected")}}
|
{{else if or (eq $m.NotifyType "audit_approved") (eq $m.NotifyType "audit_rejected")}}
|
||||||
{{$link = "/settings"}}
|
{{$link = "/settings"}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@ -703,8 +703,55 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====== Hash 定位:从消息通知跳转时滚动到评论区 ======
|
// ====== 评论高亮 + 已删除评论Toast ======
|
||||||
if (window.location.hash === '#comments') {
|
function showToast(msg) {
|
||||||
|
var toast = document.createElement('div');
|
||||||
|
toast.className = 'toast';
|
||||||
|
toast.textContent = msg;
|
||||||
|
toast.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.85);color:#fff;padding:10px 24px;border-radius:6px;font-size:14px;z-index:9999;animation:fadeIn 0.3s ease';
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
setTimeout(function() {
|
||||||
|
toast.style.opacity = '0';
|
||||||
|
toast.style.transition = 'opacity 0.3s';
|
||||||
|
setTimeout(function() { toast.remove(); }, 300);
|
||||||
|
}, 2500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightComment(commentId) {
|
||||||
|
var card = commentsList.querySelector('.comment-card[data-id="' + commentId + '"]') ||
|
||||||
|
commentsList.querySelector('.comment-reply-card[data-id="' + commentId + '"]');
|
||||||
|
if (card) {
|
||||||
|
// 置顶:移动卡片到列表顶部
|
||||||
|
commentsList.insertBefore(card, commentsList.firstChild);
|
||||||
|
card.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
card.style.background = '#fff8e1';
|
||||||
|
card.style.transition = 'background 1.5s';
|
||||||
|
setTimeout(function() { card.style.background = ''; }, 2000);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 hash(支持 #comment-{id}、#comments 以及回退)
|
||||||
|
var hash = window.location.hash;
|
||||||
|
var commentMatch = hash.match(/^#comment-(\d+)$/);
|
||||||
|
if (commentMatch) {
|
||||||
|
var targetCommentId = commentMatch[1];
|
||||||
|
// 等待评论加载完成后查找
|
||||||
|
var checkInterval = setInterval(function() {
|
||||||
|
if (highlightComment(targetCommentId)) {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
// 3秒后超时(评论可能已被删除)
|
||||||
|
setTimeout(function() {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
if (!commentsList.querySelector('.comment-card[data-id="' + targetCommentId + '"]') &&
|
||||||
|
!commentsList.querySelector('.comment-reply-card[data-id="' + targetCommentId + '"]')) {
|
||||||
|
showToast('该评论已不存在');
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
} else if (hash === '#comments') {
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
var section = document.getElementById('comments');
|
var section = document.getElementById('comments');
|
||||||
if (section) {
|
if (section) {
|
||||||
|
|||||||
Reference in New Issue
Block a user