feat: @提及功能优化 + 默认头像改用SVG图标
- 评论区有效@提及渲染为蓝色链接跳转/space/{uid},无效@保持纯文本
- 输入@触发悬浮窗,根据输入实时搜索用户(含头像+等级)
- 无匹配时显示未搜索到用户,选中后插入为@用户名 格式(尾部空格)
- 悬浮窗定位于输入光标上方,不遮挡输入内容
- 后端Comment新增Mentions字段,批量加载提及映射供前端判断
- SearchUsers/parseMentions 统一限制LV2+用户
- 所有默认头像由首字符文字改为统一SVG人形图标(nav/home/space/mention等7处)
This commit is contained in:
@ -25,6 +25,9 @@ type Comment struct {
|
||||
ReplyToName string `gorm:"-:migration;<-:false" json:"reply_to_name,omitempty"`
|
||||
// RepliesCount 回复数(非DB字段,查询填充)
|
||||
RepliesCount int `gorm:"-:migration;<-:false" json:"replies_count,omitempty"`
|
||||
// Mentions 有效@提及映射 username -> uid(非DB字段,批量查询填充)
|
||||
// 前端据此判断 @username 是否为有效提及并生成链接
|
||||
Mentions map[string]uint `gorm:"-" json:"mentions,omitempty"`
|
||||
}
|
||||
|
||||
// CommentMention @提及映射(绑定UID,不是username,支持改名后自动更新显示名)
|
||||
@ -39,6 +42,7 @@ type UserSearchResult struct {
|
||||
UID uint `json:"uid"`
|
||||
Username string `json:"username"`
|
||||
Level int `json:"level"`
|
||||
Avatar string `json:"avatar"`
|
||||
}
|
||||
|
||||
// AdminCommentRow 后台评论列表行
|
||||
|
||||
@ -17,15 +17,62 @@ func (r *CommentRepo) FindMentionsByComment(commentID uint) ([]model.CommentMent
|
||||
return list, err
|
||||
}
|
||||
|
||||
// SearchUsersByLevel 搜索指定等级及以上用户(用于@艾特),返回 uid/username/exp
|
||||
// LoadMentionsForComments 批量加载多条评论的有效@提及映射(username -> uid)
|
||||
// 传入的 comments 会被原地修改,填充 Mentions 字段
|
||||
func (r *CommentRepo) LoadMentionsForComments(comments []model.Comment) error {
|
||||
if len(comments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
commentIDs := make([]uint, len(comments))
|
||||
for i, c := range comments {
|
||||
commentIDs[i] = c.ID
|
||||
}
|
||||
|
||||
// 查询 comment_mentions JOIN users,拿到 username 和 uid
|
||||
type mentionRow struct {
|
||||
CommentID uint
|
||||
UID uint
|
||||
Username string
|
||||
}
|
||||
var rows []mentionRow
|
||||
err := r.db.Table("comment_mentions").
|
||||
Select("comment_mentions.comment_id, comment_mentions.uid, users.username").
|
||||
Joins("LEFT JOIN users ON users.id = comment_mentions.uid").
|
||||
Where("comment_mentions.comment_id IN ?", commentIDs).
|
||||
Find(&rows).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 按 comment_id 分组
|
||||
mentionMap := make(map[uint]map[string]uint)
|
||||
for _, row := range rows {
|
||||
if mentionMap[row.CommentID] == nil {
|
||||
mentionMap[row.CommentID] = make(map[string]uint)
|
||||
}
|
||||
mentionMap[row.CommentID][row.Username] = row.UID
|
||||
}
|
||||
|
||||
for i := range comments {
|
||||
if m, ok := mentionMap[comments[i].ID]; ok {
|
||||
comments[i].Mentions = m
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchUsersByLevel 搜索指定等级及以上用户(用于@艾特),返回 uid/username/avatar/exp
|
||||
func (r *CommentRepo) SearchUsersByLevel(keyword string, minLevel int, limit int) ([]struct {
|
||||
UID uint
|
||||
Username string
|
||||
Avatar string
|
||||
Exp int
|
||||
}, error) {
|
||||
type result struct {
|
||||
UID uint
|
||||
Username string
|
||||
Avatar string
|
||||
Exp int
|
||||
}
|
||||
var list []result
|
||||
@ -33,7 +80,7 @@ func (r *CommentRepo) SearchUsersByLevel(keyword string, minLevel int, limit int
|
||||
threshold := model.LevelThresholds[minLevel]
|
||||
|
||||
err := r.db.Table("users").
|
||||
Select("id, username, exp").
|
||||
Select("id, username, avatar, exp").
|
||||
Where("username LIKE ? AND exp >= ? AND deleted_at IS NULL", "%"+keyword+"%", threshold).
|
||||
Order("exp DESC").
|
||||
Limit(limit).
|
||||
@ -42,14 +89,16 @@ func (r *CommentRepo) SearchUsersByLevel(keyword string, minLevel int, limit int
|
||||
var res []struct {
|
||||
UID uint
|
||||
Username string
|
||||
Avatar string
|
||||
Exp int
|
||||
}
|
||||
for _, u := range list {
|
||||
res = append(res, struct {
|
||||
UID uint
|
||||
Username string
|
||||
Avatar string
|
||||
Exp int
|
||||
}{UID: u.UID, Username: u.Username, Exp: u.Exp})
|
||||
}{UID: u.UID, Username: u.Username, Avatar: u.Avatar, Exp: u.Exp})
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
@ -152,7 +152,15 @@ func (s *CommentService) Delete(commentID uint, requesterID uint, isAdmin bool)
|
||||
func (s *CommentService) ListRootComments(postID uint, page, pageSize int) ([]model.Comment, int64, error) {
|
||||
p := common.Pagination{Page: page, PageSize: pageSize}
|
||||
p.DefaultPagination()
|
||||
return s.repo.FindRootComments(postID, p.Offset(), p.PageSize)
|
||||
comments, total, err := s.repo.FindRootComments(postID, p.Offset(), p.PageSize)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
// 批量加载有效@提及映射,前端据此决定是否为链接
|
||||
if len(comments) > 0 {
|
||||
_ = s.repo.LoadMentionsForComments(comments)
|
||||
}
|
||||
return comments, total, nil
|
||||
}
|
||||
|
||||
// ListReplies 获取某条顶级评论的全部回复
|
||||
@ -164,6 +172,10 @@ func (s *CommentService) ListReplies(rootID uint) ([]model.Comment, error) {
|
||||
if list == nil {
|
||||
list = []model.Comment{}
|
||||
}
|
||||
// 批量加载有效@提及映射
|
||||
if len(list) > 0 {
|
||||
_ = s.repo.LoadMentionsForComments(list)
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
@ -178,9 +190,9 @@ func (s *CommentService) ListAllComments(keyword string, showDeleted bool, page,
|
||||
return rows, total, err
|
||||
}
|
||||
|
||||
// SearchUsers 搜索LV3+用户(用于@艾特)
|
||||
// SearchUsers 搜索LV2+用户(用于@艾特悬浮窗)
|
||||
func (s *CommentService) SearchUsers(keyword string) ([]model.UserSearchResult, error) {
|
||||
rows, err := s.repo.SearchUsersByLevel(keyword, 3, 10)
|
||||
rows, err := s.repo.SearchUsersByLevel(keyword, 2, 10)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("搜索用户失败: %w", err)
|
||||
}
|
||||
@ -191,6 +203,7 @@ func (s *CommentService) SearchUsers(keyword string) ([]model.UserSearchResult,
|
||||
UID: row.UID,
|
||||
Username: row.Username,
|
||||
Level: model.GetLevelByExp(row.Exp),
|
||||
Avatar: row.Avatar,
|
||||
}
|
||||
}
|
||||
return users, nil
|
||||
@ -206,7 +219,7 @@ func (s *CommentService) parseMentions(commentID uint, body string) {
|
||||
continue
|
||||
}
|
||||
seen[username] = true
|
||||
rows, _ := s.repo.SearchUsersByLevel(username, 0, 1)
|
||||
rows, _ := s.repo.SearchUsersByLevel(username, 2, 1)
|
||||
if len(rows) > 0 {
|
||||
_ = s.repo.CreateMention(&model.CommentMention{
|
||||
CommentID: commentID,
|
||||
|
||||
@ -158,9 +158,11 @@ type commentStore interface {
|
||||
SearchUsersByLevel(keyword string, minLevel, limit int) ([]struct {
|
||||
UID uint
|
||||
Username string
|
||||
Avatar string
|
||||
Exp int
|
||||
}, error)
|
||||
GetUsernameByID(userID uint) (string, error)
|
||||
LoadMentionsForComments(comments []model.Comment) error
|
||||
ListAllComments(keyword string, showDeleted bool, offset, limit int) ([]model.AdminCommentRow, int64, error)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user