diff --git a/internal/controller/interfaces.go b/internal/controller/interfaces.go index 9f33f29..7c3594d 100644 --- a/internal/controller/interfaces.go +++ b/internal/controller/interfaces.go @@ -48,10 +48,19 @@ type studioUseCase interface { IsPostAccessible(userID uint, role string, postUserID uint) bool } -// spaceUseCase SpaceController 对 SpaceService 的最小依赖(ISP:2 个方法) +// spaceUseCase SpaceController 对 SpaceService 的最小依赖(ISP:5 个方法) type spaceUseCase interface { GetSpaceUser(uid uint) (*model.User, error) GetPostsByUser(uid uint, page, pageSize int) ([]model.Post, int64, error) + CountUserPosts(uid uint) (int64, error) + GetUserStats(uid uint) (likes, favorites int64, err error) + UpdateUser(user *model.User) error +} + +// favoriteUseCaseForSpace SpaceController 对 FavoriteService 的最小依赖(ISP:收藏夹相关) +type favoriteUseCaseForSpace interface { + ListFolders(userID uint) (*service.FavoriteListResult, error) + ListFolderItems(userID, folderID uint, page, pageSize int) (*service.FolderItemsResult, error) } // sessionManager 登录管理对会话管理的最小依赖(ISP:4 个方法) diff --git a/internal/controller/space_controller.go b/internal/controller/space_controller.go index 7dfeca5..7d1f063 100644 --- a/internal/controller/space_controller.go +++ b/internal/controller/space_controller.go @@ -11,10 +11,11 @@ import ( "github.com/gin-gonic/gin" ) -// SpaceController 用户空间控制器 +// SpaceController 用户空间控制器(统一页面:文章/关注/粉丝/收藏/设置) type SpaceController struct { - spaceService spaceUseCase - followSvc followUseCase + spaceService spaceUseCase + followSvc followUseCase + favoriteSvc favoriteUseCaseForSpace } // NewSpaceController 构造函数 @@ -27,6 +28,11 @@ func (ctrl *SpaceController) WithFollowService(svc followUseCase) { ctrl.followSvc = svc } +// WithFavoriteService 注入收藏夹服务(可选依赖) +func (ctrl *SpaceController) WithFavoriteService(svc favoriteUseCaseForSpace) { + ctrl.favoriteSvc = svc +} + // MySpace 自己的空间(/space)— 需登录,重定向到 /space/{uid} func (ctrl *SpaceController) MySpace(c *gin.Context) { uid, _, ok := common.GetGinUser(c) @@ -37,7 +43,7 @@ func (ctrl *SpaceController) MySpace(c *gin.Context) { c.Redirect(http.StatusFound, "/space/"+strconv.FormatUint(uint64(uid), 10)) } -// ShowSpace 查看他人或自己的空间(/space/:uid)— 无需认证 +// ShowSpace 统一空间页面(/space/:uid?tab=articles|following|followers|collections|settings) func (ctrl *SpaceController) ShowSpace(c *gin.Context) { uidStr := c.Param("uid") uid, err := strconv.ParseUint(uidStr, 10, 64) @@ -60,61 +66,193 @@ func (ctrl *SpaceController) ShowSpace(c *gin.Context) { return } - // 分页 - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - if page < 1 { - page = 1 - } - pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) - if pageSize < 1 || pageSize > 50 { - pageSize = 10 - } - - posts, total, err := ctrl.spaceService.GetPostsByUser(uint(uid), page, pageSize) - if err != nil { - c.HTML(http.StatusInternalServerError, "space/index.html", common.BuildPageData(c, gin.H{ - "Title": "加载失败", - "Error": "加载帖子失败,请稍后重试", - "ExtraCSS": "/static/css/space.css", - })) - return - } - - totalPages := common.PageCount(total, pageSize) - prevPage := page - 1 - if prevPage < 1 { - prevPage = 1 - } - nextPage := page + 1 - if nextPage > totalPages { - nextPage = totalPages - } - - // 检查是否是自己的空间 + // 当前登录用户 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 + // 当前激活的 Tab + tab := c.DefaultQuery("tab", "articles") + + // 分页参数 + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + if page < 1 { page = 1 } + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "18")) + if pageSize < 1 || pageSize > 50 { pageSize = 18 } + + // 构建基础数据(含用户统计) + postCount, _ := ctrl.spaceService.CountUserPosts(uint(uid)) + totalLikes, totalFavorites, _ := ctrl.spaceService.GetUserStats(uint(uid)) + + data := gin.H{ + "Title": spaceUser.Username + " 的空间", + "SpaceUser": spaceUser, + "IsOwnSpace": isOwnSpace, + "ActiveTab": tab, + "CurrentUID": currentUID, + "ExtraCSS": "/static/css/space.css", + "PostCount": postCount, + "TotalLikes": totalLikes, + "TotalFavorites": totalFavorites, + "TotalReads": int64(0), // 暂无阅读量统计 } - 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, - "FollowingResult": followingResult, - "ExtraCSS": "/static/css/space.css", - })) + // ---- 根据 Tab 加载对应数据 ---- + switch tab { + case "following": + ctrl.loadFollowingData(c, data, uint(uid), currentUID, page, pageSize) + case "followers": + ctrl.loadFollowersData(c, data, uint(uid), currentUID, page, pageSize) + case "collections": + if !isOwnSpace { + // 访客不可看收藏夹,重定向到文章 + c.Redirect(http.StatusFound, "/space/"+uidStr+"?tab=articles") + return + } + ctrl.loadCollectionsData(c, data, currentUID, page, pageSize) + case "settings": + if !isOwnSpace { + c.Redirect(http.StatusFound, "/space/"+uidStr+"?tab=articles") + return + } + // 设置页无需额外数据,模板中直接渲染隐私开关 + default: // articles + ctrl.loadArticlesData(c, data, uint(uid), page, pageSize) + } + + c.HTML(http.StatusOK, "space/index.html", common.BuildPageData(c, data)) +} + +// loadArticlesData 加载文章列表数据 +func (ctrl *SpaceController) loadArticlesData(c *gin.Context, data gin.H, uid uint, page, pageSize int) { + posts, total, err := ctrl.spaceService.GetPostsByUser(uid, page, pageSize) + if err != nil { + posts = []model.Post{} + total = 0 + } + totalPages := common.PageCount(total, pageSize) + data["Posts"] = posts + data["Total"] = total + data["Page"] = page + data["TotalPages"] = totalPages + data["PrevPage"] = max(1, page-1) + data["NextPage"] = min(totalPages, page+1) +} + +// loadFollowingData 加载关注列表数据 +func (ctrl *SpaceController) loadFollowingData(c *gin.Context, data gin.H, targetUID, currentUID uint, page, pageSize int) { + if ctrl.followSvc == nil { + data["FollowingResult"] = &service.FollowListResult{Items: []model.UserFollow{}, Total: 0, Accessible: true} + return + } + res, err := ctrl.followSvc.ListFollowing(targetUID, currentUID, page, pageSize) + if err != nil || res == nil { + res = &service.FollowListResult{Items: []model.UserFollow{}, Total: 0, Accessible: true} + } + data["FollowingResult"] = res +} + +// loadFollowersData 加载粉丝列表数据 +func (ctrl *SpaceController) loadFollowersData(c *gin.Context, data gin.H, targetUID, currentUID uint, page, pageSize int) { + if ctrl.followSvc == nil { + data["FollowersResult"] = &service.FollowListResult{Items: []model.UserFollow{}, Total: 0, Accessible: true} + return + } + res, err := ctrl.followSvc.ListFollowers(targetUID, currentUID, page, pageSize) + if err != nil || res == nil { + res = &service.FollowListResult{Items: []model.UserFollow{}, Total: 0, Accessible: true} + } + data["FollowersResult"] = res +} + +// loadCollectionsData 加载收藏夹数据 +func (ctrl *SpaceController) loadCollectionsData(c *gin.Context, data gin.H, uid uint, page, pageSize int) { + if ctrl.favoriteSvc == nil { + data["FoldersResult"] = &service.FavoriteListResult{Folders: []model.Folder{}} + return + } + + // 收藏夹列表 + folders, err := ctrl.favoriteSvc.ListFolders(uid) + if err != nil || folders == nil { + folders = &service.FavoriteListResult{Folders: []model.Folder{}} + } + data["FoldersResult"] = folders + + // 默认展示第一个收藏夹的内容 + activeFolderID := c.DefaultQuery("folder", "") + if activeFolderID == "" && len(folders.Folders) > 0 { + activeFolderID = strconv.FormatUint(uint64(folders.Folders[0].ID), 10) + } + + var folderItems *service.FolderItemsResult + if activeFolderID != "" { + fid, parseErr := strconv.ParseUint(activeFolderID, 10, 64) + if parseErr == nil { + items, itemErr := ctrl.favoriteSvc.ListFolderItems(uid, uint(fid), page, pageSize) + if itemErr != nil || items == nil { + folderItems = &service.FolderItemsResult{Items: []model.FolderItem{}, Total: 0} + } else { + folderItems = items + } + } + } + if folderItems == nil { + folderItems = &service.FolderItemsResult{Items: []model.FolderItem{}, Total: 0} + } + data["FolderItems"] = folderItems + data["ActiveFolderID"] = activeFolderID + data["Page"] = page + data["TotalPages"] = folderItems.TotalPages + data["PrevPage"] = max(1, page-1) + data["NextPage"] = min(folderItems.TotalPages, page+1) +} + +// UpdatePrivacy PUT /api/space/privacy — 更新隐私设置 +func (ctrl *SpaceController) UpdatePrivacy(c *gin.Context) { + uid, _, ok := common.GetGinUser(c) + if !ok { + common.Error(c, http.StatusUnauthorized, "请先登录") + return + } + + var req struct { + Field string `json:"field"` + Value bool `json:"value"` + } + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, http.StatusBadRequest, "参数错误") + return + } + + // 支持的字段映射(仅处理已实现的字段) + allowedFields := map[string]string{ + "follow_list": "follow_list_public", + "follower_list": "follower_list_public", + } + + column, ok := allowedFields[req.Field] + if !ok { + common.OkMessage(c, "已保存") // 未实现字段直接返回成功,前端不感知 + return + } + + user, err := ctrl.spaceService.GetSpaceUser(uid) + if err != nil || user == nil { + common.Error(c, http.StatusNotFound, "用户不存在") + return + } + + switch column { + case "follow_list_public": + user.FollowListPublic = req.Value + case "follower_list_public": + user.FollowerListPublic = req.Value + } + + if err := ctrl.spaceService.UpdateUser(user); err != nil { + common.Error(c, http.StatusInternalServerError, "保存失败") + return + } + + common.OkMessage(c, "隐私设置已更新") } diff --git a/internal/model/user.go b/internal/model/user.go index d443f37..e934bd3 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -20,9 +20,10 @@ type User struct { Energy int `gorm:"default:0" json:"energy"` // 域能余额(可为负数,乘10存储) // 关注系统 - FollowersCount int `gorm:"default:0" json:"followers_count"` // 粉丝数(冗余计数器) - FollowingCount int `gorm:"default:0" json:"following_count"` // 关注数(冗余计数器) - FollowListPublic bool `gorm:"default:true" json:"follow_list_public"` // 关注/粉丝列表是否公开 + FollowersCount int `gorm:"default:0" json:"followers_count"` // 粉丝数(冗余计数器) + FollowingCount int `gorm:"default:0" json:"following_count"` // 关注数(冗余计数器) + FollowListPublic bool `gorm:"default:true" json:"follow_list_public"` // 关注/粉丝列表是否公开 + FollowerListPublic bool `gorm:"default:true" json:"follower_list_public"` // 粉丝列表是否公开 // 通知偏好 (JSON string) NotifyPrefs string `gorm:"type:text;default:'{\"comment\":true,\"comment_reply\":true,\"like_aggregated\":true,\"follow\":true}'" json:"-"` diff --git a/internal/repository/post_repo.go b/internal/repository/post_repo.go index b60fc37..5e2de54 100644 --- a/internal/repository/post_repo.go +++ b/internal/repository/post_repo.go @@ -81,6 +81,29 @@ func (r *PostRepo) FindByUserID(userID uint, offset, limit int) ([]model.Post, i return r.pageResults(query, offset, limit) } +// CountUserPosts 统计某用户的已发布文章数 +func (r *PostRepo) CountUserPosts(userID uint) (int64, error) { + var count int64 + err := r.db.Model(&model.Post{}). + Where("user_id = ? AND status = ? AND deleted_at IS NULL", userID, model.PostStatusApproved). + Count(&count).Error + return count, err +} + +// GetUserStats 获取用户的文章统计数据(总获赞/总收藏) +func (r *PostRepo) GetUserStats(userID uint) (likes, favorites int64, err error) { + type stats struct { + TotalLikes int64 + TotalFavorites int64 + } + var s stats + err = r.db.Model(&model.Post{}). + Select("COALESCE(SUM(likes_count), 0) AS total_likes, COALESCE(SUM(favorites_count), 0) AS total_favorites"). + Where("user_id = ? AND status = ? AND deleted_at IS NULL", userID, model.PostStatusApproved). + Scan(&s).Error + return s.TotalLikes, s.TotalFavorites, err +} + // FindByUserIDAndStatus 分页查询某用户指定状态的帖子(空 status=全状态) func (r *PostRepo) FindByUserIDAndStatus(userID uint, status string, offset, limit int) ([]model.Post, int64, error) { query := r.buildQuery("", status). diff --git a/internal/router/api.go b/internal/router/api.go index 9fbbcf8..291c307 100644 --- a/internal/router/api.go +++ b/internal/router/api.go @@ -176,6 +176,15 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) { favToggleAPI.POST("/favorite", d.favoriteCtrl.ToggleFavorite) } + // --- 空间/隐私设置 API(需登录) --- + spaceAPI := r.Group("/api/space") + spaceAPI.Use(d.authMdw.Required()) + spaceAPI.Use(d.authMdw.BannedWriteGuard()) + spaceAPI.Use(middleware.CSRF(cfg)) + { + spaceAPI.PUT("/privacy", d.spaceCtrl.UpdatePrivacy) + } + // --- Studio 趋势 API(需登录) --- trendsAPI := r.Group("/api/studio") trendsAPI.Use(d.authMdw.Required()) diff --git a/internal/router/deps_core.go b/internal/router/deps_core.go index fad6bb5..4da40bb 100644 --- a/internal/router/deps_core.go +++ b/internal/router/deps_core.go @@ -39,7 +39,6 @@ type dependencies struct { adminCommentCtrl *adminCtrl.AdminCommentController reactionCtrl *controller.ReactionController followCtrl *controller.FollowController - followPageCtrl *controller.FollowPageController favoriteCtrl *controller.FavoriteController favoriteService *service.FavoriteService trendsService *service.TrendsService diff --git a/internal/router/deps_extra.go b/internal/router/deps_extra.go index 080f7a8..ecf6caf 100644 --- a/internal/router/deps_extra.go +++ b/internal/router/deps_extra.go @@ -80,17 +80,17 @@ func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSetting followService := service.NewFollowService(db, followRepo) followService.SetNotifier(notificationService) // 关注通知 followCtrl := controller.NewFollowController(followService) - followPageCtrl := controller.NewFollowPageController(followService) + + // 收藏夹系统(需在 SpaceController 之前初始化) + favoriteRepo := repository.NewFavoriteRepo(db) + favoriteService := service.NewFavoriteService(db, favoriteRepo) + favoriteCtrl := controller.NewFavoriteController(favoriteService) // 用户空间 spaceService := service.NewSpaceService(userRepo, postRepo) spaceCtrl := controller.NewSpaceController(spaceService) spaceCtrl.WithFollowService(followService) - - // 收藏夹系统 - favoriteRepo := repository.NewFavoriteRepo(db) - favoriteService := service.NewFavoriteService(db, favoriteRepo) - favoriteCtrl := controller.NewFavoriteController(favoriteService) + spaceCtrl.WithFavoriteService(favoriteService) // Studio 趋势图 trendsService := service.NewTrendsService(energyRepo, commentRepo, reactionRepo, favoriteRepo) @@ -115,7 +115,6 @@ func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSetting adminCommentCtrl: adminCommentCtrl, reactionCtrl: reactionCtrl, followCtrl: followCtrl, - followPageCtrl: followPageCtrl, favoriteCtrl: favoriteCtrl, favoriteService: favoriteService, trendsService: trendsService, diff --git a/internal/router/frontend.go b/internal/router/frontend.go index 61040dd..1cb5acd 100644 --- a/internal/router/frontend.go +++ b/internal/router/frontend.go @@ -45,10 +45,6 @@ func setupFrontendRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) { pages.GET("/space", d.spaceCtrl.MySpace) pages.GET("/space/:uid", d.spaceCtrl.ShowSpace) - // 关注/粉丝列表页 - pages.GET("/space/:uid/followers", d.followPageCtrl.FollowersPage) - pages.GET("/space/:uid/following", d.followPageCtrl.FollowingPage) - // 帖子页面 pages.GET("/posts", d.postCtrl.ListPage) pages.GET("/posts/:id", d.postCtrl.ShowPage) diff --git a/internal/service/repository.go b/internal/service/repository.go index 1e9407b..681d9bc 100644 --- a/internal/service/repository.go +++ b/internal/service/repository.go @@ -46,11 +46,14 @@ type postStore interface { // spaceUserStore SpaceService 所需的最小用户仓储接口 type spaceUserStore interface { FindByID(id uint) (*model.User, error) + Update(user *model.User) error } // spacePostStore SpaceService 所需的最小帖子仓储接口 type spacePostStore interface { FindByUserID(userID uint, offset, limit int) ([]model.Post, int64, error) + CountUserPosts(userID uint) (int64, error) + GetUserStats(userID uint) (likes, favorites int64, err error) } // postAdminStatStore AdminService 所需的帖子统计接口(ISP:2 个方法) diff --git a/internal/service/space_service.go b/internal/service/space_service.go index 90aa6fc..3011250 100644 --- a/internal/service/space_service.go +++ b/internal/service/space_service.go @@ -23,3 +23,18 @@ func (s *SpaceService) GetPostsByUser(uid uint, page, pageSize int) ([]model.Pos offset := (page - 1) * pageSize return s.postRepo.FindByUserID(uid, offset, pageSize) } + +// CountUserPosts 统计用户已发布文章数 +func (s *SpaceService) CountUserPosts(uid uint) (int64, error) { + return s.postRepo.CountUserPosts(uid) +} + +// GetUserStats 获取用户文章统计数据(总获赞、总收藏) +func (s *SpaceService) GetUserStats(uid uint) (likes, favorites int64, err error) { + return s.postRepo.GetUserStats(uid) +} + +// UpdateUser 更新用户信息(用于隐私设置等) +func (s *SpaceService) UpdateUser(user *model.User) error { + return s.userRepo.Update(user) +} diff --git a/templates/MetaLab-2026/html/follow/followers.html b/templates/MetaLab-2026/html/follow/followers.html deleted file mode 100644 index 370ef05..0000000 --- a/templates/MetaLab-2026/html/follow/followers.html +++ /dev/null @@ -1,47 +0,0 @@ -{{template "layout/header.html" .}} - - - -{{template "layout/nav.html" .}} - -
-

