feat: 书库导入导出/图片变体/书籍搜索/小组件运行时等

新增:
- 书库导入导出(library_import/library_export)及测试
- 图片变体生成(image_variants)与响应式图片(responsiveImage)
- 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件
- 小组件运行时(widgetRuntime)与静态检查(widgetLint)
- 上传缓存中间件(upload_cache)与字体 CSS 提取脚本

其它:
- 后端 handlers/services 全量调整
- 前端页面、组件、库函数与配置更新
This commit is contained in:
2026-10-01 03:06:05 +08:00
parent 7fbd6ba699
commit ff2ab286fb
141 changed files with 7527 additions and 1407 deletions

View File

@@ -250,8 +250,7 @@ func TestAdminMediaLibraryAttachSources(t *testing.T) {
if !ok {
t.Fatal("comment image missing")
}
if cimg.SourceURL != fmt.Sprintf("/post/%d#comment-", post.ID) &&
!strings.HasPrefix(cimg.SourceURL, fmt.Sprintf("/post/%d#comment-", post.ID)) {
if cimg.SourceURL != fmt.Sprintf("/post/%d#comment-1", post.ID) {
t.Fatalf("comment source url wrong: %+v", cimg)
}
if !strings.Contains(cimg.SourceLabel, "下的评论") {
@@ -259,6 +258,63 @@ func TestAdminMediaLibraryAttachSources(t *testing.T) {
}
}
func TestAdminMediaLibraryBookCategory(t *testing.T) {
s, dir := newMediaLibraryService(t)
// 书籍封面(本地磁盘)与正文插图(远程对象),另有一张普通帖子插图
writeMediaFile(t, dir, "images/cover.webp", []byte("fake-webp-bytes"))
if err := s.db.Create(&model.User{Username: "alice", Nickname: "爱丽丝"}).Error; err != nil {
t.Fatalf("create user: %v", err)
}
var alice model.User
if err := s.db.Where("username = ?", "alice").First(&alice).Error; err != nil {
t.Fatalf("load user: %v", err)
}
created := time.Date(2026, 9, 1, 10, 0, 0, 0, time.UTC)
mkAtt := func(source, url string) {
if err := s.db.Create(&model.Attachment{
UserID: alice.ID, Kind: model.AttachmentKindImage, Source: source,
URL: url, MIME: "image/webp", Size: 100, Width: 10, Height: 10,
CreatedAt: created,
}).Error; err != nil {
t.Fatalf("create attachment %s: %v", source, err)
}
}
mkAtt(model.AttachmentSourceLibraryCover, "/uploads/images/cover.webp")
mkAtt(model.AttachmentSourceLibraryContent, "/api/media/book-content")
mkAtt(model.AttachmentSourcePost, "/api/media/post-image")
items, counts, err := s.AdminMediaLibrary()
if err != nil {
t.Fatalf("AdminMediaLibrary: %v", err)
}
byURL := map[string]MediaLibraryItem{}
for _, it := range items {
byURL[it.URL] = it
}
// 封面(磁盘)与正文插图(远程)都归“书籍”
for _, u := range []string{"/uploads/images/cover.webp", "/api/media/book-content"} {
it, ok := byURL[u]
if !ok {
t.Fatalf("%s missing", u)
}
if it.Category != "book" || it.CategoryName != "书籍" {
t.Fatalf("%s should be book category, got %q/%q", u, it.Category, it.CategoryName)
}
}
// 普通帖子插图仍归 image
if post := byURL["/api/media/post-image"]; post.Category != "image" {
t.Fatalf("post image should stay image category, got %q", post.Category)
}
if counts["book"] != 2 || counts["image"] != 1 {
t.Fatalf("counts wrong: %v", counts)
}
}
func TestMediaThumb(t *testing.T) {
s, dir := newMediaLibraryService(t)