83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
|
|
'use client';
|
|||
|
|
|
|||
|
|
import ReactMarkdown, { type Components } from 'react-markdown';
|
|||
|
|
import { isHTMLContent } from '@/lib/content';
|
|||
|
|
|
|||
|
|
interface ContentViewProps {
|
|||
|
|
content: string | null | undefined;
|
|||
|
|
className?: string;
|
|||
|
|
/**
|
|||
|
|
* 显式指定内容格式,优先于内容探测。
|
|||
|
|
* - 'html':富文本,dangerouslySetInnerHTML 渲染
|
|||
|
|
* - 'markdown':用 react-markdown 解析
|
|||
|
|
* 不传时回退到 isHTMLContent 自动判断(旧调用方兼容)。
|
|||
|
|
*/
|
|||
|
|
format?: 'html' | 'markdown';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const mdComponents: Components = {
|
|||
|
|
pre: ({ children }) => (
|
|||
|
|
<pre className="my-4 overflow-x-auto rounded-md bg-gray-900 p-4 text-sm text-gray-100">
|
|||
|
|
{children}
|
|||
|
|
</pre>
|
|||
|
|
),
|
|||
|
|
code: ({ className: cls, children }) => {
|
|||
|
|
// 行内 code(无 language- 前缀)→ 灰底圆角;块级 code 由 <pre> 包裹
|
|||
|
|
const isBlock = typeof cls === 'string' && cls.includes('language-');
|
|||
|
|
if (isBlock) {
|
|||
|
|
return <code className={cls}>{children}</code>;
|
|||
|
|
}
|
|||
|
|
return (
|
|||
|
|
<code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm text-pink-600">
|
|||
|
|
{children}
|
|||
|
|
</code>
|
|||
|
|
);
|
|||
|
|
},
|
|||
|
|
table: ({ children }) => (
|
|||
|
|
<table className="my-4 w-full border-collapse border border-gray-300 text-sm">
|
|||
|
|
{children}
|
|||
|
|
</table>
|
|||
|
|
),
|
|||
|
|
th: ({ children }) => (
|
|||
|
|
<th className="border border-gray-300 bg-gray-50 px-3 py-2 text-left font-medium">
|
|||
|
|
{children}
|
|||
|
|
</th>
|
|||
|
|
),
|
|||
|
|
td: ({ children }) => (
|
|||
|
|
<td className="border border-gray-300 px-3 py-2">{children}</td>
|
|||
|
|
),
|
|||
|
|
hr: () => <hr className="my-6 border-gray-200" />,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 智能内容渲染器:
|
|||
|
|
* - HTML 内容(来自 RichEditor) → 用 dangerouslySetInnerHTML 直接渲染
|
|||
|
|
* - Markdown 内容(来自 .md 上传)→ 用 react-markdown 解析为 React 元素后渲染
|
|||
|
|
* 两者都套 `.prose-rich` 类以复用全局排版样式。
|
|||
|
|
*
|
|||
|
|
* 注:MD 内容用 suppressHydrationWarning 包裹,避免 react-markdown
|
|||
|
|
* SSR/客户端解析在边界字符(HTML 标签、空白)上产生 hydration 不一致。
|
|||
|
|
*/
|
|||
|
|
export function ContentView({ content, className, format }: ContentViewProps) {
|
|||
|
|
if (!content?.trim()) return null;
|
|||
|
|
|
|||
|
|
const wrapperClass = className ? `prose-rich ${className}` : 'prose-rich';
|
|||
|
|
const isHtml =
|
|||
|
|
format === 'html' ? true : format === 'markdown' ? false : isHTMLContent(content);
|
|||
|
|
|
|||
|
|
if (isHtml) {
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
className={wrapperClass}
|
|||
|
|
dangerouslySetInnerHTML={{ __html: content }}
|
|||
|
|
/>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className={wrapperClass} suppressHydrationWarning>
|
|||
|
|
<ReactMarkdown components={mdComponents}>{content}</ReactMarkdown>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|