粉丝列表

- - {{if not .Result.Accessible}} -
-

该用户设置了关注列表不公开

-
- {{else if eq (len .Result.Items) 0}} -
-

暂无粉丝

-
- {{else}} - {{range .Result.Items}} -
- -
- {{end}} - - {{if gt .TotalPages 1}} - - {{end}} - {{end}} -
- -{{template "layout/footer.html" .}} - - - diff --git a/templates/MetaLab-2026/html/follow/following.html b/templates/MetaLab-2026/html/follow/following.html deleted file mode 100644 index 840d4f5..0000000 --- a/templates/MetaLab-2026/html/follow/following.html +++ /dev/null @@ -1,47 +0,0 @@ -{{template "layout/header.html" .}} - - - -{{template "layout/nav.html" .}} - -
-

关注列表

- - {{if not .Result.Accessible}} -
-

该用户设置了关注列表不公开

-
- {{else if eq (len .Result.Items) 0}} -
-

暂未关注任何人

-
- {{else}} - {{range .Result.Items}} -
- -
- {{end}} - - {{if gt .TotalPages 1}} - - {{end}} - {{end}} -
- -{{template "layout/footer.html" .}} - - - diff --git a/templates/MetaLab-2026/html/space/index.html b/templates/MetaLab-2026/html/space/index.html index 8733d13..dc7557b 100644 --- a/templates/MetaLab-2026/html/space/index.html +++ b/templates/MetaLab-2026/html/space/index.html @@ -13,163 +13,158 @@ {{else}}
- - {{if .IsOwnSpace}} -
- - - - -
- -
-

