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

其它:
- 后端 handlers/services 全量调整
- 前端页面、组件、库函数与配置更新
2026-10-01 03:06:05 +08:00

117 lines
4.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
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: 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 正文图片:点击进入图箱,同文多图可切换 */
export default function MdImage({ src, alt, title, className, ...rest }: MdImageProps) {
const [broken, setBroken] = useState(false);
const [lightbox, setLightbox] = useState<{
items: LightboxItem[];
index: number;
} | null>(null);
const open = useCallback((el: HTMLImageElement) => {
const gallery = collectGallery(el);
if (gallery.items.length === 0) return;
setLightbox(gallery);
}, []);
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/");
const emojiSc =
typeof alt === "string" && alt.startsWith("dy:") ? `[:${alt.slice(3)}:]` : undefined;
if (isEmoji && !broken) {
return (
/* eslint-disable-next-line @next/next/no-img-element */
<img
{...rest}
src={src}
alt={imgAlt}
title={emojiSc}
className={`j13-md-emoji${className ? ` ${className}` : ""}`}
loading="lazy"
decoding="async"
draggable={false}
onContextMenu={(e) => e.preventDefault()}
onError={() => setBroken(true)}
/>
);
}
if (!src || broken) {
return (
<span className="md-img-fallback" role="img" aria-label={provided || "图片加载失败"}>
{provided || "图片加载失败"}
</span>
);
}
return (
<>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
{...rest}
// 同源图(/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"
decoding="async"
onClick={(e) => open(e.currentTarget)}
onError={() => setBroken(true)}
/>
{lightbox && (
<ImageLightbox
items={lightbox.items}
index={lightbox.index}
onClose={() => setLightbox(null)}
onIndexChange={(i) =>
setLightbox((prev) => (prev ? { ...prev, index: i } : prev))
}
/>
)}
</>
);
}