MarkdownViewer.tsx 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. import ReactMarkdown from "react-markdown";
  2. import remarkGfm from "remark-gfm";
  3. import rehypeRaw from "rehype-raw";
  4. import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
  5. import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
  6. import { useState } from "react";
  7. interface MarkdownViewerProps {
  8. content: string;
  9. }
  10. const CodeHeader: React.FC<{
  11. language: string;
  12. onCopy: () => void;
  13. copied: boolean;
  14. }> = ({ language, onCopy, copied }) => (
  15. <div
  16. className="flex justify-between items-center text-white"
  17. style={{ background: "#afadad", padding: "3px 4px" }}
  18. >
  19. <div className="flex items-center">
  20. <span className="text-xs text-#fff">{language}</span>
  21. </div>
  22. <div>
  23. <span className="cursor-pointer text-xs" onClick={onCopy}>
  24. {copied ? "已复制!" : "复制代码"}
  25. </span>
  26. </div>
  27. </div>
  28. );
  29. const MarkdownViewer: React.FC<MarkdownViewerProps> = ({ content }) => {
  30. const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
  31. const handleCopy = (code: string, index: number) => {
  32. navigator.clipboard.writeText(code);
  33. setCopiedIndex(index);
  34. setTimeout(() => setCopiedIndex(null), 2000);
  35. };
  36. return (
  37. <ReactMarkdown
  38. remarkPlugins={[remarkGfm]}
  39. rehypePlugins={[rehypeRaw]}
  40. components={{
  41. code({ node, className, children, ...props }) {
  42. const match = /language-(\w+)/.exec(className || "");
  43. const code = String(children).replace(/\n$/, "");
  44. const language = match ? match[1] : "";
  45. if (match) {
  46. return (
  47. <div className="rounded-md overflow-hidden mb-4">
  48. <CodeHeader
  49. language={language}
  50. onCopy={() =>
  51. handleCopy(code, node?.position?.start.line ?? 0)
  52. }
  53. copied={copiedIndex === node?.position?.start.line}
  54. />
  55. <div className="max-w-full overflow-x-auto">
  56. <SyntaxHighlighter
  57. style={vscDarkPlus}
  58. language={language}
  59. PreTag="div"
  60. {...props}
  61. customStyle={{
  62. margin: 0,
  63. borderTopLeftRadius: 0,
  64. borderTopRightRadius: 0,
  65. }}
  66. >
  67. {code}
  68. </SyntaxHighlighter>
  69. </div>
  70. </div>
  71. );
  72. }
  73. return (
  74. <code className={className} {...props}>
  75. {children}
  76. </code>
  77. );
  78. },
  79. }}
  80. >
  81. {content}
  82. </ReactMarkdown>
  83. );
  84. };
  85. export default MarkdownViewer;