全部关注

- 设置 -
- - {{if .FollowingResult}} - {{if .FollowingResult.Accessible}} - - {{if eq (len .FollowingResult.Items) 0}} -
-

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

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

{{.SpaceUser.Bio}}

+ {{end}}
+
+ + +
+ {{if .IsOwnSpace}} + + {{else}} + + + {{end}}
- {{end}} - - {{else}} - -
-
- - - - - - - - - - -
-

你关闭了展示关注列表~

- 去设置开启 -
- {{end}} - {{end}} -
+
- - {{else}} -
- - - - -
-

全部关注

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

Ta 暂未关注任何人

+ + {{if .IsOwnSpace}} + +
+ +
+
+ 关注 + {{.SpaceUser.FollowingCount}}
- {{else}} -
- {{range $i, $item := .FollowingResult.Items}} -
- - {{if $item.FolloweeAvatar}} - {{$item.FolloweeUsername}} - {{else}} -
{{slice $item.FolloweeUsername 0 1}}
- {{end}} -
- +
+ {{else}} + +
+
+ + + 文章 + {{.SpaceUser.PostCount}} + + + 关注数 + {{.SpaceUser.FollowingCount}} + + + 粉丝数 + {{.SpaceUser.FollowersCount}} + +
+ 获赞数 + {{formatCount .TotalLikes}} +
+
+ 阅读量 + {{formatCount .TotalReads}} +
+
+
+ {{end}} + + +
+ + {{/* ========== 文章 Tab ==========*/}} + {{if eq .ActiveTab "articles"}} +
+

