feat: 优化首页帖子列表并新增右侧栏最新注册组件
首页列表精简 meta 与统计展示,板块色标前移并淡化;有回复时显示最后回复人。右侧栏新增最新注册(4 列头像网格),友链改为标签块并排换行。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,16 +9,16 @@ func TestNormalizeAsideWidgetsPreservesOrder(t *testing.T) {
|
||||
{ID: AsideWidgetRecentComments, Enabled: false},
|
||||
}
|
||||
out := NormalizeAsideWidgets(in)
|
||||
if len(out) != 3 {
|
||||
t.Fatalf("want 3 widgets, got %d", len(out))
|
||||
if len(out) != 4 {
|
||||
t.Fatalf("want 4 widgets, got %d", len(out))
|
||||
}
|
||||
want := []string{AsideWidgetFriendLinks, AsideWidgetTagCloud, AsideWidgetRecentComments}
|
||||
want := []string{AsideWidgetFriendLinks, AsideWidgetTagCloud, AsideWidgetRecentComments, AsideWidgetRecentUsers}
|
||||
for i, id := range want {
|
||||
if out[i].ID != id {
|
||||
t.Fatalf("index %d: want %s, got %s", i, id, out[i].ID)
|
||||
}
|
||||
}
|
||||
if !out[0].Enabled || !out[1].Enabled || out[2].Enabled {
|
||||
if !out[0].Enabled || !out[1].Enabled || out[2].Enabled || out[3].Enabled {
|
||||
t.Fatalf("enabled flags mismatch: %+v", out)
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ func TestAsideBoolsFromWidgets(t *testing.T) {
|
||||
{ID: AsideWidgetRecentComments, Enabled: true},
|
||||
{ID: AsideWidgetFriendLinks, Enabled: false},
|
||||
{ID: AsideWidgetTagCloud, Enabled: true},
|
||||
{ID: AsideWidgetRecentUsers, Enabled: true},
|
||||
}
|
||||
bools := asideBoolsFromWidgets(widgets)
|
||||
if !bools.tagCloud || !bools.recentComments || bools.friendLinks {
|
||||
|
||||
@@ -56,8 +56,16 @@ type PostListQuery struct {
|
||||
// PostListItem 帖子列表项(含评论数等扩展字段)
|
||||
type PostListItem struct {
|
||||
model.Post
|
||||
CommentCount int `json:"comment_count"`
|
||||
LastReplyAt *time.Time `json:"last_reply_at,omitempty"`
|
||||
CommentCount int `json:"comment_count"`
|
||||
LastReplyAt *time.Time `json:"last_reply_at,omitempty"`
|
||||
LastReplyUser *model.User `json:"last_reply_user,omitempty"`
|
||||
LastReplyGuestNick string `json:"last_reply_guest_nick,omitempty"`
|
||||
}
|
||||
|
||||
type lastReplyInfo struct {
|
||||
At *time.Time
|
||||
User *model.User
|
||||
GuestNick string
|
||||
}
|
||||
|
||||
func (s *PostService) ListItems(q PostListQuery) ([]PostListItem, int64, error) {
|
||||
@@ -73,13 +81,16 @@ func (s *PostService) ListItems(q PostListQuery) ([]PostListItem, int64, error)
|
||||
ids[i] = p.ID
|
||||
}
|
||||
countMap := s.commentCountMap(ids)
|
||||
replyMap := s.lastReplyMap(ids)
|
||||
replyMap := s.lastReplyInfoMap(ids)
|
||||
items := make([]PostListItem, len(posts))
|
||||
for i, p := range posts {
|
||||
info := replyMap[p.ID]
|
||||
items[i] = PostListItem{
|
||||
Post: p,
|
||||
CommentCount: countMap[p.ID],
|
||||
LastReplyAt: replyMap[p.ID],
|
||||
Post: p,
|
||||
CommentCount: countMap[p.ID],
|
||||
LastReplyAt: info.At,
|
||||
LastReplyUser: info.User,
|
||||
LastReplyGuestNick: info.GuestNick,
|
||||
}
|
||||
}
|
||||
return items, total, nil
|
||||
@@ -101,44 +112,50 @@ func (s *PostService) commentCountMap(postIDs []uint) map[uint]int {
|
||||
return m
|
||||
}
|
||||
|
||||
func (s *PostService) lastReplyMap(postIDs []uint) map[uint]*time.Time {
|
||||
type row struct {
|
||||
PostID uint
|
||||
LastReply string
|
||||
func (s *PostService) lastReplyInfoMap(postIDs []uint) map[uint]lastReplyInfo {
|
||||
m := make(map[uint]lastReplyInfo, len(postIDs))
|
||||
if len(postIDs) == 0 {
|
||||
return m
|
||||
}
|
||||
var rows []row
|
||||
type idRow struct {
|
||||
PostID uint
|
||||
MaxID uint
|
||||
}
|
||||
var idRows []idRow
|
||||
model.DB.Model(&model.Comment{}).
|
||||
Select("post_id, MAX(created_at) as last_reply").
|
||||
Select("post_id, MAX(id) as max_id").
|
||||
Where("post_id IN ? AND status = ?", postIDs, model.ContentStatusPublished).
|
||||
Group("post_id").
|
||||
Scan(&rows)
|
||||
m := make(map[uint]*time.Time, len(rows))
|
||||
for _, r := range rows {
|
||||
if t, ok := parseSQLiteTime(r.LastReply); ok {
|
||||
m[r.PostID] = &t
|
||||
Scan(&idRows)
|
||||
if len(idRows) == 0 {
|
||||
return m
|
||||
}
|
||||
commentIDs := make([]uint, len(idRows))
|
||||
for i, r := range idRows {
|
||||
commentIDs[i] = r.MaxID
|
||||
}
|
||||
var comments []model.Comment
|
||||
if err := model.DB.Preload("User").Where("id IN ?", commentIDs).Find(&comments).Error; err != nil {
|
||||
return m
|
||||
}
|
||||
for i := range comments {
|
||||
c := &comments[i]
|
||||
info := lastReplyInfo{At: &c.CreatedAt}
|
||||
if c.UserID > 0 && c.User.ID > 0 {
|
||||
u := c.User
|
||||
info.User = &u
|
||||
} else {
|
||||
nick := strings.TrimSpace(c.GuestNick)
|
||||
if nick == "" {
|
||||
nick = "游客"
|
||||
}
|
||||
info.GuestNick = nick
|
||||
}
|
||||
m[c.PostID] = info
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// parseSQLiteTime 解析 SQLite 聚合查询返回的时间字符串
|
||||
func parseSQLiteTime(s string) (time.Time, bool) {
|
||||
if s == "" {
|
||||
return time.Time{}, false
|
||||
}
|
||||
for _, layout := range []string{
|
||||
"2006-01-02 15:04:05.999999999-07:00",
|
||||
time.RFC3339Nano,
|
||||
time.RFC3339,
|
||||
"2006-01-02 15:04:05",
|
||||
} {
|
||||
if t, err := time.Parse(layout, s); err == nil {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
// HotPosts 近期活跃讨论(近 7 日有公开回复,按最后回复时间倒序)
|
||||
func (s *PostService) HotPosts(limit int) ([]PostListItem, error) {
|
||||
if limit <= 0 {
|
||||
@@ -170,13 +187,16 @@ func (s *PostService) HotPosts(limit int) ([]PostListItem, error) {
|
||||
ids[i] = p.ID
|
||||
}
|
||||
countMap := s.commentCountMap(ids)
|
||||
replyMap := s.lastReplyMap(ids)
|
||||
replyMap := s.lastReplyInfoMap(ids)
|
||||
items := make([]PostListItem, len(posts))
|
||||
for i, p := range posts {
|
||||
info := replyMap[p.ID]
|
||||
items[i] = PostListItem{
|
||||
Post: p,
|
||||
CommentCount: countMap[p.ID],
|
||||
LastReplyAt: replyMap[p.ID],
|
||||
Post: p,
|
||||
CommentCount: countMap[p.ID],
|
||||
LastReplyAt: info.At,
|
||||
LastReplyUser: info.User,
|
||||
LastReplyGuestNick: info.GuestNick,
|
||||
}
|
||||
}
|
||||
return items, nil
|
||||
|
||||
@@ -144,12 +144,14 @@ type AsideWidget struct {
|
||||
const (
|
||||
AsideWidgetTagCloud = "tag_cloud"
|
||||
AsideWidgetRecentComments = "recent_comments"
|
||||
AsideWidgetRecentUsers = "recent_users"
|
||||
AsideWidgetFriendLinks = "friend_links"
|
||||
)
|
||||
|
||||
var asideWidgetDefaultOrder = []string{
|
||||
AsideWidgetTagCloud,
|
||||
AsideWidgetRecentComments,
|
||||
AsideWidgetRecentUsers,
|
||||
AsideWidgetFriendLinks,
|
||||
}
|
||||
|
||||
@@ -742,7 +744,7 @@ func asideBoolsFromWidgets(widgets []AsideWidget) asideWidgetBools {
|
||||
|
||||
func isValidAsideWidgetID(id string) bool {
|
||||
switch id {
|
||||
case AsideWidgetTagCloud, AsideWidgetRecentComments, AsideWidgetFriendLinks:
|
||||
case AsideWidgetTagCloud, AsideWidgetRecentComments, AsideWidgetRecentUsers, AsideWidgetFriendLinks:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -123,6 +123,44 @@ func (s *UserService) SearchUsersBrief(keyword string, limit int) ([]model.User,
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// RecentUserItem 右栏「最新注册」条目
|
||||
type RecentUserItem struct {
|
||||
ID uint `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// ListRecentRegistered 前台最新注册用户(排除封禁)
|
||||
func (s *UserService) ListRecentRegistered(limit int) ([]RecentUserItem, error) {
|
||||
if limit < 1 {
|
||||
limit = 8
|
||||
}
|
||||
var users []model.User
|
||||
err := model.DB.Select("id", "username", "nickname", "avatar", "created_at").
|
||||
Where("banned = ?", false).
|
||||
Order("created_at DESC, id DESC").
|
||||
Limit(limit).
|
||||
Find(&users).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]RecentUserItem, 0, len(users))
|
||||
for _, u := range users {
|
||||
nick := strings.TrimSpace(u.Nickname)
|
||||
if nick == "" {
|
||||
nick = u.Username
|
||||
}
|
||||
out = append(out, RecentUserItem{
|
||||
ID: u.ID,
|
||||
Nickname: nick,
|
||||
Avatar: u.Avatar,
|
||||
CreatedAt: u.CreatedAt.UTC().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// UpdateNickname 修改昵称
|
||||
func (s *UserService) UpdateNickname(userID uint, nickname string) error {
|
||||
nickname = strings.TrimSpace(nickname)
|
||||
|
||||
Reference in New Issue
Block a user