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

@@ -2,27 +2,33 @@
import { useState, useCallback, type ImgHTMLAttributes } from "react";
import ImageLightbox, { type LightboxItem } from "./ImageLightbox";
import { isOwnImage, variantURL, widthSrcSet } from "@/lib/responsiveImage";
type MdImageProps = ImgHTMLAttributes<HTMLImageElement>;
/** 图箱收集的原图地址:data-full 优先(页面显示变体时仍指向原图) */
function fullSrc(el: HTMLImageElement): string {
return el.dataset.full || el.currentSrc || el.getAttribute("src") || "";
}
function collectGallery(from: HTMLImageElement): { items: LightboxItem[]; index: number } {
const root = from.closest(".prose-content");
const nodes = root
? Array.from(root.querySelectorAll<HTMLImageElement>("img.md-content-img[src]"))
: [from];
const items: LightboxItem[] = nodes
.map((el) => ({
src: el.currentSrc || el.getAttribute("src") || "",
alt: el.dataset.caption || undefined,
}))
.filter((it) => !!it.src);
const selfSrc = from.currentSrc || from.getAttribute("src") || "";
let index = items.findIndex((it) => it.src === selfSrc);
if (index < 0) index = 0;
if (items.length === 0 && selfSrc) {
return { items: [{ src: selfSrc, alt: from.dataset.caption || undefined }], index: 0 };
}
return { items, index };
const root = from.closest(".prose-content");
const nodes = root
? Array.from(root.querySelectorAll<HTMLImageElement>("img.md-content-img[src]"))
: [from];
const items: LightboxItem[] = nodes
.map((el) => ({
src: fullSrc(el),
alt: el.dataset.caption || undefined,
}))
.filter((it) => !!it.src);
const selfSrc = fullSrc(from);
let index = items.findIndex((it) => it.src === selfSrc);
if (index < 0) index = 0;
if (items.length === 0 && selfSrc) {
return { items: [{ src: selfSrc, alt: from.dataset.caption || undefined }], index: 0 };
}
return { items, index };
}
/** Markdown 正文图片:点击进入图箱,同文多图可切换 */
@@ -42,6 +48,9 @@ export default function MdImage({ src, alt, title, className, ...rest }: MdImage
const provided = (typeof alt === "string" ? alt.trim() : "") || (typeof title === "string" ? title.trim() : "");
const imgAlt = provided || "图片";
// 同源图(本地 /uploads 或 /api/media 对象)才可走变体端点;外链图原样加载
const isLocalSrc = typeof src === "string" && isOwnImage(src);
// 抖音表情(/emoji/ 路径):行内小图,不进图箱、不带 caption;
// alt 由 expandDyEmojiShortcodes 产出("dy:<key>"),悬停 title 显示其短代码
const isEmoji = typeof src === "string" && src.includes("/emoji/");
@@ -78,9 +87,13 @@ export default function MdImage({ src, alt, title, className, ...rest }: MdImage
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
{...rest}
src={src}
// 同源图(/uploads、/api/media)页面内走宽度变体;外链图保持原 src 不回归
src={isLocalSrc ? variantURL(src, 512) : src}
srcSet={isLocalSrc ? widthSrcSet(src, [512, 768, 1080, 1536]) : undefined}
sizes={isLocalSrc ? "(max-width:768px) 92vw, 720px" : undefined}
alt={imgAlt}
data-caption={provided || undefined}
data-full={src}
title={title}
className={`md-content-img${className ? ` ${className}` : ""}`}
loading="lazy"