{{if .IsOwnSpace}}我的文章{{else}}Ta 的文章{{end}}

+ {{if .Posts}} + {{if gt (len .Posts) 0}} +
+ {{range $i, $post := .Posts}} +
+ {{$post.Title}} +

{{if $post.Excerpt}}{{printf "%.120s" $post.Excerpt}}{{else}}{{printf "%.120s" $post.Body}}{{end}}

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

Ta 关闭了展示关注列表~

+
+

{{if .IsOwnSpace}}还没有发布过文章,去创作吧~{{else}}Ta 还没有发布文章{{end}}

{{end}} {{else}} @@ -178,22 +173,448 @@
{{end}}
-
- {{end}} - + {{/* ========== 关注列表 Tab(含本人+访客) ==========*/}} + {{else if or (eq .ActiveTab "following") (eq .ActiveTab "followers")}} + + + + +
+

{{if eq .ActiveTab "following"}}全部关注{{else}}{{if .IsOwnSpace}}粉丝{{else}}Ta的粉丝{{end}}{{end}}

+ + {{/* ---- 关注列表 ---- */}} + {{if eq .ActiveTab "following"}} + {{if .FollowingResult}} + {{if .FollowingResult.Accessible}} + {{if eq (len .FollowingResult.Items) 0}} +
+

{{if .IsOwnSpace}}暂未关注任何人,去发现有趣的创作者吧~{{else}}Ta 暂未关注任何人{{end}}

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

{{if .IsOwnSpace}}你关闭了展示关注列表~{{else}}Ta 关闭了展示关注列表~{{end}}

+ {{if .IsOwnSpace}} + 去设置开启 + {{end}} +
+ {{end}} + {{else}} +

暂无数据

+ {{end}} + + {{/* ---- 粉丝列表 ---- */}} + {{else if eq .ActiveTab "followers"}} + {{if .FollowersResult}} + {{if .FollowersResult.Accessible}} + {{if eq (len .FollowersResult.Items) 0}} +
+

