feat(frontend): Markdown 支持 GFM、代码高亮与图片灯箱
统一 SSR/客户端渲染路径,评论与预览共用增强组件。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
76
frontend/components/markdown/MdImage.tsx
Normal file
76
frontend/components/markdown/MdImage.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, type ImgHTMLAttributes } from "react";
|
||||
import ImageLightbox, { type LightboxItem } from "./ImageLightbox";
|
||||
|
||||
type MdImageProps = ImgHTMLAttributes<HTMLImageElement>;
|
||||
|
||||
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.getAttribute("alt") || 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.alt || 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);
|
||||
}, []);
|
||||
|
||||
if (!src || broken) {
|
||||
return (
|
||||
<span className="md-img-fallback" role="img" aria-label={alt || "图片加载失败"}>
|
||||
{alt || "图片加载失败"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
{...rest}
|
||||
src={src}
|
||||
alt={alt || ""}
|
||||
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))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user