feat: 书库导入导出/图片变体/书籍搜索/小组件运行时等
新增: - 书库导入导出(library_import/library_export)及测试 - 图片变体生成(image_variants)与响应式图片(responsiveImage) - 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件 - 小组件运行时(widgetRuntime)与静态检查(widgetLint) - 上传缓存中间件(upload_cache)与字体 CSS 提取脚本 其它: - 后端 handlers/services 全量调整 - 前端页面、组件、库函数与配置更新
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/middleware"
|
||||
"github.com/freefire/jiang13-bbs/model"
|
||||
"github.com/freefire/jiang13-bbs/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -102,7 +103,7 @@ func (h *Handlers) UploadImage(c *gin.Context) {
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
att, err := h.Upload.SaveImage(claims.ID, f)
|
||||
att, err := h.Upload.SaveImage(claims.ID, f, c.PostForm("source"))
|
||||
if err != nil {
|
||||
var maxErr *http.MaxBytesError
|
||||
if errors.As(err, &maxErr) {
|
||||
@@ -314,7 +315,9 @@ func (h *Handlers) UploadBrandFromMedia(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"url": url})
|
||||
}
|
||||
|
||||
// UploadLibraryCoverFromMedia 把本人媒体库图片复制一份作书籍封面素材,只返回 URL,不绑定条目
|
||||
// UploadLibraryCoverFromMedia 把本人媒体库图片用作书籍封面素材。
|
||||
// 若该图已是封面类型(source=library_cover),直接复用原记录,不再复制;
|
||||
// 否则复制一份并标记为封面类型(与原图解耦,删除原图不影响封面)。
|
||||
func (h *Handlers) UploadLibraryCoverFromMedia(c *gin.Context) {
|
||||
var req struct {
|
||||
AttachmentID uint `json:"attachment_id"`
|
||||
@@ -328,7 +331,14 @@ func (h *Handlers) UploadLibraryCoverFromMedia(c *gin.Context) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
|
||||
return
|
||||
}
|
||||
att, err := h.Upload.CopyImageFromMedia(claims.ID, req.AttachmentID)
|
||||
|
||||
// 已是封面类型的图直接复用,避免重复复制产生冗余记录
|
||||
if existing, err := h.Upload.FindMediaBySource(claims.ID, req.AttachmentID, model.AttachmentSourceLibraryCover); err == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"url": existing.URL, "attachment": existing})
|
||||
return
|
||||
}
|
||||
|
||||
att, err := h.Upload.CopyImageFromMedia(claims.ID, req.AttachmentID, model.AttachmentSourceLibraryCover)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -404,6 +414,23 @@ func (h *Handlers) MediaLibraryThumb(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "image/webp", data)
|
||||
}
|
||||
|
||||
// ImageVariant 公开图片变体(GET /api/img?u=<源图URL>&w=<白名单宽度>):
|
||||
// 供全站 srcset 消费,源内容不可变 → immutable 长缓存;失败 404 由前端 onError 兜底
|
||||
func (h *Handlers) ImageVariant(c *gin.Context) {
|
||||
w, err := strconv.Atoi(c.Query("w"))
|
||||
if err != nil || !service.IsVariantWidth(w) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的尺寸"})
|
||||
return
|
||||
}
|
||||
data, m, err := h.Upload.Variant(c.Query("u"), w)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "图片不可用"})
|
||||
return
|
||||
}
|
||||
c.Header("Cache-Control", "public, max-age=31536000, immutable")
|
||||
c.Data(http.StatusOK, m, data)
|
||||
}
|
||||
|
||||
func brandTooLarge(slot string) string {
|
||||
if slot == service.BrandSlotFavicon {
|
||||
return "Favicon 不能超过 512KB"
|
||||
|
||||
Reference in New Issue
Block a user