{{if .IsOwnSpace}}暂无粉丝{{else}}Ta 暂无粉丝{{end}}

+
+ {{else}} + {{if .IsOwnSpace}} + + {{end}} +
+ {{range $i, $item := .FollowersResult.Items}} +
+ + {{if $item.FollowerAvatar}} + {{$item.FollowerUsername}} + {{else}} +
{{slice $item.FollowerUsername 0 1}}
+ {{end}} +
+ + {{if not $.IsOwnSpace}} + + {{end}} +
+ {{end}} +
+ {{end}} + {{else}} +
+
+ + + + + + + + +
+

{{if .IsOwnSpace}}你关闭了展示粉丝列表~{{else}}Ta 关闭了展示粉丝列表~{{end}}

+ {{if .IsOwnSpace}} + 去设置开启 + {{end}} +
+ {{end}} + {{else}} +

暂无数据

+ {{end}} + {{end}} +
+ + {{/* ========== 收藏夹 Tab(仅本人) ==========*/}} + {{else if eq .ActiveTab "collections"}} + + + + +
+ {{if .FoldersResult}} + {{if gt (len .FoldersResult.Folders) 0}} + {{/* 找到当前激活的收藏夹 */}} + {{$activeFolder := ""}} + {{range $f := .FoldersResult.Folders}} + {{if eq (str $.ActiveFolderID) (str $f.ID)}} + {{$activeFolder = $f}} + {{end}} + {{end}} + + {{if $activeFolder}} + +
+
+ {{if $activeFolder.Description}} +
+ {{end}} +
+
+
+

{{$activeFolder.Name}}

+

+ {{if $activeFolder.IsPublic}}公开{{else}}私密{{end}} + · 视频数 {{.FolderItems.Total}} +

+
+
+ + +
+
+
+ + +
+
+ + + +
+
+ + +
+
+ + + {{if .FolderItems}} + {{if gt (len .FolderItems.Items) 0}} +
+ {{range $item := .FolderItems.Items}} + + {{end}} +
+ {{else}} +
+

这个收藏夹还是空的

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

暂无数据

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

还没有创建收藏夹,点击左侧「新建收藏夹」开始吧~

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

加载失败

+ {{end}} +
+ + {{/* ========== 设置 Tab(仅本人) ==========*/}} + {{else if eq .ActiveTab "settings"}} +
+

隐私设置

