feat: 完成头像上传、站点主题配置与UI优化
- 新增附件模型与上传服务,拆分头像上传/管理逻辑 - 重构用户资料接口,移除直接修改头像字段,改用专用上传接口 - 添加站点公开设置API与管理员外观配置页 - 全局替换hover背景为accent-soft,优化交互一致性 - 完善头像组件,支持图片加载失败回退与src更新重置 - 新增用户状态事件总线,解决同页头像更新不及时问题 - 调整依赖包顺序与CI依赖版本 - 重构板块列表接口,新增帖子计数 - 迁移账号设置到用户主页tab,合并重复代码
This commit is contained in:
@@ -17,4 +17,6 @@ type Handlers struct {
|
||||
OverviewSvc *service.OverviewService
|
||||
Checkin *service.CheckinService
|
||||
Announcement *service.AnnouncementService
|
||||
Upload *service.UploadService
|
||||
Setting *service.SettingService
|
||||
}
|
||||
|
||||
44
backend/handler/setting.go
Normal file
44
backend/handler/setting.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// PublicSettings 公开站点设置(无需登录):前端 SSR 据此注入主题 CSS 变量
|
||||
// GET /api/settings
|
||||
func (h *Handlers) PublicSettings(c *gin.Context) {
|
||||
accent, err := h.Setting.AccentColor()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取站点设置失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"accent": accent})
|
||||
}
|
||||
|
||||
type updateSettingsRequest struct {
|
||||
// Accent 主题色 #RRGGBB;空串恢复默认
|
||||
Accent string `json:"accent"`
|
||||
}
|
||||
|
||||
// UpdateSettings 更新站点设置(管理员)
|
||||
// PUT /api/admin/settings
|
||||
func (h *Handlers) UpdateSettings(c *gin.Context) {
|
||||
var req updateSettingsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"})
|
||||
return
|
||||
}
|
||||
if err := h.Setting.SetAccent(req.Accent); err != nil {
|
||||
if errors.Is(err, service.ErrInvalidAccent) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true, "accent": req.Accent})
|
||||
}
|
||||
100
backend/handler/upload.go
Normal file
100
backend/handler/upload.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/middleware"
|
||||
"github.com/freefire/jiang13-bbs/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// UploadAvatar 上传裁剪后的头像(multipart 字段 file,内容必须是 WebP)
|
||||
func (h *Handlers) UploadAvatar(c *gin.Context) {
|
||||
claims := middleware.CurrentUser(c)
|
||||
|
||||
// 限制请求体:以裁剪后文件为依据,允许少量 multipart 边界开销
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, service.AvatarMaxBytes+4096)
|
||||
|
||||
fh, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "图片过大(裁剪后不能超过 2MB)或格式不正确"})
|
||||
return
|
||||
}
|
||||
if fh.Size > service.AvatarMaxBytes {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "裁剪后的图片不能超过 2MB"})
|
||||
return
|
||||
}
|
||||
f, err := fh.Open()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "读取图片失败"})
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// fh.Size 已受 MaxBytesReader 约束;读取时二次防御
|
||||
data, err := io.ReadAll(io.LimitReader(f, service.AvatarMaxBytes+1))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "读取图片失败"})
|
||||
return
|
||||
}
|
||||
|
||||
att, err := h.Upload.SaveAvatar(claims.ID, data)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"url": att.URL, "attachment": att})
|
||||
}
|
||||
|
||||
// UseAvatarRequest 选用历史头像请求
|
||||
type UseAvatarRequest struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// UseAvatar 选用本人历史上传的头像
|
||||
func (h *Handlers) UseAvatar(c *gin.Context) {
|
||||
claims := middleware.CurrentUser(c)
|
||||
var req UseAvatarRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数格式错误"})
|
||||
return
|
||||
}
|
||||
if err := h.Upload.UseAvatar(claims.ID, req.URL); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"url": req.URL})
|
||||
}
|
||||
|
||||
// MyMedia 媒体库:本人上传的全部图片(头像、帖子插图等)
|
||||
func (h *Handlers) MyMedia(c *gin.Context) {
|
||||
claims := middleware.CurrentUser(c)
|
||||
list, err := h.Upload.ListMedia(claims.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"attachments": list})
|
||||
}
|
||||
|
||||
// DeleteAttachment 彻底删除本人附件(DB + 物理文件)
|
||||
func (h *Handlers) DeleteAttachment(c *gin.Context) {
|
||||
claims := middleware.CurrentUser(c)
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的附件 ID"})
|
||||
return
|
||||
}
|
||||
if err := h.Upload.DeleteAttachment(claims.ID, uint(id)); err != nil {
|
||||
if errors.Is(err, service.ErrAttachmentInUse) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
@@ -8,15 +8,14 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// UpdateProfileRequest 更新资料请求
|
||||
// UpdateProfileRequest 更新资料请求(头像不在此通道,见 /api/upload/avatar)
|
||||
type UpdateProfileRequest struct {
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar"`
|
||||
Email string `json:"email"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
// UpdateProfile 更新当前登录用户的资料(昵称、头像、邮箱、签名)
|
||||
// UpdateProfile 更新当前登录用户的资料(昵称、邮箱、签名)
|
||||
func (h *Handlers) UpdateProfile(c *gin.Context) {
|
||||
claims := middleware.CurrentUser(c)
|
||||
var req UpdateProfileRequest
|
||||
@@ -25,7 +24,7 @@ func (h *Handlers) UpdateProfile(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.Auth.UpdateProfile(claims.ID, req.Nickname, req.Avatar, req.Email, req.Signature)
|
||||
user, err := h.Auth.UpdateProfile(claims.ID, req.Nickname, req.Email, req.Signature)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user