Files
jiang13-bbs/frontend/components/MarkdownBodyClient.tsx
freefire 99c0f0319d feat: 品牌裁剪弹窗/Markdown 行内短代码与详情视频块等
新增:
- BrandCropModal 品牌图裁剪弹窗 + cropImage 工具
- Markdown 行内短代码(inlineShortcodes + remark 插件)
- MdDetails 折叠块、MdVideo 视频块组件
- directiveBlocks/inlineShortcodes 测试

其它:
- 聊天后端(handler/service chat)调整
- Markdown 渲染链路、编辑器、全局样式与品牌相关面板更新
2026-10-02 01:40:06 +08:00

146 lines
4.6 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 ReactMarkdown, { type Components } from "react-markdown";
import remarkGfm from "remark-gfm";
import { mergeMarkdownComponents } from "@/components/markdown/mergeComponents";
import { remarkInlineShortcodes } from "@/components/markdown/remarkInlineShortcodes";
import { splitContentSegments } from "@/lib/directiveBlocks";
import { expandDyEmojiShortcodes } from "@/lib/dyEmoji";
import type { HideKind } from "@/lib/hideBlocks";
import MdHideBlock from "@/components/markdown/MdHideBlock";
import MdTimeline from "@/components/markdown/MdTimeline";
import MdDetails from "@/components/markdown/MdDetails";
import MdVideo from "@/components/markdown/MdVideo";
import { MdEntriesView } from "@/components/markdown/MdEntries";
type MarkdownBodyClientProps = {
content: string;
className?: string;
components?: Components;
postId?: number;
loginHref?: string;
loggedIn?: boolean;
/** 书级「全章节条目卡模式」:章节内容免写 [entries] 壳 */
entriesAuto?: boolean;
/** 按分段索引的条目标题锚点 id(与章内目录共享;阅读页传入,其他场景缺省) */
entriesTitleIds?: (string[] | undefined)[];
};
/**
* 客户端 Markdown(发帖预览 / 评论):GFM + 隐藏块 / 时间线 / 条目卡预览。
*/
export default function MarkdownBodyClient({
content,
className = "prose-content",
components,
postId,
loginHref,
loggedIn = false,
entriesAuto = false,
entriesTitleIds,
}: MarkdownBodyClientProps) {
const segs = splitContentSegments(expandDyEmojiShortcodes(content || ""), {
entriesAuto,
});
const mdComponents = mergeMarkdownComponents(components);
// 时间线/隐藏块正文不套外层 heading 覆盖,避免预览与正文目录 id 错位
const nestedMd = mergeMarkdownComponents();
return (
<div className={className}>
{segs.map((seg, i) => {
if (seg.type === "md") {
if (!seg.text.trim()) return null;
return (
<ReactMarkdown
key={`md-${i}`}
remarkPlugins={[remarkGfm, remarkInlineShortcodes]}
components={mdComponents}
>
{seg.text}
</ReactMarkdown>
);
}
if (seg.type === "timeline") {
return (
<MdTimeline
key={`tl-${i}`}
items={seg.items}
renderBody={(it) =>
it.body.trim() ? (
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkInlineShortcodes]}
components={nestedMd}
>
{expandDyEmojiShortcodes(it.body)}
</ReactMarkdown>
) : null
}
/>
);
}
if (seg.type === "entries") {
return (
<MdEntriesView
key={`entries-${i}`}
items={seg.items}
titleIds={entriesTitleIds?.[i]}
renderMd={(t) => (
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkInlineShortcodes]}
components={nestedMd}
>
{expandDyEmojiShortcodes(t)}
</ReactMarkdown>
)}
/>
);
}
if (seg.type === "details") {
return (
<MdDetails key={`details-${i}`} title={seg.title}>
{seg.body.trim() ? (
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkInlineShortcodes]}
components={nestedMd}
>
{expandDyEmojiShortcodes(seg.body)}
</ReactMarkdown>
) : null}
</MdDetails>
);
}
if (seg.type === "video") {
return <MdVideo key={`video-${i}`} provider={seg.provider} id={seg.id} />;
}
const kind = seg.kind as HideKind;
if (seg.locked) {
return (
<MdHideBlock
key={`hide-${i}`}
kind={kind}
points={seg.points}
locked
postId={postId}
loginHref={loginHref}
loggedIn={loggedIn}
/>
);
}
return (
<MdHideBlock key={`hide-${i}`} kind={kind} points={seg.points}>
{seg.body.trim() ? (
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkInlineShortcodes]}
components={nestedMd}
>
{expandDyEmojiShortcodes(seg.body)}
</ReactMarkdown>
) : null}
</MdHideBlock>
);
})}
</div>
);
}