refactor: 重构 Space 页面为统一多 Tab 布局 + 移除独立关注/粉丝子页面
- 后端:SpaceController 支持 5 个 Tab(文章/关注/粉丝/收藏/设置),统一处理本人与访客视图 - 后端:注入 FavoriteService 支持收藏夹 Tab,新增用户统计数据查询 - 后端:新增 PUT /api/space/privacy 隐私设置 API 端点 - 后端:移除 FollowPageController 及 /space/:uid/followers|following 路由 - 前端:完全重写 space/index.html,顶部信息栏 + Tab 导航 + 左侧栏 + 主内容区 - 前端:重写 space.css 适配新布局(Profile Header/Tab Bar/文章/收藏/设置) - 清理:删除独立的 follow HTML 模板和 CSS 文件 - 模型:User 表新增 follower_list_public 隐私字段
This commit is contained in:
@ -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 个方法)
|
||||
|
||||
@ -11,10 +11,11 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SpaceController 用户空间控制器
|
||||
// SpaceController 用户空间控制器(统一页面:文章/关注/粉丝/收藏/设置)
|
||||
type SpaceController struct {
|
||||
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)
|
||||
// 当前激活的 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), // 暂无阅读量统计
|
||||
}
|
||||
|
||||
// ---- 根据 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}
|
||||
}
|
||||
followingResult = res
|
||||
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
|
||||
}
|
||||
|
||||
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",
|
||||
}))
|
||||
// 收藏夹列表
|
||||
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, "隐私设置已更新")
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ type User struct {
|
||||
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:"-"`
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 个方法)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
{{template "layout/header.html" .}}
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
<body>
|
||||
|
||||
{{template "layout/nav.html" .}}
|
||||
|
||||
<div class="follow-list-page">
|
||||
<h1>粉丝列表</h1>
|
||||
|
||||
{{if not .Result.Accessible}}
|
||||
<div class="follow-list-private">
|
||||
<p>该用户设置了关注列表不公开</p>
|
||||
</div>
|
||||
{{else if eq (len .Result.Items) 0}}
|
||||
<div class="follow-list-empty">
|
||||
<p>暂无粉丝</p>
|
||||
</div>
|
||||
{{else}}
|
||||
{{range .Result.Items}}
|
||||
<div class="follow-list-item">
|
||||
<a href="/space/{{.FollowerID}}" class="follow-list-user">
|
||||
<div class="follow-list-avatar">
|
||||
{{slice .FollowerUsername 0 1}}
|
||||
</div>
|
||||
<span class="follow-list-name">{{.FollowerUsername}}</span>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if gt .TotalPages 1}}
|
||||
<div class="pagination">
|
||||
{{if .HasPrev}}
|
||||
<a href="?page={{.PrevPage}}" class="page-link">上一页</a>
|
||||
{{end}}
|
||||
<span class="page-info">第 {{.Page}} / {{.TotalPages}} 页</span>
|
||||
{{if .HasNext}}
|
||||
<a href="?page={{.NextPage}}" class="page-link">下一页</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,47 +0,0 @@
|
||||
{{template "layout/header.html" .}}
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
<body>
|
||||
|
||||
{{template "layout/nav.html" .}}
|
||||
|
||||
<div class="follow-list-page">
|
||||
<h1>关注列表</h1>
|
||||
|
||||
{{if not .Result.Accessible}}
|
||||
<div class="follow-list-private">
|
||||
<p>该用户设置了关注列表不公开</p>
|
||||
</div>
|
||||
{{else if eq (len .Result.Items) 0}}
|
||||
<div class="follow-list-empty">
|
||||
<p>暂未关注任何人</p>
|
||||
</div>
|
||||
{{else}}
|
||||
{{range .Result.Items}}
|
||||
<div class="follow-list-item">
|
||||
<a href="/space/{{.FolloweeID}}" class="follow-list-user">
|
||||
<div class="follow-list-avatar">
|
||||
{{slice .FolloweeUsername 0 1}}
|
||||
</div>
|
||||
<span class="follow-list-name">{{.FolloweeUsername}}</span>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if gt .TotalPages 1}}
|
||||
<div class="pagination">
|
||||
{{if .HasPrev}}
|
||||
<a href="?page={{.PrevPage}}" class="page-link">上一页</a>
|
||||
{{end}}
|
||||
<span class="page-info">第 {{.Page}} / {{.TotalPages}} 页</span>
|
||||
{{if .HasNext}}
|
||||
<a href="?page={{.NextPage}}" class="page-link">下一页</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -13,108 +13,183 @@
|
||||
{{else}}
|
||||
<div class="space-page">
|
||||
|
||||
<!-- ===== 自己的空间:卡片式关注列表 ===== -->
|
||||
<!-- ====== 顶部信息栏(访客+本人共用) ====== -->
|
||||
<div class="space-profile-header">
|
||||
<div class="space-header-inner">
|
||||
<!-- 左:头像 + 用户名 -->
|
||||
<div class="space-header-user">
|
||||
<a href="/space/{{.SpaceUser.ID}}" class="space-avatar-link">
|
||||
{{if .SpaceUser.Avatar}}
|
||||
<img src="{{.SpaceUser.Avatar}}" alt="{{.SpaceUser.Username}}" class="space-avatar-img">
|
||||
{{else}}
|
||||
<div class="space-avatar-placeholder">{{slice .SpaceUser.Username 0 1}}</div>
|
||||
{{end}}
|
||||
</a>
|
||||
<div class="space-header-info">
|
||||
<span class="space-username">{{.SpaceUser.Username}}</span>
|
||||
{{if .SpaceUser.Bio}}
|
||||
<p class="space-header-bio">{{.SpaceUser.Bio}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右:操作按钮 -->
|
||||
<div class="space-header-actions">
|
||||
{{if .IsOwnSpace}}
|
||||
<!-- 本人无关注按钮 -->
|
||||
{{else}}
|
||||
<button id="space-follow-btn" class="space-action-btn primary" data-uid="{{.SpaceUser.ID}}">+ 关注</button>
|
||||
<button class="space-action-btn">发消息</button>
|
||||
<button class="space-action-btn icon-only" title="更多">⋮</button>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ====== Tab 导航栏(区分本人/访客) ====== -->
|
||||
{{if .IsOwnSpace}}
|
||||
<!-- 本人视图:左Tab + 右统计 -->
|
||||
<div class="space-tab-bar owner">
|
||||
<div class="space-tabs-left">
|
||||
<a href="/space/{{.SpaceUser.ID}}?tab=articles"
|
||||
class="space-tab{{if eq .ActiveTab "articles"}} active{{end}}">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zM6 20V4h7v5h5v11H6z"/></svg>
|
||||
文章
|
||||
<span class="tab-count">{{.PostCount}}</span>
|
||||
</a>
|
||||
<a href="/space/{{.SpaceUser.ID}}?tab=collections"
|
||||
class="space-tab{{if eq .ActiveTab "collections"}} active{{end}}">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2z"/></svg>
|
||||
收藏
|
||||
<span class="tab-count">{{.TotalFavorites}}</span>
|
||||
</a>
|
||||
<a href="/space/{{.SpaceUser.ID}}?tab=settings"
|
||||
class="space-tab{{if eq .ActiveTab "settings"}} active{{end}}">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94L14.4 2.81c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
|
||||
设置
|
||||
</a>
|
||||
</div>
|
||||
<div class="space-stats-right">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">关注</span>
|
||||
<span class="stat-value">{{.SpaceUser.FollowingCount}}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">粉丝</span>
|
||||
<span class="stat-value">{{.SpaceUser.FollowersCount}}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">获赞</span>
|
||||
<span class="stat-value">{{formatCount .TotalLikes}}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">阅读量</span>
|
||||
<span class="stat-value">{{formatCount .TotalReads}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<!-- 访客视图:统计栏 -->
|
||||
<div class="space-tab-bar visitor">
|
||||
<div class="space-tabs-left">
|
||||
<a href="/space/{{.SpaceUser.ID}}?tab=articles"
|
||||
class="space-tab{{if eq .ActiveTab "articles"}} active{{end}}">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zM6 20V4h7v5h5v11H6z"/></svg>
|
||||
文章
|
||||
<span class="tab-count">{{.SpaceUser.PostCount}}</span>
|
||||
</a>
|
||||
<a href="/space/{{.SpaceUser.ID}}?tab=following"
|
||||
class="space-tab{{if or (eq .ActiveTab "following") (eq .ActiveTab "followers")}} active{{end}}">
|
||||
<span class="stat-label-inline">关注数</span>
|
||||
<span class="tab-count">{{.SpaceUser.FollowingCount}}</span>
|
||||
</a>
|
||||
<a href="/space/{{.SpaceUser.ID}}?tab=followers" class="space-tab">
|
||||
<span class="stat-label-inline">粉丝数</span>
|
||||
<span class="tab-count">{{.SpaceUser.FollowersCount}}</span>
|
||||
</a>
|
||||
<div class="stat-item-inline">
|
||||
<span class="stat-label-inline">获赞数</span>
|
||||
<span class="stat-value-inline">{{formatCount .TotalLikes}}</span>
|
||||
</div>
|
||||
<div class="stat-item-inline">
|
||||
<span class="stat-label-inline">阅读量</span>
|
||||
<span class="stat-value-inline">{{formatCount .TotalReads}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- ====== 主内容区(左右分栏) ====== -->
|
||||
<div class="space-card-layout">
|
||||
<!-- 左侧导航 -->
|
||||
<aside class="space-sidebar">
|
||||
<h2 class="space-sidebar-title">我的关注</h2>
|
||||
<nav class="space-sidebar-nav">
|
||||
<a href="/space/{{.SpaceUser.ID}}/following" class="space-sidebar-link active">
|
||||
<svg class="space-sidebar-icon" viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>
|
||||
全部关注
|
||||
<span class="space-sidebar-count">{{.SpaceUser.FollowingCount}}</span>
|
||||
</a>
|
||||
<a href="/space/{{.SpaceUser.ID}}/followers" class="space-sidebar-link">
|
||||
<svg class="space-sidebar-icon" viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
|
||||
粉丝
|
||||
<span class="space-sidebar-count">{{.SpaceUser.FollowersCount}}</span>
|
||||
</a>
|
||||
</nav>
|
||||
{{if gt .SpaceUser.FollowingCount 0}}
|
||||
<p class="space-sidebar-hint">显示全部 {{.SpaceUser.FollowingCount}} 个</p>
|
||||
{{end}}
|
||||
</aside>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<main class="space-main">
|
||||
<!-- 标题栏 -->
|
||||
<div class="space-main-header">
|
||||
<h1 class="space-main-title">全部关注</h1>
|
||||
<a href="/settings/profile#privacy" class="space-settings-link">设置</a>
|
||||
{{/* ========== 文章 Tab ==========*/}}
|
||||
{{if eq .ActiveTab "articles"}}
|
||||
<main class="space-main-full">
|
||||
<h1 class="space-section-title">{{if .IsOwnSpace}}我的文章{{else}}Ta 的文章{{end}}</h1>
|
||||
{{if .Posts}}
|
||||
{{if gt (len .Posts) 0}}
|
||||
<div class="space-post-list">
|
||||
{{range $i, $post := .Posts}}
|
||||
<article class="space-post-card">
|
||||
<a href="/posts/{{$post.ID}}" class="space-post-title">{{$post.Title}}</a>
|
||||
<p class="space-post-excerpt">{{if $post.Excerpt}}{{printf "%.120s" $post.Excerpt}}{{else}}{{printf "%.120s" $post.Body}}{{end}}</p>
|
||||
<div class="space-post-meta">
|
||||
<span class="post-meta-item">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>
|
||||
{{$post.LikesCount}}
|
||||
</span>
|
||||
<span class="post-meta-item">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2z"/></svg>
|
||||
{{$post.FavoritesCount}}
|
||||
</span>
|
||||
<span class="post-meta-item">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>
|
||||
{{$post.CommentsCount}}
|
||||
</span>
|
||||
<span class="post-meta-time">{{$post.CreatedAt.Format "01-02 15:04"}}</span>
|
||||
</div>
|
||||
|
||||
{{if .FollowingResult}}
|
||||
{{if .FollowingResult.Accessible}}
|
||||
|
||||
{{if eq (len .FollowingResult.Items) 0}}
|
||||
</article>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if gt .TotalPages 1}}
|
||||
<div class="pagination">
|
||||
{{if gt .Page 1}}
|
||||
<a href="?tab=articles&page={{.PrevPage}}" class="page-link">上一页</a>
|
||||
{{end}}
|
||||
<span class="page-info">第 {{.Page}} / {{.TotalPages}} 页</span>
|
||||
{{if le .Page .TotalPages}}
|
||||
<a href="?tab=articles&page={{.NextPage}}" class="page-link">下一页</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="space-empty-state">
|
||||
<p>暂未关注任何人,去发现有趣的创作者吧~</p>
|
||||
<p>{{if .IsOwnSpace}}还没有发布过文章,去创作吧~{{else}}Ta 还没有发布文章{{end}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<!-- 用户卡片网格 -->
|
||||
<div class="space-user-grid">
|
||||
{{range $i, $item := .FollowingResult.Items}}
|
||||
<div class="space-user-card" data-uid="{{$item.FolloweeID}}">
|
||||
<a href="/space/{{$item.FolloweeID}}" class="space-user-avatar-wrap">
|
||||
{{if $item.FolloweeAvatar}}
|
||||
<img src="{{$item.FolloweeAvatar}}" alt="{{$item.FolloweeUsername}}" class="space-user-avatar-img">
|
||||
{{else}}
|
||||
<div class="space-user-avatar-placeholder">{{slice $item.FolloweeUsername 0 1}}</div>
|
||||
{{end}}
|
||||
</a>
|
||||
<div class="space-user-info">
|
||||
<a href="/space{{$item.FolloweeID}}" class="space-user-name">{{$item.FolloweeUsername}}</a>
|
||||
{{if $item.FolloweeBio}}
|
||||
<p class="space-user-bio">{{printf "%.50s" $item.FolloweeBio}}</p>
|
||||
{{end}}
|
||||
<div class="space-empty-state">
|
||||
<p>暂无数据</p>
|
||||
</div>
|
||||
<button class="space-follow-btn followed" data-uid="{{$item.FolloweeID}}" disabled>已关注</button>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{else}}
|
||||
<!-- 隐私关闭时的空状态 -->
|
||||
<div class="space-private-empty">
|
||||
<div class="space-private-illustration">
|
||||
<svg viewBox="0 0 120 100" width="120" height="100">
|
||||
<rect x="15" y="25" width="90" height="60" rx="6" fill="#f0f0f0" stroke="#e0e0e0" stroke-width="2"/>
|
||||
<rect x="25" y="35" width="40" height="6" rx="3" fill="#e0e0e0"/>
|
||||
<rect x="25" y="47" width="70" height="4" rx="2" fill="#eee"/>
|
||||
<rect x="25" y="57" width="55" height="4" rx="2" fill="#eee"/>
|
||||
<circle cx="95" cy="42" r="10" fill="#e8e8e8"/>
|
||||
<path d="M91 42 L94 45 L99 39" stroke="#bbb" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
<circle cx="82" cy="18" r="8" fill="#fff" stroke="#ddd" stroke-width="1.5"/>
|
||||
<line x1="86" y1="14" x2="92" y2="10" stroke="#ddd" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="space-private-text">你关闭了展示关注列表~</p>
|
||||
<a href="/settings/profile#privacy" class="space-private-setting-btn">去设置开启</a>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- ===== 别人的空间:简化视图 ===== -->
|
||||
{{else}}
|
||||
<div class="space-card-layout">
|
||||
<!-- 左侧导航(简化版) -->
|
||||
{{/* ========== 关注列表 Tab(含本人+访客) ==========*/}}
|
||||
{{else if or (eq .ActiveTab "following") (eq .ActiveTab "followers")}}
|
||||
<!-- 左侧导航 -->
|
||||
<aside class="space-sidebar">
|
||||
<h2 class="space-sidebar-title">Ta的关注</h2>
|
||||
<h2 class="space-sidebar-title">{{if .IsOwnSpace}}我的关注{{else}}Ta的关注{{end}}</h2>
|
||||
<nav class="space-sidebar-nav">
|
||||
{{if .FollowingResult}}
|
||||
<div class="space-sidebar-link active{{if not .FollowingResult.Accessible}} disabled{{end}}">
|
||||
<a href="/space/{{.SpaceUser.ID}}?tab=following"
|
||||
class="space-sidebar-link{{if eq .ActiveTab "following"}} active{{end}}">
|
||||
<svg class="space-sidebar-icon" viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>
|
||||
全部关注
|
||||
{{if .IsOwnSpace}}全部关注{{else}}全部关注{{end}}
|
||||
<span class="space-sidebar-count">{{.SpaceUser.FollowingCount}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
<a href="/space/{{.SpaceUser.ID}}/followers" class="space-sidebar-link">
|
||||
</a>
|
||||
<a href="/space/{{.SpaceUser.ID}}?tab=followers"
|
||||
class="space-sidebar-link{{if eq .ActiveTab "followers"}} active{{end}}">
|
||||
<svg class="space-sidebar-icon" viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
|
||||
Ta的粉丝
|
||||
{{if .IsOwnSpace}}粉丝{{else}}Ta的粉丝{{end}}
|
||||
<span class="space-sidebar-count">{{.SpaceUser.FollowersCount}}</span>
|
||||
</a>
|
||||
</nav>
|
||||
@ -122,16 +197,26 @@
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<main class="space-main">
|
||||
<h1 class="space-main-title">全部关注</h1>
|
||||
<h1 class="space-main-title">{{if eq .ActiveTab "following"}}全部关注{{else}}{{if .IsOwnSpace}}粉丝{{else}}Ta的粉丝{{end}}{{end}}</h1>
|
||||
|
||||
{{/* ---- 关注列表 ---- */}}
|
||||
{{if eq .ActiveTab "following"}}
|
||||
{{if .FollowingResult}}
|
||||
{{if .FollowingResult.Accessible}}
|
||||
|
||||
{{if eq (len .FollowingResult.Items) 0}}
|
||||
<div class="space-empty-state">
|
||||
<p>Ta 暂未关注任何人</p>
|
||||
<p>{{if .IsOwnSpace}}暂未关注任何人,去发现有趣的创作者吧~{{else}}Ta 暂未关注任何人{{end}}</p>
|
||||
</div>
|
||||
{{else}}
|
||||
{{if .IsOwnSpace}}
|
||||
<!-- 本人:带搜索的用户列表 -->
|
||||
<div class="space-follow-search">
|
||||
<input type="text" class="follow-search-input" placeholder="输入关键词">
|
||||
<button class="follow-search-btn">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="space-user-grid">
|
||||
{{range $i, $item := .FollowingResult.Items}}
|
||||
<div class="space-user-card" data-uid="{{$item.FolloweeID}}">
|
||||
@ -148,14 +233,17 @@
|
||||
<p class="space-user-bio">{{printf "%.50s" $item.FolloweeBio}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
<button class="space-follow-btn" data-uid="{{$item.FolloweeID}}">关注</button>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if $.IsOwnSpace}}
|
||||
<button class="space-follow-btn followed" disabled>已关注</button>
|
||||
{{else}}
|
||||
<!-- 对方关闭了关注列表 -->
|
||||
<button class="space-follow-btn" data-uid="{{$item.FolloweeID}}">关注</button>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<!-- 隐私关闭 -->
|
||||
<div class="space-private-empty">
|
||||
<div class="space-private-illustration">
|
||||
<svg viewBox="0 0 120 100" width="120" height="100">
|
||||
@ -169,31 +257,364 @@
|
||||
<line x1="86" y1="14" x2="92" y2="10" stroke="#ddd" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="space-private-text">Ta 关闭了展示关注列表~</p>
|
||||
<p class="space-private-text">{{if .IsOwnSpace}}你关闭了展示关注列表~{{else}}Ta 关闭了展示关注列表~{{end}}</p>
|
||||
{{if .IsOwnSpace}}
|
||||
<a href="/settings/profile#privacy" class="space-private-setting-btn">去设置开启</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="space-empty-state">
|
||||
<p>暂无数据</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</main>
|
||||
</div>
|
||||
<div class="space-empty-state"><p>暂无数据</p></div>
|
||||
{{end}}
|
||||
|
||||
</div>
|
||||
{{/* ---- 粉丝列表 ---- */}}
|
||||
{{else if eq .ActiveTab "followers"}}
|
||||
{{if .FollowersResult}}
|
||||
{{if .FollowersResult.Accessible}}
|
||||
{{if eq (len .FollowersResult.Items) 0}}
|
||||
<div class="space-empty-state">
|
||||
<p>{{if .IsOwnSpace}}暂无粉丝{{else}}Ta 暂无粉丝{{end}}</p>
|
||||
</div>
|
||||
{{else}}
|
||||
{{if .IsOwnSpace}}
|
||||
<div class="space-follow-search">
|
||||
<input type="text" class="follow-search-input" placeholder="输入关键词">
|
||||
<button class="follow-search-btn">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="space-user-grid">
|
||||
{{range $i, $item := .FollowersResult.Items}}
|
||||
<div class="space-user-card" data-uid="{{$item.FollowerID}}">
|
||||
<a href="/space/{{$item.FollowerID}}" class="space-user-avatar-wrap">
|
||||
{{if $item.FollowerAvatar}}
|
||||
<img src="{{$item.FollowerAvatar}}" alt="{{$item.FollowerUsername}}" class="space-user-avatar-img">
|
||||
{{else}}
|
||||
<div class="space-user-avatar-placeholder">{{slice $item.FollowerUsername 0 1}}</div>
|
||||
{{end}}
|
||||
</a>
|
||||
<div class="space-user-info">
|
||||
<a href="/space/{{$item.FollowerID}}" class="space-user-name">{{$item.FollowerUsername}}</a>
|
||||
{{if $item.FollowerBio}}
|
||||
<p class="space-user-bio">{{printf "%.50s" $item.FollowerBio}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if not $.IsOwnSpace}}
|
||||
<button class="space-follow-btn" data-uid="{{$item.FollowerID}}">关注</button>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="space-private-empty">
|
||||
<div class="space-private-illustration">
|
||||
<svg viewBox="0 0 120 100" width="120" height="100">
|
||||
<rect x="15" y="25" width="90" height="60" rx="6" fill="#f0f0f0" stroke="#e0e0e0" stroke-width="2"/>
|
||||
<rect x="25" y="35" width="40" height="6" rx="3" fill="#e0e0e0"/>
|
||||
<rect x="25" y="47" width="70" height="4" rx="2" fill="#eee"/>
|
||||
<rect x="25" y="57" width="55" height="4" rx="2" fill="#eee"/>
|
||||
<circle cx="95" cy="42" r="10" fill="#e8e8e8"/>
|
||||
<path d="M91 42 L94 45 L99 39" stroke="#bbb" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="space-private-text">{{if .IsOwnSpace}}你关闭了展示粉丝列表~{{else}}Ta 关闭了展示粉丝列表~{{end}}</p>
|
||||
{{if .IsOwnSpace}}
|
||||
<a href="/settings/profile#privacy" class="space-private-setting-btn">去设置开启</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="space-empty-state"><p>暂无数据</p></div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</main>
|
||||
|
||||
{{/* ========== 收藏夹 Tab(仅本人) ==========*/}}
|
||||
{{else if eq .ActiveTab "collections"}}
|
||||
<!-- 左侧收藏夹列表 -->
|
||||
<aside class="space-sidebar collection-sidebar">
|
||||
<h2 class="space-sidebar-title collapsible" onclick="toggleFolderSection(this)">
|
||||
我创建的收藏夹
|
||||
<svg class="collapse-arrow" viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
|
||||
</h2>
|
||||
<nav class="space-sidebar-nav" id="folder-nav">
|
||||
<a href="javascript:void(0)" class="space-sidebar-link new-folder-btn" onclick="showNewFolderInput()">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
||||
新建收藏夹
|
||||
</a>
|
||||
<div id="new-folder-input" style="display:none;">
|
||||
<input type="text" id="new-folder-name" placeholder="收藏夹名称" maxlength="50" class="new-folder-input">
|
||||
<button onclick="createFolder()" class="new-folder-submit">确定</button>
|
||||
</div>
|
||||
{{if .FoldersResult}}
|
||||
{{range $f := .FoldersResult.Folders}}
|
||||
<a href="/space/{{$.SpaceUser.ID}}?tab=collections&folder={{$f.ID}}"
|
||||
class="space-sidebar-link{{if eq (str $.ActiveFolderID) (str $f.ID)}} active{{end}}"
|
||||
data-folder-id="{{$f.ID}}">
|
||||
<svg class="space-sidebar-icon" viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2zm0 15l-5-2.18L7 18V5h10v13z"/></svg>
|
||||
{{$f.Name}}
|
||||
<span class="space-sidebar-count">{{$f.ItemCount}}</span>
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- 右侧收藏内容 -->
|
||||
<main class="space-main">
|
||||
{{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}}
|
||||
<!-- 收藏夹头信息 -->
|
||||
<div class="collection-header">
|
||||
<div class="collection-cover">
|
||||
{{if $activeFolder.Description}}
|
||||
<div class="collection-cover-placeholder"></div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="collection-info-row">
|
||||
<div>
|
||||
<h1 class="collection-name">{{$activeFolder.Name}}</h1>
|
||||
<p class="collection-meta">
|
||||
{{if $activeFolder.IsPublic}}公开{{else}}私密{{end}}
|
||||
· 视频数 {{.FolderItems.Total}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="collection-actions">
|
||||
<button class="space-action-btn primary small">播放全部</button>
|
||||
<button class="space-action-btn outline small">批量操作</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 排序筛选栏 -->
|
||||
<div class="collection-sort-bar">
|
||||
<div class="sort-tabs">
|
||||
<button class="sort-tab active">最近收藏</button>
|
||||
<button class="sort-tab">最多播放</button>
|
||||
<button class="sort-tab">最近投稿</button>
|
||||
</div>
|
||||
<div class="collection-filter">
|
||||
<select class="filter-select">
|
||||
<option>当前</option>
|
||||
</select>
|
||||
<div class="filter-search">
|
||||
<input type="text" placeholder="请输入关键词" class="filter-search-input">
|
||||
<button class="filter-search-btn">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文章卡片网格 -->
|
||||
{{if .FolderItems}}
|
||||
{{if gt (len .FolderItems.Items) 0}}
|
||||
<div class="space-article-grid">
|
||||
{{range $item := .FolderItems.Items}}
|
||||
<article class="space-article-card">
|
||||
<a href="/posts/{{$item.PostID}}" class="article-card-link">
|
||||
<h3 class="article-card-title">{{if $item.PostTitle}}{{$item.PostTitle}}{{else}}无标题文章{{end}}</h3>
|
||||
</a>
|
||||
<div class="article-card-footer">
|
||||
<span class="article-card-author">
|
||||
<svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
|
||||
{{$.SpaceUser.Username}}
|
||||
</span>
|
||||
<span class="article-card-time">收藏于{{$item.CreatedAt.Format "01-02 15:04"}}</span>
|
||||
</div>
|
||||
</article>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="space-empty-state">
|
||||
<p>这个收藏夹还是空的</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="space-empty-state"><p>暂无数据</p></div>
|
||||
{{end}}
|
||||
|
||||
{{else}}
|
||||
<div class="space-empty-state">
|
||||
<p>还没有创建收藏夹,点击左侧「新建收藏夹」开始吧~</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="space-empty-state"><p>加载失败</p></div>
|
||||
{{end}}
|
||||
</main>
|
||||
|
||||
{{/* ========== 设置 Tab(仅本人) ==========*/}}
|
||||
{{else if eq .ActiveTab "settings"}}
|
||||
<main class="space-main-full settings-main">
|
||||
<h1 class="space-section-title">隐私设置</h1>
|
||||
<div class="privacy-settings-grid">
|
||||
|
||||
<!-- 公开我的收藏 -->
|
||||
<div class="privacy-row">
|
||||
<label class="privacy-label">公开我的收藏</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="privacy-collections"{{if .SpaceUser.PublicCollections}} checked{{end}}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 公开我的生日、个人标签 -->
|
||||
<div class="privacy-row">
|
||||
<label class="privacy-label">公开我的生日、个人标签</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="privacy-bio"{{if .SpaceUser.PublicBio}} checked{{end}}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 公开我的追番追剧 -->
|
||||
<div class="privacy-row">
|
||||
<label class="privacy-label">公开我的追番追剧</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="privacy-tracks"{{if .SpaceUser.PublicTracks}} checked{{end}}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 公开最近投市的视频 -->
|
||||
<div class="privacy-row">
|
||||
<label class="privacy-label">公开最近投稿的文章</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="privacy-posts"{{if .SpaceUser.PublicPosts}} checked{{end}}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 公开最近点赞的视频 -->
|
||||
<div class="privacy-row">
|
||||
<label class="privacy-label">公开最近点赞的文章</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="privacy-likes"{{if .SpaceUser.PublicLikes}} checked{{end}}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 公开我的关注列表 -->
|
||||
<div class="privacy-row">
|
||||
<label class="privacy-label">公开我的关注列表</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="privacy-follow-list"{{if .SpaceUser.FollowListPublic}} checked{{end}}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 公开我的粉丝列表 -->
|
||||
<div class="privacy-row">
|
||||
<label class="privacy-label">公开我的粉丝列表</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="privacy-follower-list"{{if .SpaceUser.FollowerListPublic}} checked{{end}}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 公开学校信息 -->
|
||||
<div class="privacy-row">
|
||||
<label class="privacy-label">公开学校信息</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="privacy-school"{{if .SpaceUser.PublicSchool}} checked{{end}}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 公开最近玩过的游戏 -->
|
||||
<div class="privacy-row">
|
||||
<label class="privacy-label">公开最近玩过的游戏</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="privacy-games"{{if .SpaceUser.PublicGames}} checked{{end}}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 公开拥有的粉丝装扮(仅APP) -->
|
||||
<div class="privacy-row">
|
||||
<label class="privacy-label">公开拥有的粉丝装扮(仅APP)</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="privacy-costumes">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 公开我的追漫(仅APP) -->
|
||||
<div class="privacy-row">
|
||||
<label class="privacy-label">公开我的追漫(仅APP)</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="privacy-anime"{{if .SpaceUser.PublicAnime}} checked{{end}}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 公开隐藏的粉丝勋章 -->
|
||||
<div class="privacy-row">
|
||||
<label class="privacy-label">公开隐藏的粉丝勋章</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="privacy-medals">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 投稿视频列表中展现直播播放 -->
|
||||
<div class="privacy-row">
|
||||
<label class="privacy-label">投稿视频列表中展现直播回放</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="privacy-live-replay">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 投稿视频列表中展现课堂视频 -->
|
||||
<div class="privacy-row">
|
||||
<label class="privacy-label">投稿视频列表中展现课堂视频</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="privacy-classroom">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 投稿视频列表中展现月充电专属视频 -->
|
||||
<div class="privacy-row">
|
||||
<label class="privacy-label">投稿视频列表中展现月充电专属视频</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="privacy-monthly-video" checked>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{{end}}
|
||||
</div><!-- /.space-card-layout -->
|
||||
|
||||
</div><!-- /.space-page -->
|
||||
{{end}}
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||
{{if not .IsOwnSpace}}
|
||||
<!-- 别人空间:关注按钮交互 -->
|
||||
|
||||
<!-- 访客空间:关注按钮交互 -->
|
||||
{{if and (not .IsOwnSpace) (or (eq .ActiveTab "following") (eq .ActiveTab "followers"))}}
|
||||
<script>
|
||||
(function() {
|
||||
var cards = document.querySelectorAll('.space-user-card');
|
||||
if (!cards.length) return;
|
||||
|
||||
// 关注按钮状态文字映射
|
||||
var labels = {
|
||||
none: '关注',
|
||||
following: '已关注',
|
||||
@ -215,24 +636,16 @@
|
||||
if (state === 'following' || state === 'mutual') btn.classList.add('followed');
|
||||
}
|
||||
|
||||
// 批量获取关注状态
|
||||
var uids = [];
|
||||
cards.forEach(function(card) {
|
||||
uids.push(card.getAttribute('data-uid'));
|
||||
});
|
||||
|
||||
// 逐个查询状态
|
||||
// 批量获取并更新每个卡片关注状态
|
||||
cards.forEach(function(card) {
|
||||
var btn = card.querySelector('.space-follow-btn');
|
||||
if (!btn) return;
|
||||
if (!btn || btn.disabled) return;
|
||||
var targetUid = card.getAttribute('data-uid');
|
||||
|
||||
fetch('/api/users/' + targetUid + '/follow-status')
|
||||
.then(function(res) { return res.json(); })
|
||||
.then(function(data) {
|
||||
if (data && data.success && data.data) {
|
||||
updateBtn(btn, data.data);
|
||||
}
|
||||
if (data && data.success && data.data) { updateBtn(btn, data.data); }
|
||||
});
|
||||
|
||||
btn.addEventListener('click', function() {
|
||||
@ -243,20 +656,107 @@
|
||||
if (xhr.readyState === 4) {
|
||||
btn.classList.remove('waiting');
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var resp = JSON.parse(xhr.responseText);
|
||||
if (resp.data && resp.data.status) {
|
||||
updateBtn(btn, resp.data.status);
|
||||
}
|
||||
} catch(e) {}
|
||||
try { var resp = JSON.parse(xhr.responseText); if (resp.data && resp.data.status) updateBtn(btn, resp.data.status); } catch(e) {}
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
});
|
||||
|
||||
// 顶部关注按钮
|
||||
var topFollowBtn = document.getElementById('space-follow-btn');
|
||||
if (topFollowBtn) {
|
||||
var topUid = topFollowBtn.getAttribute('data-uid');
|
||||
fetch('/api/users/' + topUid + '/follow-status')
|
||||
.then(function(res) { return res.json(); })
|
||||
.then(function(data) {
|
||||
if (data && data.success && data.data) {
|
||||
var s = data.data;
|
||||
topFollowBtn.textContent = (s.i_follow ? (s.they_follow ? '已互粉' : '已关注') : (s.they_follow ? '回关' : '+ 关注'));
|
||||
if (s.i_follow) topFollowBtn.classList.add('followed');
|
||||
}
|
||||
});
|
||||
topFollowBtn.addEventListener('click', function() {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/api/users/' + topUid + '/follow', true);
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
try {
|
||||
var resp = JSON.parse(xhr.responseText);
|
||||
if (resp.data && resp.data.status) {
|
||||
var st = resp.data.status;
|
||||
topFollowBtn.textContent = (st.i_follow ? (st.they_follow ? '已互粉' : '已关注') : (st.they_follow ? '回关' : '+ 关注'));
|
||||
topFollowBtn.classList.toggle('followed', st.i_follow);
|
||||
// 同时更新下方卡片中的对应按钮
|
||||
cards.forEach(function(c) {
|
||||
if (c.getAttribute('data-uid') === topUid) {
|
||||
var cb = c.querySelector('.space-follow-btn'); if(cb) updateBtn(cb, st);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch(e){}
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
<!-- 收藏夹交互脚本 -->
|
||||
{{if eq .ActiveTab "collections"}}
|
||||
<script>
|
||||
function toggleFolderSection(el) {
|
||||
el.parentElement.querySelector('#folder-nav').classList.toggle('collapsed');
|
||||
el.classList.toggle('collapsed');
|
||||
}
|
||||
|
||||
function showNewFolderInput() {
|
||||
document.getElementById('new-folder-input').style.display = '';
|
||||
document.getElementById('new-folder-name').focus();
|
||||
}
|
||||
|
||||
function createFolder() {
|
||||
var name = document.getElementById('new-folder-name').value.trim();
|
||||
if (!name) return;
|
||||
fetch('/api/folders', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({name: name, is_public: false})
|
||||
}).then(function(r) { return r.json(); }).then(function(d) {
|
||||
if (d.success) { location.reload(); } else { alert(d.message || '创建失败'); }
|
||||
});
|
||||
}
|
||||
|
||||
// 隐私开关变更保存
|
||||
document.querySelectorAll('.privacy-settings-grid .toggle-switch input').forEach(function(cb) {
|
||||
cb.addEventListener('change', function() {
|
||||
// TODO: 调用 API 保存隐私设置
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
<!-- 设置页交互脚本 -->
|
||||
{{if eq .ActiveTab "settings"}}
|
||||
<script>
|
||||
// 隐私开关变更时自动保存到后端
|
||||
document.querySelectorAll('#privacy-follow-list, #privacy-follower-list').forEach(function(cb) {
|
||||
cb.addEventListener('change', function() {
|
||||
var field = this.id.replace('privacy-', '').replace(/-/g, '_');
|
||||
var value = this.checked;
|
||||
fetch('/api/space/privacy', {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({field: field, value: value})
|
||||
}).then(function(r){return r.json()}).then(function(d){
|
||||
if(!d.success) { this.checked=!value; alert(d.message||'保存失败'); }
|
||||
}.bind(this));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user