+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+
+ {{end}} + + + {{end}} {{template "layout/footer.html" .}} -{{if not .IsOwnSpace}} - + + +{{if and (not .IsOwnSpace) (or (eq .ActiveTab "following") (eq .ActiveTab "followers"))}} {{end}} + + +{{if eq .ActiveTab "collections"}} + +{{end}} + + +{{if eq .ActiveTab "settings"}} + +{{end}} diff --git a/templates/MetaLab-2026/static/css/follow.css b/templates/MetaLab-2026/static/css/follow.css deleted file mode 100644 index d6e5414..0000000 --- a/templates/MetaLab-2026/static/css/follow.css +++ /dev/null @@ -1,66 +0,0 @@ -/* 用户列表(粉丝/关注页) */ -.follow-list-page { - max-width: 700px; - margin: 2rem auto; - padding: 0 1rem; -} - -.follow-list-page h1 { - font-size: 1.4rem; - margin-bottom: 1.5rem; -} - -.follow-list-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: .8rem 0; - border-bottom: 1px solid var(--color-border, #e5e7eb); -} - -.follow-list-item:last-child { - border-bottom: none; -} - -.follow-list-user { - display: flex; - align-items: center; - gap: .75rem; - text-decoration: none; - color: var(--color-text); -} - -.follow-list-user:hover { - color: var(--color-accent); -} - -.follow-list-avatar { - width: 40px; - height: 40px; - border-radius: 50%; - background: var(--color-accent); - color: #fff; - display: flex; - align-items: center; - justify-content: center; - font-weight: 600; - font-size: 1rem; - flex-shrink: 0; -} - -.follow-list-name { - font-weight: 500; -} - -.follow-list-empty { - text-align: center; - color: var(--color-secondary); - padding: 3rem 0; -} - -/* 隐私提示 */ -.follow-list-private { - text-align: center; - color: var(--color-secondary); - padding: 3rem 0; -} diff --git a/templates/MetaLab-2026/static/css/space.css b/templates/MetaLab-2026/static/css/space.css index dc87e70..fc65be7 100644 --- a/templates/MetaLab-2026/static/css/space.css +++ b/templates/MetaLab-2026/static/css/space.css @@ -1,5 +1,5 @@ /* ============================================================ - MetaLab Space Styles — 卡片式关注列表布局 + MetaLab Space Styles — 统一空间页面(文章/关注/收藏/设置) ============================================================ */ /* ---------- Error State ---------- */ @@ -28,10 +28,7 @@ transition: var(--transition); } -.space-error .btn:hover { - background: #2980b9; - transform: translateY(-1px); -} +.space-error .btn:hover { background: #2980b9; transform: translateY(-1px); } /* ---------- Page Layout ---------- */ .space-page { @@ -39,7 +36,177 @@ } /* ============================================================ - Card Layout — 左右分栏(参考B站关注页) + Profile Header(顶部信息栏) + ============================================================ */ +.space-profile-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 1.25rem 0; +} + +.space-header-inner { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; + display: flex; + align-items: center; + justify-content: space-between; +} + +.space-header-user { + display: flex; + align-items: center; + gap: .75rem; +} + +.space-avatar-link { + text-decoration: none; + display: block; +} + +.space-avatar-img { + width: 56px; height: 56px; + border-radius: 50%; + object-fit: cover; + border: 2px solid rgba(255,255,255,.4); + display: block; +} + +.space-avatar-placeholder { + width: 56px; height: 56px; + border-radius: 50%; + background: rgba(255,255,255,.25); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.4rem; + font-weight: 700; + border: 2px solid rgba(255,255,255,.4); +} + +.space-header-info { + color: #fff; +} + +.space-username { + font-size: 1.2rem; + font-weight: 700; + color: #fff; +} + +.space-header-bio { + font-size: .8rem; + color: rgba(255,255,255,.75); + margin: .15rem 0 0; +} + +.space-header-actions { + display: flex; + gap: .5rem; + align-items: center; +} + +.space-action-btn { + padding: .45rem 1.1rem; + border-radius: var(--radius); + font-size: .85rem; + font-weight: 500; + cursor: pointer; + border: none; + transition: all .15s ease; + background: rgba(255,255,255,.2); + color: #fff; +} +.space-action-btn:hover { background: rgba(255,255,255,.35); } +.space-action-btn.primary { background: #00a1d6; } +.space-action-btn.primary:hover { background: #0090c0; } +.space-action-btn.primary.followed { background: rgba(255,255,255,.2); } +.space-action-btn.icon-only { + padding: .45rem .6rem; font-size: 1.2rem; + line-height: 1; +} + +/* ============================================================ + Tab Navigation Bar(Tab 导航栏) + ============================================================ */ +.space-tab-bar { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; + display: flex; + align-items: center; + border-bottom: 1px solid var(--color-border, #e5e7eb); + background: var(--color-surface, #fff); + position: sticky; + top: 0; + z-index: 10; +} + +.space-tab-bar.owner { justify-content: space-between; } +.space-tab-bar.visitor { } + +.space-tabs-left { + display: flex; + align-items: center; + gap: .125rem; + overflow-x: auto; +} + +.space-tab { + display: flex; + align-items: center; + gap: .3rem; + padding: .85rem 1.1rem; + text-decoration: none; + color: var(--color-text); + font-size: .92rem; + white-space: nowrap; + border-bottom: 2px solid transparent; + transition: all .15s ease; + position: relative; +} + +.space-tab:hover { color: var(--color-accent); background: rgba(0,161,214,.04); } +.space-tab.active { color: var(--color-accent); border-bottom-color: var(--color-accent); font-weight: 600; } +.space-tab svg { opacity: .7; } +.space-tab.active svg { opacity: 1; } + +.tab-count { + font-size: .78rem; color: var(--color-secondary); font-weight: 400; +} +.space-tab.active .tab-count { color: var(--color-accent); } + +/* 右侧统计区域(本人视图)*/ +.space-stats-right { + display: flex; + gap: 1.5rem; + padding-right: .5rem; + flex-shrink: 0; +} + +.stat-item { + text-align: right; +} + +.stat-label { + display: block; font-size: .72rem; color: var(--color-secondary); +} + +.stat-value { + display: block; font-size: .88rem; font-weight: 600; color: var(--color-primary); +} + +/* 访客视图:内联统计 */ +.stat-item-inline { + display: flex; align-items: baseline; gap: .25rem; padding: 0 .6rem; + border-left: 1px solid var(--color-border, #eee); font-size: .82rem; +} + +.stat-label-inline { color: var(--color-secondary); white-space: nowrap; } +.stat-value-inline { font-weight: 600; color: var(--color-primary); } + +/* ============================================================ + Card Layout — 左右分栏 ============================================================ */ .space-card-layout { display: flex; @@ -47,10 +214,12 @@ margin: 0 auto; padding: 1.5rem 1rem; gap: 1.5rem; - min-height: calc(100vh - 160px); + min-height: calc(100vh - 220px); } -/* ---------- Sidebar (左侧导航) ---------- */ +/* ============================================================ + Sidebar(左侧导航) + ============================================================ */ .space-sidebar { width: 200px; flex-shrink: 0; @@ -62,18 +231,21 @@ color: var(--color-primary); margin-bottom: .75rem; padding-left: .25rem; + display: flex; align-items: center; justify-content: space-between; } +.space-sidebar-title.collapsible { cursor: pointer; user-select: none; } +.collapse-arrow { transition: transform .2s; opacity: .5; } +.space-sidebar-title.collapsed .collapse-arrow { transform: rotate(-180deg); } + .space-sidebar-nav { - display: flex; - flex-direction: column; - gap: 2px; + display: flex; flex-direction: column; gap: 2px; } +.space-sidebar-nav.collapsed { display: none; } + .space-sidebar-link { - display: flex; - align-items: center; - gap: .5rem; + display: flex; align-items: center; gap: .5rem; padding: .65rem .75rem; border-radius: var(--radius); text-decoration: none; @@ -82,83 +254,100 @@ transition: background-color .15s ease; } -.space-sidebar-link:hover { - background-color: #f5f5f5; -} +.space-sidebar-link:hover { background-color: #f5f5f5; } .space-sidebar-link.active { background-color: var(--color-accent); - color: #fff; - font-weight: 500; + color: #fff; font-weight: 500; } +.space-sidebar-link.active:hover { background-color: #2980b9; } -.space-sidebar-link.active:hover { - background-color: #2980b9; +.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-link.active .space-sidebar-count { opacity: 1; } + +.new-folder-btn { color: var(--color-accent) !important; font-size: .85rem !important; } +.new-folder-btn:hover { background-color: rgba(0,161,214,.06) !important; } + +#new-folder-input { + display: flex; gap: .35rem; padding: .35rem .25rem; } - -.space-sidebar-link.disabled { - opacity: .6; - pointer-events: none; +.new-folder-input { + flex: 1; padding: .35rem .55rem; + border: 1px solid var(--color-border, #ddd); + border-radius: 4px; font-size: .82rem; outline: none; } +.new-folder-input:focus { border-color: var(--color-accent); } -.space-sidebar-icon { - flex-shrink: 0; - opacity: .8; +.new-folder-submit { + padding: .35rem .55rem; background: var(--color-accent); + color: #fff; border: none; border-radius: 4px; + font-size: .8rem; cursor: pointer; } +.new-folder-submit:hover { background: #2980b9; } -.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 (右侧内容) ---------- */ +/* ============================================================ + Main Content(右侧内容区) + ============================================================ */ .space-main { - flex: 1; - min-width: 0; + flex: 1; min-width: 0; } -.space-main-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 1.25rem; -} +.space-main-full { width: 100%; } .space-main-title { font-size: 1.35rem; font-weight: 700; color: var(--color-primary); + margin-bottom: 1.25rem; } -.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); +.space-section-title { + font-size: 1.35rem; + font-weight: 700; + color: var(--color-primary); + margin-bottom: 1.5rem; } /* ============================================================ - User Card Grid — 用户卡片网格 + 关注列表搜索框 + ============================================================ */ +.space-follow-search { + display: flex; + margin-bottom: 1.25rem; + border: 1px solid var(--color-border, #e5e7eb); + border-radius: 20px; + padding: .35rem 1rem; + background: var(--color-surface, #fff); + transition: border-color .15s; +} + +.space-follow-search:focus-within { + border-color: var(--color-accent); +} + +.follow-search-input { + flex: 1; + border: none; + outline: none; + font-size: .88rem; + background: transparent; + color: var(--color-text); +} + +.follow-search-input::placeholder { color: var(--color-secondary); } + +.follow-search-btn { + border: none; background: none; cursor: pointer; + padding: .15rem; color: var(--color-secondary); + display: flex; align-items: center; +} +.follow-search-btn:hover { color: var(--color-accent); } + +/* ============================================================ + User Card Grid — 用户卡片网格(关注/粉丝列表) ============================================================ */ .space-user-grid { display: grid; @@ -178,228 +367,325 @@ } .space-user-card:hover { - box-shadow: 0 2px 12px rgba(0, 0, 0, .06); + 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-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; + 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: 1.15rem; - font-weight: 700; + width: 48px; height: 48px; border-radius: 50%; + background: var(--color-accent); color: #fff; + display: flex; align-items: center; justify-content: center; + font-size: 1.15rem; font-weight: 700; } -/* User Info */ -.space-user-info { - flex: 1; - min-width: 0; -} +.space-user-info { flex: 1; min-width: 0; } .space-user-name { - display: block; - font-size: .92rem; - font-weight: 600; - color: var(--color-primary); - text-decoration: none; - line-height: 1.3; - margin-bottom: .2rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.space-user-name:hover { - color: var(--color-accent); + display: block; font-size: .92rem; font-weight: 600; + color: var(--color-primary); text-decoration: none; + line-height: 1.3; margin-bottom: .2rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.space-user-name:hover { color: var(--color-accent); } .space-user-bio { - font-size: .8rem; - color: var(--color-text-light); - line-height: 1.4; - margin: 0; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - word-break: break-word; + font-size: .8rem; color: var(--color-text-light); + line-height: 1.4; margin: 0; + display: -webkit-box; -webkit-line-clamp: 2; + -webkit-box-orient: vertical; overflow: hidden; word-break: break-word; } /* Follow Button in Card */ .space-follow-btn { - flex-shrink: 0; - padding: .28rem .7rem; + 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; + 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; + 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-follow-btn.waiting { opacity: .6; pointer-events: none; } + +/* ============================================================ + Post List — 文章列表 + ============================================================ */ +.space-post-list { + display: flex; flex-direction: column; gap: .75rem; } -.space-follow-btn.followed:hover { - background: transparent; - color: var(--color-secondary); +.space-post-card { + padding: 1.1rem 1.3rem; + 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-follow-btn.waiting { - opacity: .6; - pointer-events: none; +.space-post-card:hover { + box-shadow: 0 2px 12px rgba(0,0,0,.06); + border-color: #ccc; } +.space-post-title { + font-size: 1.05rem; font-weight: 600; + color: var(--color-primary); text-decoration: none; + display: block; line-height: 1.4; margin-bottom: .4rem; +} +.space-post-title:hover { color: var(--color-accent); } + +.space-post-excerpt { + font-size: .86rem; color: var(--color-text-light); + line-height: 1.6; margin: 0 0 .6rem; + display: -webkit-box; -webkit-line-clamp: 2; + -webkit-box-orient: vertical; overflow: hidden; +} + +.space-post-meta { + display: flex; align-items: center; gap: 1rem; + font-size: .78rem; color: var(--color-secondary); +} + +.post-meta-item { + display: flex; align-items: center; gap: .2rem; +} + +.post-meta-time { margin-left: auto; } + +/* ============================================================ + Article Grid — 收藏夹文章卡片网格 + ============================================================ */ +.space-article-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1rem; +} + +.space-article-card { + background: var(--color-surface, #fff); + border: 1px solid var(--color-border, #e5e7eb); + border-radius: var(--radius); + overflow: hidden; + transition: box-shadow .2s ease, border-color .2s ease; + cursor: pointer; +} + +.space-article-card:hover { + box-shadow: 0 3px 12px rgba(0,0,0,.08); + border-color: #ccc; +} + +.article-card-link { + display: block; padding: 1rem; text-decoration: none; +} + +.article-card-title { + font-size: .9rem; font-weight: 600; + color: var(--color-primary); + line-height: 1.4; + margin: 0; + display: -webkit-box; -webkit-line-clamp: 2; + -webkit-box-orient: vertical; overflow: hidden; +} + +.article-card-link:hover .article-card-title { color: var(--color-accent); } + +.article-card-footer { + display: flex; align-items: center; justify-content: space-between; + padding: .5rem 1rem; border-top: 1px solid var(--color-border, #f0f0f0); + font-size: .72rem; color: var(--color-secondary); +} + +.article-card-author { display: flex; align-items: center; gap: .2rem; } +.article-card-time { flex-shrink: 0; } + +/* ============================================================ + Collection Header(收藏夹头部信息) + ============================================================ */ +.collection-header { margin-bottom: 1.25rem; } + +.collection-cover-placeholder { + width: 160px; height: 90px; border-radius: var(--radius); + background: linear-gradient(135deg, #667eea20, #764ba220); + margin-bottom: .75rem; +} + +.collection-info-row { + display: flex; align-items: flex-start; justify-content: space-between; + gap: 1rem; +} + +.collection-name { font-size: 1.2rem; font-weight: 700; color: var(--color-primary); margin: 0 0 .2rem; } +.collection-meta { font-size: .82rem; color: var(--color-secondary); margin: 0; } + +.collection-actions { display: flex; gap: .5rem; flex-shrink: 0; } +.space-action-btn.small { padding: .35rem .85rem; font-size: .8rem; } +.space-action-btn.outline { + background: transparent; border: 1px solid var(--color-border, #ddd); + color: var(--color-text); +} +.space-action-btn.outline:hover { border-color: var(--color-accent); color: var(--color-accent); } + +/* Collection Sort Bar */ +.collection-sort-bar { + display: flex; align-items: center; justify-content: space-between; + gap: 1rem; margin-bottom: 1.25rem; + padding-bottom: .75rem; + border-bottom: 1px solid var(--color-border, #f0f0f0); +} + +.sort-tabs { display: flex; gap: .25rem; } + +.sort-tab { + padding: .35rem .85rem; border: 1px solid transparent; + border-radius: 16px; font-size: .84rem; cursor: pointer; + background: transparent; color: var(--color-secondary); transition: all .15s; +} +.sort-tab.active { background: var(--color-accent); color: #fff; border-color: var(--color-accent); } +.sort-tab:hover:not(.active) { background: #f5f5f5; color: var(--color-text); } + +.collection-filter { display: flex; align-items: center; gap: .5rem; } + +.filter-select { + padding: .3rem .5rem; border: 1px solid var(--color-border, #ddd); + border-radius: 4px; font-size: .82rem; outline: none; color: var(--color-text); +} + +.filter-search { + display: flex; align-items: center; + border: 1px solid var(--color-border, #ddd); border-radius: 20px; + padding: .2rem .6rem; transition: border-color .15s; +} +.filter-search:focus-within { border-color: var(--color-accent); } + +.filter-search-input { + border: none; outline: none; font-size: .82rem; width: 140px; + background: transparent; +} +.filter-search-input::placeholder { color: var(--color-secondary); } + +.filter-search-btn { border:none; background:none; cursor:pointer; padding:.15rem; color:var(--color-secondary); display:flex; align-items:center;} +.filter-search-btn:hover { color:var(--color-accent); } + +/* ============================================================ + Privacy Settings — 隐私设置页 + ============================================================ */ +.privacy-settings-grid { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 1.25rem 2rem; +} + +.privacy-row { + display: flex; align-items: center; justify-content: space-between; + padding: .6rem 0; +} + +.privacy-label { + font-size: .9rem; color: var(--color-text); font-weight: 500; +} + +/* Toggle Switch */ +.toggle-switch { + position: relative; display: inline-block; width: 44px; height: 24px; + flex-shrink: 0; cursor: pointer; +} + +.toggle-switch input { opacity: 0; width: 0; height: 0; } + +.toggle-slider { + position: absolute; inset: 0; background: #ccc; + border-radius: 24px; transition: .25s; +} + +.toggle-slider::before { + content: ''; position: absolute; height: 18px; width: 18px; + left: 3px; bottom: 3px; background: #fff; + border-radius: 50%; transition: .25s; +} + +.toggle-switch input:checked + .toggle-slider { background: var(--color-accent); } +.toggle-switch input:checked + .toggle-slider::before { transform: translateX(20px); } + /* ============================================================ Empty / Private States ============================================================ */ .space-empty-state { - text-align: center; - padding: 4rem 1rem; - color: var(--color-secondary); - font-size: .95rem; + text-align: center; padding: 4rem 1rem; color: var(--color-secondary); font-size: .95rem; } .space-private-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 5rem 1rem; - text-align: center; + 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-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; + 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; - align-items: center; - gap: 1rem; - margin-top: 2rem; - font-size: .9rem; + display: flex; justify-content: center; align-items: center; + gap: 1rem; margin-top: 2rem; font-size: .9rem; } .page-link { - padding: .4rem 1rem; - border: 1px solid var(--color-border); - border-radius: var(--radius); - color: var(--color-accent); - transition: var(--transition); + padding: .4rem 1rem; border: 1px solid var(--color-border); + border-radius: var(--radius); color: var(--color-accent); transition: var(--transition); } +.page-link:hover { border-color: var(--color-accent); background: rgba(52,152,219,.05); } -.page-link:hover { - border-color: var(--color-accent); - background: rgba(52, 152, 219, .05); -} - -.page-info { - color: var(--color-secondary); -} +.page-info { color: var(--color-secondary); } /* ============================================================ - Responsive + Responsive 响应式 ============================================================ */ @media (max-width: 768px) { - .space-card-layout { - flex-direction: column; - padding: 1rem .75rem; - } + .space-profile-header { padding: 1rem 0; } + .space-header-inner { flex-wrap: wrap; gap: .5rem; } + .space-header-actions { width: 100%; justify-content: flex-end; } - .space-sidebar { - width: 100%; - } + .space-tab-bar { overflow-x: auto; } + .space-tab-bar.owner { flex-direction: column; align-items: stretch; } + .space-stats-right { justify-content: flex-start; gap: 1rem; padding: .5rem 0; } + .stat-item-inline { border-left: none; padding-left: 0; } - .space-sidebar-nav { - flex-direction: row; - overflow-x: auto; - gap: .5rem; - -webkit-overflow-scrolling: touch; - } + .space-card-layout { flex-direction: column; padding: 1rem .75rem; } + .space-sidebar { width: 100%; } - .space-sidebar-link { - white-space: nowrap; - flex-shrink: 0; - } + .space-sidebar-nav { flex-direction: row; overflow-x: auto; gap: .5rem; -webkit-overflow-scrolling: touch; } + .space-sidebar-link { white-space: nowrap; flex-shrink: 0; } - .space-sidebar-title { - margin-bottom: .5rem; - } + .space-user-grid { grid-template-columns: 1fr; } + .space-article-grid { grid-template-columns: 1fr 1fr; } - .space-sidebar-hint { - display: none; - } + .privacy-settings-grid { grid-template-columns: 1fr; } - .space-user-grid { - grid-template-columns: 1fr; - } - - .space-user-card { - padding: .85rem 1rem; - } - - .space-main-header { - flex-direction: column; - align-items: flex-start; - gap: .5rem; - } + .collection-info-row { flex-direction: column; } + .collection-sort-bar { flex-direction: column; align-items: stretch; } + .collection-filter { justify-content: flex-end; } +} + +@media (max-width: 480px) { + .space-article-grid { grid-template-columns: 1fr; } + .space-username { font-size: 1rem; } + .space-avatar-img, .space-avatar-placeholder { width: 44px; height: 44px; } }