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>
|
||
);
|
||
}
|