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

@@ -21,7 +21,9 @@ import { splitContentSegments } from "@/lib/directiveBlocks";
import { wrapImplicitEntries } from "@/lib/entryBlocks";
import PostToc, { type TocItem } from "@/components/PostToc";
import MobileRailDrawers from "@/components/MobileRailDrawers";
import BookSearch from "@/components/BookSearch";
import { formatRelative } from "@/lib/format";
import { markdownToSearchText, type BookChapterIndex } from "@/lib/bookSearch";
interface PageProps {
params: Promise<{ slug: string }>;
@@ -127,6 +129,18 @@ export default async function LibraryReadPage({ params, searchParams }: PageProp
[current.content, ...subs.map((s) => s.content)].filter(Boolean).join("\n\n")
);
// 全书搜索索引:详情接口已一次返回全部章节,这里把每章(含小节标题/正文)
// 剥成可见纯文本随 RSC 下发给 BookSearch,无需额外交互请求
const searchIndex: BookChapterIndex[] = groups.map((g, i) => ({
no: i + 1,
title: g.chapter.title,
plain: markdownToSearchText(
[g.chapter.title, g.chapter.content, ...g.subs.flatMap((s) => [s.title, s.content])]
.filter(Boolean)
.join("\n\n")
),
}));
// 书籍侧栏内容:桌面左栏与移动端左抽屉共用(详情入口 + 章节列表)
const bookRail = (
<div className="flex flex-col gap-3">
@@ -247,11 +261,19 @@ export default async function LibraryReadPage({ params, searchParams }: PageProp
{/* 中间:单章正文 + 上下章导航(占满中栏全宽;手机端 panel 贴边铺满) */}
<div className="j13-doc-bleed min-w-0">
<article className="panel p-6 sm:p-8">
<article id="j13-book-article" className="panel p-6 sm:p-8">
<header>
<p className="meta text-[12px]">
第 {nav.index + 1} / {nav.total} 章 · 约 {chapterMinutes} 分钟
</p>
<div className="flex items-center justify-between gap-3">
<p className="meta text-[12px]">
第 {nav.index + 1} / {nav.total} 章 · 约 {chapterMinutes} 分钟
</p>
<BookSearch
slug={d.slug}
urlStyle={urlStyle}
currentNo={nav.index + 1}
chapters={searchIndex}
/>
</div>
<h2
id="ch-top"
className="mt-1.5 scroll-mt-24 text-[24px] sm:text-[26px] font-extrabold leading-snug"