From e2e5ba077c4602763c5e1176d707515f14683e81 Mon Sep 17 00:00:00 2001 From: freefire Date: Mon, 14 Sep 2026 16:10:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=B8=AA=E4=BA=BA=E8=B5=84=E6=96=99=E7=BC=96=E8=BE=91=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96=E9=A1=B5=E9=9D=A2=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E4=B8=8E=E5=AF=BC=E8=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 后端新增用户资料更新接口,支持修改昵称、头像、邮箱和签名 2. 前端新增资料编辑表单页面,适配响应式布局 3. 优化帖子详情页布局与移动端适配逻辑 4. 重构首页侧边栏导航,使用卡片式板块展示 5. 移除全局板块缓存,调整Header导航移除板块菜单 6. 补充用户签名展示功能,完善用户信息返回字段 --- backend/handler/auth.go | 12 +- backend/handler/user.go | 38 ++++++ backend/model/models.go | 1 + backend/router/router.go | 1 + backend/service/auth.go | 45 +++++++ frontend/app/globals.css | 52 ++++++++ frontend/app/layout.tsx | 9 +- frontend/app/post/[id]/page.tsx | 14 +-- frontend/app/settings/ProfileForm.tsx | 145 ++++++++++++++++++++++ frontend/app/settings/page.tsx | 8 +- frontend/app/u/[id]/page.tsx | 5 + frontend/components/Header.tsx | 129 +------------------ frontend/components/home/WorkspaceNav.tsx | 81 ++++++++---- frontend/lib/api.ts | 19 +++ frontend/lib/serverData.ts | 12 +- frontend/next-env.d.ts | 4 +- 16 files changed, 388 insertions(+), 187 deletions(-) create mode 100644 frontend/app/settings/ProfileForm.tsx diff --git a/backend/handler/auth.go b/backend/handler/auth.go index 8758cca..d813e09 100644 --- a/backend/handler/auth.go +++ b/backend/handler/auth.go @@ -224,11 +224,13 @@ func (h *Handlers) ChangePassword(c *gin.Context) { // meUserBody 统一的用户信息响应体(Me/Login/Refresh 共用形状) func meUserBody(user *model.User) gin.H { return gin.H{ - "id": user.ID, - "username": user.Username, - "nickname": user.Nickname, - "avatar": user.Avatar, - "role": user.Role, + "id": user.ID, + "username": user.Username, + "nickname": user.Nickname, + "avatar": user.Avatar, + "signature": user.Signature, + "email": user.Email, + "role": user.Role, } } diff --git a/backend/handler/user.go b/backend/handler/user.go index e3d85cb..21c62bc 100644 --- a/backend/handler/user.go +++ b/backend/handler/user.go @@ -8,6 +8,43 @@ import ( "github.com/gin-gonic/gin" ) +// UpdateProfileRequest 更新资料请求 +type UpdateProfileRequest struct { + Nickname string `json:"nickname"` + Avatar string `json:"avatar"` + Email string `json:"email"` + Signature string `json:"signature"` +} + +// UpdateProfile 更新当前登录用户的资料(昵称、头像、邮箱、签名) +func (h *Handlers) UpdateProfile(c *gin.Context) { + claims := middleware.CurrentUser(c) + var req UpdateProfileRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数格式错误"}) + return + } + + user, err := h.Auth.UpdateProfile(claims.ID, req.Nickname, req.Avatar, req.Email, req.Signature) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 返回更新后的完整资料(含 email/signature,供前端刷新展示) + c.JSON(http.StatusOK, gin.H{ + "user": gin.H{ + "id": user.ID, + "username": user.Username, + "nickname": user.Nickname, + "avatar": user.Avatar, + "signature": user.Signature, + "email": user.Email, + "role": user.Role, + }, + }) +} + // UserProfile 获取用户资料:基本信息 + 统计 + 发帖列表(分页) func (h *Handlers) UserProfile(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) @@ -58,6 +95,7 @@ func (h *Handlers) UserProfile(c *gin.Context) { "username": user.Username, "nickname": user.Nickname, "avatar": user.Avatar, + "signature": user.Signature, "role": user.Role, "created_at": user.CreatedAt, }, diff --git a/backend/model/models.go b/backend/model/models.go index d212d21..2c2b83e 100644 --- a/backend/model/models.go +++ b/backend/model/models.go @@ -35,6 +35,7 @@ type User struct { Password string `gorm:"size:128;not null" json:"-"` Nickname string `gorm:"size:64" json:"nickname"` Avatar string `gorm:"size:512" json:"avatar"` + Signature string `gorm:"size:255;default:''" json:"signature"` // 个性签名 Role Role `gorm:"size:16;default:user" json:"role"` Banned bool `gorm:"default:false" json:"banned"` TokenVersion int `gorm:"default:0" json:"-"` // token 版本号,改密码/封禁时递增使旧 JWT 失效 diff --git a/backend/router/router.go b/backend/router/router.go index cf85284..544a1cd 100644 --- a/backend/router/router.go +++ b/backend/router/router.go @@ -92,6 +92,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) { { api.POST("/logout", h.Logout) api.POST("/change-password", h.ChangePassword) + api.PUT("/profile", h.UpdateProfile) api.POST("/posts", middleware.RateLimitMiddleware(limiter, service.RatePost), h.CreatePost) api.PUT("/posts/:id", h.UpdatePost) api.DELETE("/posts/:id", h.DeletePost) diff --git a/backend/service/auth.go b/backend/service/auth.go index de7ef0f..a2f72fe 100644 --- a/backend/service/auth.go +++ b/backend/service/auth.go @@ -10,6 +10,7 @@ import ( "errors" "io" "log" + "strings" "time" "github.com/freefire/jiang13-bbs/model" @@ -491,3 +492,47 @@ func (s *AuthService) ChangePassword(userID uint, oldPassword, newPassword strin // 递增 token_version,使所有旧 JWT 和 refresh token 失效 return s.IncrementTokenVersion(userID) } + +// UpdateProfile 更新用户资料(昵称、头像、邮箱、签名) +// - nickname 非空且不超过 64 字符 +// - email 可为空;非空时需符合邮箱格式且不与他人重复 +// - avatar 可为空 URL +// - signature 不超过 255 字符 +func (s *AuthService) UpdateProfile(userID uint, nickname, avatar, email, signature string) (*model.User, error) { + nickname = strings.TrimSpace(nickname) + if nickname == "" { + return nil, errors.New("昵称不能为空") + } + if len(nickname) > 64 { + return nil, errors.New("昵称不能超过 64 个字符") + } + email = strings.TrimSpace(email) + if email != "" { + if !strings.Contains(email, "@") || len(email) > 128 { + return nil, errors.New("邮箱格式不正确") + } + // 邮箱唯一性校验(排除自身) + var count int64 + s.db.Model(&model.User{}).Where("email = ? AND id <> ?", email, userID).Count(&count) + if count > 0 { + return nil, errors.New("该邮箱已被使用") + } + } + if len(avatar) > 512 { + return nil, errors.New("头像链接过长") + } + if len(signature) > 255 { + return nil, errors.New("签名不能超过 255 个字符") + } + + updates := map[string]any{ + "nickname": nickname, + "avatar": avatar, + "email": email, + "signature": signature, + } + if err := s.db.Model(&model.User{}).Where("id = ?", userID).Updates(updates).Error; err != nil { + return nil, err + } + return s.GetUserByID(userID) +} diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 162f557..0e6f0cd 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -559,6 +559,58 @@ h1, h2, h3, h4 { font-weight: 700; } + /* 首页左栏「板块目录」卡片:与面包屑的纯文字位置指引区分, + 承载板块介绍与讨论量等发现型信息,点击在工作台内筛选帖子流 */ + .j13-bcard { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 9px 10px; + background: var(--panel); + border: 1px solid var(--line); + border-radius: var(--r-sm); + transition: border-color 0.16s ease, background 0.16s ease; + } + .j13-bcard:hover { + border-color: var(--nav-active-line); + background: var(--panel-2); + } + .j13-bcard:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } + .j13-bcard[aria-current="page"] { + border-color: var(--accent); + background: var(--nav-active-bg); + } + .j13-bcard-ico { + width: 34px; + height: 34px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: 800; + flex-shrink: 0; + } + .j13-bcard-desc { + display: block; + margin-top: 2px; + font-size: 11.5px; + line-height: 1.4; + color: var(--ink-3); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + @media (prefers-reduced-motion: reduce) { + .j13-bcard { + transition: none; + } + } + /* 工作台模块卡 */ .j13-module { background: var(--panel); diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index b7f4e55..cfdd511 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -5,8 +5,8 @@ import "./globals.css"; import Header from "@/components/Header"; import Footer from "@/components/Footer"; import Toaster from "@/components/Toaster"; -import type { User, Board } from "@/lib/api"; -import { getMeCached, getBoardsCached } from "@/lib/serverData"; +import type { User } from "@/lib/api"; +import { getMeCached } from "@/lib/serverData"; import { TOKEN_COOKIE, REFRESH_COOKIE, authCookieHeader } from "@/lib/cookies"; // 构建期下载并自托管字体:同源加载 + 自动 preload + fallback 度量校正, @@ -69,9 +69,6 @@ export default async function RootLayout({ unread = me.unread_count; } - // 全站板块导航(Header 桌面下拉 + 移动抽屉共用) - const boards: Board[] = await getBoardsCached(); - return ( 跳到主要内容 -
+
- {/* 三栏阅读布局:xl = 左导航240 + 正文 + 右辅助280;lg = 正文 + 右辅助260;以下单栏 */} -
+ {/* 三栏阅读布局:宽度与首页工作台一致——lg = 左224 + 正文 + 右280,xl = 左240 + 正文 + 右300;以下单栏 */} +
{/* 左栏:板块 / 作者 / 作者其他帖 */} {/* sticky 必须直接挂在 grid 子项上:aside 作为 grid item,其可粘行程 等于行高(正文高度);内层包裹会被 items-start 缩成内容高而失去行程 */}
- {/* 窄屏作者卡(xl 起由左栏承载,避免重复展示) */} + {/* 窄屏作者卡(lg 起由左栏承载,避免重复展示) */} {profile && ( -
+
)} @@ -515,7 +515,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps {/* 右栏:目录 / 同板块热议(阅读数据已并入标题下元信息条) */}