feat: 添加Markdown渲染组件,TODO:样式需要更改
This commit is contained in:
4668
frontend-react/package-lock.json
generated
Normal file
4668
frontend-react/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,18 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.2.0",
|
"highlight.js": "^11.11.1",
|
||||||
"react-dom": "^18.2.0"
|
"katex": "^0.16.38",
|
||||||
|
"marked": "^17.0.4",
|
||||||
|
"marked-highlight": "^2.2.3",
|
||||||
|
"mermaid": "^11.13.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-copy-to-clipboard": "^5.1.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"react-syntax-highlighter": "^16.1.1",
|
||||||
|
"rehype-katex": "^7.0.1",
|
||||||
|
"remark-math": "^6.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.43",
|
"@types/react": "^18.2.43",
|
||||||
|
|||||||
146
frontend-react/src/components/Markdown2Html/Markdown2Html.jsx
Normal file
146
frontend-react/src/components/Markdown2Html/Markdown2Html.jsx
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
// npm install react react-dom react-markdown react-syntax-highlighter remark-math rehype-katex react-copy-to-clipboard mermaid katex
|
||||||
|
import React, { useState, useCallback, useEffect } from "react";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
|
import { materialLight } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||||
|
import remarkMath from "remark-math";
|
||||||
|
import rehypeKatex from "rehype-katex";
|
||||||
|
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||||
|
import mermaid from "mermaid";
|
||||||
|
import "katex/dist/katex.min.css";
|
||||||
|
|
||||||
|
// Mermaid 初始化配置
|
||||||
|
// Mermaid是一个画图的语言
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
|
theme: "default",
|
||||||
|
securityLevel: "loose",
|
||||||
|
});
|
||||||
|
|
||||||
|
const DownSvg = () => (
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24">
|
||||||
|
<path d="M7 10l5 5 5-5z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const CopySvg = () => (
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24">
|
||||||
|
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const MermaidChart= ({ code }) => {
|
||||||
|
const [svg, setSvg] = useState("");
|
||||||
|
const [id] = useState(() => `mermaid-${Math.random().toString(36).substr(2, 9)}`);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
mermaid.parse(code);
|
||||||
|
mermaid.render(id, code).then(({ svg }) => setSvg(svg));
|
||||||
|
} catch (err) {
|
||||||
|
setSvg(`<pre>Error rendering mermaid: ${err}</pre>`);
|
||||||
|
}
|
||||||
|
}, [code, id]);
|
||||||
|
|
||||||
|
return <div dangerouslySetInnerHTML={{ __html: svg }} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MarkdownToHTML= ({ markdownRAW, className }) => {
|
||||||
|
const [isMermaidLoaded] = useState(true);
|
||||||
|
// 渲染代码块的方法
|
||||||
|
const CodeBlock = useCallback(
|
||||||
|
({ node, inline, className, children, ...props } ) => {
|
||||||
|
const [isShowCode, setIsShowCode] = useState(true); // 添加展开与收起代码块状态
|
||||||
|
const [isShowCopy, setIsShowCopy] = useState(false);// 添加点击复制的状态
|
||||||
|
const match = /language-(\w+)/.exec(className || ""); // 这是用来匹配代码块对应语言的方法
|
||||||
|
const codeContent = String(children).replace(/\n$/, "");
|
||||||
|
// 处理复制成功提示
|
||||||
|
const handleCopy = () => {
|
||||||
|
setIsShowCopy(true);
|
||||||
|
setTimeout(() => setIsShowCopy(false), 1500);
|
||||||
|
};
|
||||||
|
//处理单行代码
|
||||||
|
if (inline) {
|
||||||
|
return <code className={className} {...props}>{children}</code>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 Mermaid 图表代码
|
||||||
|
if (match?.[1] === "mermaid" && isMermaidLoaded) {
|
||||||
|
return (
|
||||||
|
<div style={{ position: "relative", margin: "20px 0" }}>
|
||||||
|
<div className="code-header">
|
||||||
|
<div
|
||||||
|
style={{ cursor: "pointer", marginRight: "10px", transformOrigin: "8px" }}
|
||||||
|
className={isShowCode ? "code-rotate-down" : "code-rotate-right"}
|
||||||
|
onClick={() => setIsShowCode(!isShowCode)}
|
||||||
|
>
|
||||||
|
<DownSvg />
|
||||||
|
</div>
|
||||||
|
<div>{match[1]}</div>
|
||||||
|
<CopyToClipboard text={codeContent} onCopy={handleCopy}>
|
||||||
|
<div className="preview-code-copy" style={{ cursor: "pointer" }}>
|
||||||
|
{isShowCopy && <span className="copy-success">✓ 复制成功</span>}
|
||||||
|
<CopySvg />
|
||||||
|
</div>
|
||||||
|
</CopyToClipboard>
|
||||||
|
</div>
|
||||||
|
{isShowCode && <MermaidChart code={codeContent} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: "relative", padding:"0"}}>
|
||||||
|
<div className="code-header">
|
||||||
|
<div
|
||||||
|
style={{ cursor: "pointer", marginRight: "10px", transformOrigin: "8px" }}
|
||||||
|
className={isShowCode ? "code-rotate-down" : "code-rotate-right"}
|
||||||
|
onClick={() => setIsShowCode(!isShowCode)}
|
||||||
|
>
|
||||||
|
<DownSvg style={{
|
||||||
|
transform: isShowCode ? "rotate(0deg)" : "rotate(-90deg)",
|
||||||
|
transition: "transform 0.2s"
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
<div>{match?.[1] || "code"}</div>
|
||||||
|
<CopyToClipboard text={codeContent} onCopy={handleCopy}>
|
||||||
|
<div className="preview-code-copy" style={{ cursor: "pointer" }}>
|
||||||
|
{isShowCopy && <span className="copy-success">✓ 复制成功</span>}
|
||||||
|
<CopySvg />
|
||||||
|
</div>
|
||||||
|
</CopyToClipboard>
|
||||||
|
</div>
|
||||||
|
{isShowCode && (
|
||||||
|
<SyntaxHighlighter
|
||||||
|
style={materialLight}
|
||||||
|
language={match?.[1] || "text"}
|
||||||
|
PreTag="div"
|
||||||
|
showLineNumbers
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{codeContent}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[isMermaidLoaded]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className} >
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkMath]}
|
||||||
|
rehypePlugins={[rehypeKatex]}
|
||||||
|
components={{ code: CodeBlock }}
|
||||||
|
>
|
||||||
|
{markdownRAW}
|
||||||
|
</ReactMarkdown>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarkdownToHTML;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// 下面这些包如果显示不存在则运行这个代码安装依赖:
|
||||||
|
// npm install marked marked-highlight highlight.js
|
||||||
|
import { marked } from "marked";
|
||||||
|
import { markedHighlight } from "marked-highlight";
|
||||||
|
import hljs from "highlight.js";
|
||||||
|
import "highlight.js/styles/base16/github.css";
|
||||||
|
|
||||||
|
// 配置 marked 和 markedHighlight
|
||||||
|
marked.use(
|
||||||
|
markedHighlight({
|
||||||
|
langPrefix: "hljs language-",
|
||||||
|
highlight(code, lang) {
|
||||||
|
const language = hljs.getLanguage(lang) ? lang : "plaintext"; // 如果语言不支持,回退为纯文本
|
||||||
|
return hljs.highlight(code, { language }).value; // 使用 highlight.js 高亮代码
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Markdown 解析函数
|
||||||
|
const MarkdownRenderer = (markdownString)=> {
|
||||||
|
const res= marked.parse(markdownString).toString()
|
||||||
|
return res; // 将 Markdown 转换为 HTML
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarkdownRenderer;
|
||||||
Reference in New Issue
Block a user