diff --git a/internal/controller/space_controller.go b/internal/controller/space_controller.go index 342f94b..7dfeca5 100644 --- a/internal/controller/space_controller.go +++ b/internal/controller/space_controller.go @@ -5,6 +5,8 @@ import ( "strconv" "metazone.cc/metalab/internal/common" + "metazone.cc/metalab/internal/model" + "metazone.cc/metalab/internal/service" "github.com/gin-gonic/gin" ) @@ -12,6 +14,7 @@ import ( // SpaceController 用户空间控制器 type SpaceController struct { spaceService spaceUseCase + followSvc followUseCase } // NewSpaceController 构造函数 @@ -19,6 +22,11 @@ func NewSpaceController(spaceService spaceUseCase) *SpaceController { return &SpaceController{spaceService: spaceService} } +// WithFollowService 注入关注服务(可选依赖) +func (ctrl *SpaceController) WithFollowService(svc followUseCase) { + ctrl.followSvc = svc +} + // MySpace 自己的空间(/space)— 需登录,重定向到 /space/{uid} func (ctrl *SpaceController) MySpace(c *gin.Context) { uid, _, ok := common.GetGinUser(c) @@ -83,22 +91,30 @@ func (ctrl *SpaceController) ShowSpace(c *gin.Context) { } // 检查是否是自己的空间 - isOwnSpace := false - if currentUID, _, ok := common.GetGinUser(c); ok { - isOwnSpace = currentUID == uint(uid) + currentUID, _, _ := common.GetGinUser(c) + isOwnSpace := currentUID == uint(uid) + + // 获取关注列表数据 + var followingResult *service.FollowListResult + if ctrl.followSvc != nil { + res, err := ctrl.followSvc.ListFollowing(uint(uid), currentUID, 1, 24) + if err != nil || res == nil { + res = &service.FollowListResult{Items: []model.UserFollow{}, Total: 0, Accessible: true} + } + followingResult = res } c.HTML(http.StatusOK, "space/index.html", common.BuildPageData(c, gin.H{ - "Title": spaceUser.Username + " 的空间", - "SpaceUser": spaceUser, - "Posts": posts, - "Total": total, - "Page": page, - "TotalPages": totalPages, - "PrevPage": prevPage, - "NextPage": nextPage, - "IsOwnSpace": isOwnSpace, - "ExtraCSS": "/static/css/space.css", - "ExtraCSS2": "/static/css/follow.css", + "Title": spaceUser.Username + " 的空间", + "SpaceUser": spaceUser, + "Posts": posts, + "Total": total, + "Page": page, + "TotalPages": totalPages, + "PrevPage": prevPage, + "NextPage": nextPage, + "IsOwnSpace": isOwnSpace, + "FollowingResult": followingResult, + "ExtraCSS": "/static/css/space.css", })) } diff --git a/internal/model/user_follow.go b/internal/model/user_follow.go index 19b109f..87ef291 100644 --- a/internal/model/user_follow.go +++ b/internal/model/user_follow.go @@ -11,4 +11,8 @@ type UserFollow struct { // 联表查询填充(非数据库字段) FollowerUsername string `gorm:"-:migration;<-:false;column:follower_username" json:"follower_username,omitempty"` FolloweeUsername string `gorm:"-:migration;<-:false;column:followee_username" json:"followee_username,omitempty"` + FollowerAvatar string `gorm:"-:migration;<-:false;column:follower_avatar" json:"follower_avatar,omitempty"` + FolloweeAvatar string `gorm:"-:migration;<-:false;column:followee_avatar" json:"followee_avatar,omitempty"` + FollowerBio string `gorm:"-:migration;<-:false;column:follower_bio" json:"follower_bio,omitempty"` + FolloweeBio string `gorm:"-:migration;<-:false;column:followee_bio" json:"followee_bio,omitempty"` } diff --git a/internal/repository/follow_repo.go b/internal/repository/follow_repo.go index 2031818..9bc615b 100644 --- a/internal/repository/follow_repo.go +++ b/internal/repository/follow_repo.go @@ -56,7 +56,7 @@ func (r *FollowRepo) CountFollowing(userID uint) (int64, error) { return count, err } -// ListFollowers 粉丝列表(分页,JOIN users 获取用户名) +// ListFollowers 粉丝列表(分页,JOIN users 获取用户名、头像、简介) func (r *FollowRepo) ListFollowers(userID uint, offset, limit int) ([]model.UserFollow, int64, error) { var total int64 if err := r.db.Model(&model.UserFollow{}).Where("followee_id = ?", userID).Count(&total).Error; err != nil { @@ -64,7 +64,7 @@ func (r *FollowRepo) ListFollowers(userID uint, offset, limit int) ([]model.User } var items []model.UserFollow err := r.db.Table("user_follows"). - Select("user_follows.*, u.username AS follower_username"). + Select("user_follows.*, u.username AS follower_username, u.avatar AS follower_avatar, u.bio AS follower_bio"). Joins("INNER JOIN users u ON u.uid = user_follows.follower_id"). Where("user_follows.followee_id = ?", userID). Order("user_follows.created_at DESC"). @@ -73,7 +73,7 @@ func (r *FollowRepo) ListFollowers(userID uint, offset, limit int) ([]model.User return items, total, err } -// ListFollowing 关注列表(分页,JOIN users 获取用户名) +// ListFollowing 关注列表(分页,JOIN users 获取用户名、头像、简介) func (r *FollowRepo) ListFollowing(userID uint, offset, limit int) ([]model.UserFollow, int64, error) { var total int64 if err := r.db.Model(&model.UserFollow{}).Where("follower_id = ?", userID).Count(&total).Error; err != nil { @@ -81,7 +81,7 @@ func (r *FollowRepo) ListFollowing(userID uint, offset, limit int) ([]model.User } var items []model.UserFollow err := r.db.Table("user_follows"). - Select("user_follows.*, u.username AS followee_username"). + Select("user_follows.*, u.username AS followee_username, u.avatar AS followee_avatar, u.bio AS followee_bio"). Joins("INNER JOIN users u ON u.uid = user_follows.followee_id"). Where("user_follows.follower_id = ?", userID). Order("user_follows.created_at DESC"). diff --git a/internal/router/deps_extra.go b/internal/router/deps_extra.go index d0252aa..080f7a8 100644 --- a/internal/router/deps_extra.go +++ b/internal/router/deps_extra.go @@ -85,6 +85,7 @@ func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSetting // 用户空间 spaceService := service.NewSpaceService(userRepo, postRepo) spaceCtrl := controller.NewSpaceController(spaceService) + spaceCtrl.WithFollowService(followService) // 收藏夹系统 favoriteRepo := repository.NewFavoriteRepo(db) diff --git a/templates/MetaLab-2026/html/space/index.html b/templates/MetaLab-2026/html/space/index.html index 10cdd4d..8733d13 100644 --- a/templates/MetaLab-2026/html/space/index.html +++ b/templates/MetaLab-2026/html/space/index.html @@ -12,86 +12,175 @@ {{else}}
- -
-
-
- {{if .SpaceUser.Avatar}} - {{.SpaceUser.Username}} - {{else}} -
{{slice .SpaceUser.Username 0 1}}
- {{end}} -
-
-

{{.SpaceUser.Username}}

-

{{if eq .SpaceUser.Status "banned"}}ⓘ 该账号已被封禁{{else if .SpaceUser.Bio}}{{.SpaceUser.Bio}}{{else}} {{end}}

-
- UID: {{.SpaceUser.ID}} - · - 加入于 {{.SpaceUser.CreatedAt.Format "2006-01-02"}} -
- - {{if not .IsOwnSpace}} - - {{end}} -
-
-
- -
- -
-
- {{.Total}} - 稿件 + + {{if .IsOwnSpace}} +
+ + + + +
+ +
+

全部关注

+ 设置
-
- -
-

稿件

+ {{if .FollowingResult}} + {{if .FollowingResult.Accessible}} - {{if eq (len .Posts) 0}} -
暂无稿件
+ {{if eq (len .FollowingResult.Items) 0}} +
+

暂未关注任何人,去发现有趣的创作者吧~

+
{{else}} -
- {{range .Posts}} - + +
{{end}}
{{end}} - - {{if gt .TotalPages 1}} - + + + {{else}} +
+ + + + +
+

全部关注

+ + {{if .FollowingResult}} + {{if .FollowingResult.Accessible}} + + {{if eq (len .FollowingResult.Items) 0}} +
+

Ta 暂未关注任何人

+
+ {{else}} +
+ {{range $i, $item := .FollowingResult.Items}} +
+ + {{if $item.FolloweeAvatar}} + {{$item.FolloweeUsername}} + {{else}} +
{{slice $item.FolloweeUsername 0 1}}
+ {{end}} +
+ + +
{{end}}
{{end}} -
+ + {{else}} + +
+
+ + + + + + + + + + +
+

Ta 关闭了展示关注列表~

+
+ {{end}} + {{else}} +
+

暂无数据

+
+ {{end}} +
+ {{end}} +
{{end}} @@ -99,13 +188,12 @@ {{if not .IsOwnSpace}} + diff --git a/templates/MetaLab-2026/static/css/follow.css b/templates/MetaLab-2026/static/css/follow.css index e29f963..d6e5414 100644 --- a/templates/MetaLab-2026/static/css/follow.css +++ b/templates/MetaLab-2026/static/css/follow.css @@ -1,79 +1,3 @@ -/* 关注按钮 */ -.follow-btn-wrapper { - display: flex; - align-items: center; - gap: 0.5rem; - margin-top: 0.5rem; -} - -.follow-btn { - display: inline-flex; - align-items: center; - gap: .35rem; - padding: .4rem 1rem; - border: 1.5px solid var(--color-accent); - border-radius: 6px; - font-size: .85rem; - font-weight: 600; - cursor: pointer; - transition: all .15s ease; - background: transparent; - color: var(--color-accent); -} - -.follow-btn:hover { - background: var(--color-accent); - color: #fff; -} - -.follow-btn.following { - background: var(--color-accent); - color: #fff; -} - -.follow-btn.following:hover { - background: var(--color-danger, #e74c3c); - border-color: var(--color-danger, #e74c3c); -} - -.follow-btn.mutual { - background: var(--color-accent); - color: #fff; -} - -.follow-btn.mutual:hover { - background: var(--color-danger, #e74c3c); - border-color: var(--color-danger, #e74c3c); -} - -.follow-btn.waiting { - opacity: .6; - pointer-events: none; -} - -/* 关注/粉丝链接 */ -.follow-links { - display: flex; - gap: 1rem; - margin-top: .35rem; -} - -.follow-links a { - color: var(--color-secondary); - font-size: .85rem; - text-decoration: none; - transition: color .15s; -} - -.follow-links a:hover { - color: var(--color-accent); -} - -.follow-links strong { - color: var(--color-text); - font-weight: 600; -} - /* 用户列表(粉丝/关注页) */ .follow-list-page { max-width: 700px; diff --git a/templates/MetaLab-2026/static/css/space.css b/templates/MetaLab-2026/static/css/space.css index 4f90998..dc87e70 100644 --- a/templates/MetaLab-2026/static/css/space.css +++ b/templates/MetaLab-2026/static/css/space.css @@ -1,6 +1,5 @@ /* ============================================================ - MetaLab Space Styles — 用户空间页面 - 参考 Bilibili 空间布局,轻量版 + MetaLab Space Styles — 卡片式关注列表布局 ============================================================ */ /* ---------- Error State ---------- */ @@ -40,210 +39,297 @@ } /* ============================================================ - Header — 用户信息横幅(B站风格) + Card Layout — 左右分栏(参考B站关注页) ============================================================ */ -.space-header { - background: linear-gradient(135deg, #e8f0f8 0%, #f0f4f8 100%); - border-bottom: 1px solid var(--color-border); - padding: 2rem 0; -} - -.space-header-inner { +.space-card-layout { display: flex; - align-items: center; + max-width: 1200px; + margin: 0 auto; + padding: 1.5rem 1rem; gap: 1.5rem; + min-height: calc(100vh - 160px); } -/* Avatar */ -.space-avatar { +/* ---------- Sidebar (左侧导航) ---------- */ +.space-sidebar { + width: 200px; flex-shrink: 0; } -.space-avatar img { - width: 80px; - height: 80px; - border-radius: 50%; - object-fit: cover; - border: 3px solid #fff; - box-shadow: 0 2px 8px rgba(0,0,0,.1); +.space-sidebar-title { + font-size: .95rem; + font-weight: 600; + color: var(--color-primary); + margin-bottom: .75rem; + padding-left: .25rem; } -.space-avatar-placeholder { - width: 80px; - height: 80px; +.space-sidebar-nav { + display: flex; + flex-direction: column; + gap: 2px; +} + +.space-sidebar-link { + display: flex; + align-items: center; + gap: .5rem; + padding: .65rem .75rem; + border-radius: var(--radius); + text-decoration: none; + color: var(--color-text); + font-size: .9rem; + transition: background-color .15s ease; +} + +.space-sidebar-link:hover { + background-color: #f5f5f5; +} + +.space-sidebar-link.active { + background-color: var(--color-accent); + color: #fff; + font-weight: 500; +} + +.space-sidebar-link.active:hover { + background-color: #2980b9; +} + +.space-sidebar-link.disabled { + opacity: .6; + pointer-events: none; +} + +.space-sidebar-icon { + flex-shrink: 0; + opacity: .8; +} + +.space-sidebar-link.active .space-sidebar-icon { + opacity: 1; +} + +.space-sidebar-count { + margin-left: auto; + font-size: .8rem; + opacity: .7; +} + +.space-sidebar-hint { + margin-top: 1rem; + padding: 0 .25rem; + font-size: .78rem; + color: var(--color-secondary); +} + +/* ---------- Main Content (右侧内容) ---------- */ +.space-main { + flex: 1; + min-width: 0; +} + +.space-main-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1.25rem; +} + +.space-main-title { + font-size: 1.35rem; + font-weight: 700; + color: var(--color-primary); +} + +.space-settings-link { + font-size: .85rem; + color: var(--color-accent); + text-decoration: none; + padding: .3rem .75rem; + border: 1px solid var(--color-border, #e5e7eb); + border-radius: var(--radius); + transition: all .15s; +} + +.space-settings-link:hover { + background-color: rgba(52, 152, 219, .05); + border-color: var(--color-accent); +} + +/* ============================================================ + User Card Grid — 用户卡片网格 + ============================================================ */ +.space-user-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; +} + +.space-user-card { + display: flex; + align-items: flex-start; + gap: .75rem; + padding: 1rem 1.1rem; + background: var(--color-surface, #fff); + border: 1px solid var(--color-border, #e5e7eb); + border-radius: var(--radius); + transition: box-shadow .2s ease, border-color .2s ease; +} + +.space-user-card:hover { + box-shadow: 0 2px 12px rgba(0, 0, 0, .06); + border-color: #ccc; +} + +/* Avatar */ +.space-user-avatar-wrap { + flex-shrink: 0; + text-decoration: none; + display: block; +} + +.space-user-avatar-img { + width: 48px; + height: 48px; + border-radius: 50%; + object-fit: cover; + display: block; +} + +.space-user-avatar-placeholder { + width: 48px; + height: 48px; border-radius: 50%; background: var(--color-accent); color: #fff; display: flex; align-items: center; justify-content: center; - font-size: 2rem; + font-size: 1.15rem; font-weight: 700; - border: 3px solid #fff; - box-shadow: 0 2px 8px rgba(0,0,0,.1); - text-transform: uppercase; } -/* Info */ -.space-info { - min-width: 0; -} - -.space-username { - font-size: 1.5rem; - font-weight: 700; - color: var(--color-primary); - line-height: 1.3; - margin-bottom: .25rem; -} - -.space-bio { - font-size: .9rem; - color: var(--color-text-light); - line-height: 1.5; - margin-bottom: .5rem; - word-break: break-word; -} - -.space-meta { - display: flex; - align-items: center; - gap: .3rem; - font-size: .8rem; - color: var(--color-secondary); - flex-wrap: wrap; -} - -.space-divider { - color: var(--color-border); -} - -/* ============================================================ - Stats Bar - ============================================================ */ -.space-stats { - display: flex; - gap: 2rem; - padding: 1rem 0; - border-bottom: 1px solid var(--color-border); - margin-bottom: 1.5rem; -} - -.space-stat-item { - display: flex; - flex-direction: column; - align-items: center; - gap: .15rem; -} - -.space-stat-num { - font-size: 1.4rem; - font-weight: 700; - color: var(--color-primary); -} - -.space-stat-label { - font-size: .8rem; - color: var(--color-secondary); -} - -/* ============================================================ - Content Section — 稿件列表 - ============================================================ */ -.space-content { - padding-bottom: 3rem; -} - -.space-section { - margin-top: .5rem; -} - -.space-section-title { - font-size: 1.1rem; - font-weight: 600; - color: var(--color-primary); - margin-bottom: 1rem; - padding-bottom: .5rem; - border-bottom: 2px solid var(--color-accent); - display: inline-block; -} - -/* ---------- Post Card ---------- */ -.space-posts-list { - display: flex; - flex-direction: column; - gap: .75rem; -} - -.space-post-card { - display: flex; - align-items: center; - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius); - padding: 1rem 1.25rem; - transition: box-shadow .2s ease, border-color .2s ease; -} - -.space-post-card:hover { - box-shadow: 0 2px 12px rgba(0,0,0,.06); - border-color: #ccc; -} - -.space-post-main { +/* User Info */ +.space-user-info { flex: 1; min-width: 0; } -.space-post-title { - font-size: 1rem; +.space-user-name { + display: block; + font-size: .92rem; font-weight: 600; - margin-bottom: .3rem; - line-height: 1.4; -} - -.space-post-title a { color: var(--color-primary); - transition: var(--transition); + text-decoration: none; + line-height: 1.3; + margin-bottom: .2rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.space-post-title a:hover { +.space-user-name:hover { color: var(--color-accent); } -.space-post-excerpt { - font-size: .85rem; +.space-user-bio { + font-size: .8rem; color: var(--color-text-light); - line-height: 1.5; - margin-bottom: .5rem; + line-height: 1.4; + margin: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; + word-break: break-word; } -.space-post-meta { - display: flex; - align-items: center; - gap: .75rem; - font-size: .75rem; +/* Follow Button in Card */ +.space-follow-btn { + flex-shrink: 0; + padding: .28rem .7rem; + border: 1.5px solid var(--color-accent); + border-radius: 6px; + font-size: .8rem; + font-weight: 500; + cursor: pointer; + transition: all .15s ease; + background: transparent; + color: var(--color-accent); + line-height: 1.4; +} + +.space-follow-btn:hover { + background: var(--color-accent); + color: #fff; +} + +.space-follow-btn.followed { + background: transparent; + border-color: var(--color-border, #e5e7eb); + color: var(--color-secondary); + cursor: default; +} + +.space-follow-btn.followed:hover { + background: transparent; color: var(--color-secondary); } -.space-post-status { - color: var(--color-accent); - font-weight: 500; +.space-follow-btn.waiting { + opacity: .6; + pointer-events: none; } -/* ---------- Empty State ---------- */ -.empty-state { +/* ============================================================ + Empty / Private States + ============================================================ */ +.space-empty-state { text-align: center; - padding: 3rem 1rem; + padding: 4rem 1rem; color: var(--color-secondary); font-size: .95rem; } -/* ---------- Pagination ---------- */ +.space-private-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 5rem 1rem; + text-align: center; +} + +.space-private-illustration { + margin-bottom: 1.25rem; + opacity: .6; +} + +.space-private-text { + color: var(--color-secondary); + font-size: .95rem; + margin-bottom: 1rem; +} + +.space-private-setting-btn { + display: inline-block; + padding: .5rem 1.25rem; + background: var(--color-accent); + color: #fff; + border-radius: var(--radius); + font-size: .88rem; + text-decoration: none; + transition: background .15s; +} + +.space-private-setting-btn:hover { + background: #2980b9; +} + +/* ============================================================ + Pagination + ============================================================ */ .pagination { display: flex; justify-content: center; @@ -274,40 +360,46 @@ Responsive ============================================================ */ @media (max-width: 768px) { - .space-header { - padding: 1.5rem 0; - } - - .space-header-inner { + .space-card-layout { flex-direction: column; - text-align: center; - gap: .75rem; + padding: 1rem .75rem; } - .space-avatar img, - .space-avatar-placeholder { - width: 64px; - height: 64px; - font-size: 1.5rem; + .space-sidebar { + width: 100%; } - .space-username { - font-size: 1.25rem; + .space-sidebar-nav { + flex-direction: row; + overflow-x: auto; + gap: .5rem; + -webkit-overflow-scrolling: touch; } - .space-meta { - justify-content: center; + .space-sidebar-link { + white-space: nowrap; + flex-shrink: 0; } - .space-stats { - justify-content: center; + .space-sidebar-title { + margin-bottom: .5rem; } - .space-post-card { + .space-sidebar-hint { + display: none; + } + + .space-user-grid { + grid-template-columns: 1fr; + } + + .space-user-card { padding: .85rem 1rem; } - .space-post-title { - font-size: .95rem; + .space-main-header { + flex-direction: column; + align-items: flex-start; + gap: .5rem; } }