feat: 友链/书库/移动端导航/排行榜等多模块功能

新增模块:
- 友链(friendlink)后端处理 + 前端管理页与友链板
- 书库(library):导入、章节、封面、阅读页、横竖版自适应 AdaptiveCoverSlot
- 顶栏导航(header_nav)配置与 MobileTabBar/MobileRailDrawers 移动端抽屉
- 排行榜 service 测试、站点页面测试、时间线发布(timeline_release)

其它改动:
- 后端 handlers/services 全量小幅调整
- 前端组件、库函数、URL/品牌/站点 URL 工具更新
- Lightbox 图片、MdEntries 条目卡、coverColor 派生色相等前端能力
This commit is contained in:
2026-09-30 16:30:07 +08:00
parent e487bc2e92
commit 4f3f3a265c
105 changed files with 12371 additions and 852 deletions

View File

@@ -0,0 +1,69 @@
package service
import (
"errors"
"strings"
"testing"
"github.com/freefire/jiang13-bbs/model"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
func newSitePageTestService(t *testing.T) *SitePageService {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
if err := db.AutoMigrate(&model.SitePage{}); err != nil {
t.Fatalf("migrate: %v", err)
}
return NewSitePageService(db)
}
func sitePageInput(slug, title string) *SitePageInput {
return &SitePageInput{Slug: slug, Title: title, Content: "正文内容"}
}
func TestSitePageSlugReuseAfterSoftDelete(t *testing.T) {
s := newSitePageTestService(t)
p, err := s.Create(sitePageInput("test", "测试"))
if err != nil {
t.Fatalf("create: %v", err)
}
if err := s.Delete(p.ID); err != nil {
t.Fatalf("delete: %v", err)
}
// 软删后重建同 slug:报"已删除"提示而非数据库唯一索引错误
_, err = s.Create(sitePageInput("test", "重建"))
if err == nil || !strings.Contains(err.Error(), "已删除") {
t.Fatalf("软删后重建应提示已删除单页占用,got %v", err)
}
// 管理端列表含软删行且带 deleted 标记
all, err := s.ListAll()
if err != nil {
t.Fatalf("ListAll: %v", err)
}
if len(all) != 1 || !all[0].Deleted || all[0].Slug != "test" {
t.Fatalf("ListAll 应含软删单页且 Deleted=true,got %+v", all)
}
// 彻底删除后可重建
if err := s.Purge(p.ID); err != nil {
t.Fatalf("purge: %v", err)
}
if _, err := s.Create(sitePageInput("test", "重建")); err != nil {
t.Fatalf("purge 后重建同 slug 不应报错: %v", err)
}
}
func TestSitePagePurgeRequiresDeleted(t *testing.T) {
s := newSitePageTestService(t)
p, _ := s.Create(sitePageInput("alive", "在用"))
if err := s.Purge(p.ID); err == nil || !errors.Is(err, ErrSitePageNotDeleted) {
t.Fatalf("未删单页 Purge 应拒绝,got %v", err)
}
}