diff --git a/client/next-env.d.ts b/client/next-env.d.ts new file mode 100644 index 000000000..4f11a03dc --- /dev/null +++ b/client/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/client/package.json b/client/package.json index a681c4123..e5f3a99bd 100644 --- a/client/package.json +++ b/client/package.json @@ -55,7 +55,8 @@ "sass": "^1.58.3", "tunnel": "^0.0.6", "wxpay-v3": "^3.0.2", - "zustand": "^4.3.5" + "zustand": "^4.3.5", + "mermaid": "^8.13.5" }, "devDependencies": { "@svgr/webpack": "^6.5.1", diff --git a/client/src/components/Markdown/MermaidCodeBlock.tsx b/client/src/components/Markdown/MermaidCodeBlock.tsx new file mode 100644 index 000000000..e312e2731 --- /dev/null +++ b/client/src/components/Markdown/MermaidCodeBlock.tsx @@ -0,0 +1,63 @@ +import React, { FC, useEffect, useState, useRef } from 'react'; +import mermaid from 'mermaid'; +import { Spinner } from '@chakra-ui/react'; + +interface MermaidCodeBlockProps { + code: string; +} + +const MermaidCodeBlock: FC = ({ code }) => { + const [svg, setSvg] = useState(null); + const [loading, setLoading] = useState(true); + const codeTimeoutIdRef = useRef(null); + + useEffect(() => { + if (codeTimeoutIdRef.current) { + clearTimeout(codeTimeoutIdRef.current); + } + + codeTimeoutIdRef.current = window.setTimeout(() => { + setLoading(true); + + const mermaidAPI = (mermaid as any).mermaidAPI as any; + mermaidAPI.initialize({ startOnLoad: false, theme: 'forest' }); + + try { + mermaidAPI.parse(code); + mermaidAPI.render('mermaid-svg', code, (svgCode: string) => { + setSvg(svgCode); + setLoading(false); + }); + } catch (error) { + console.error('Error parsing Mermaid code:', '\n', error, '\n', 'Code:', code); + setLoading(false); + return; + } + }, 1000); + }, [code]); + + useEffect(() => { + return () => { + if (codeTimeoutIdRef.current) { + clearTimeout(codeTimeoutIdRef.current); + } + }; + }, []); + + return ( + <> + {loading ? ( +
+ Loading... +
+ ) : ( +
+ )} + + ); +}; + +export default MermaidCodeBlock; diff --git a/client/src/components/Markdown/index.tsx b/client/src/components/Markdown/index.tsx index fc25f7af3..1a2b43c5f 100644 --- a/client/src/components/Markdown/index.tsx +++ b/client/src/components/Markdown/index.tsx @@ -1,4 +1,4 @@ -import React, { memo, useMemo } from 'react'; +import React, { memo, useMemo, useEffect } from 'react'; import ReactMarkdown from 'react-markdown'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { Box, Flex, useColorModeValue } from '@chakra-ui/react'; @@ -7,6 +7,7 @@ import Icon from '@/components/Icon'; import remarkGfm from 'remark-gfm'; import remarkMath from 'remark-math'; import rehypeKatex from 'rehype-katex'; +import MermaidCodeBlock from './MermaidCodeBlock'; import 'katex/dist/katex.min.css'; import styles from './index.module.scss'; @@ -29,49 +30,54 @@ const Markdown = ({ return ( + if (match && match[1] === "mermaid") { + return ; + } + + return !inline && match ? ( + {match?.[1]} - copyData(code)} alignItems={'center'}> - + copyData(code)} alignItems={"center"}> + 复制代码 - + {code} ) : ( - {code} + {children} ); - } + }, }} linkTarget="_blank" > diff --git a/client/src/types/mermaid.d.ts b/client/src/types/mermaid.d.ts new file mode 100644 index 000000000..a303d0259 --- /dev/null +++ b/client/src/types/mermaid.d.ts @@ -0,0 +1,19 @@ +declare module "mermaid" { + import mermaidAPI from "mermaid"; + const mermaid: any; + export default mermaid; + + // 扩展 mermaidAPI + interface MermaidAPI extends mermaidAPI.mermaidAPI { + contentLoaded: ( + targetEl: Element, + options?: mermaidAPI.mermaidAPI.Config + ) => void; + } + + const mermaidAPIInstance: MermaidAPI; + export default mermaidAPIInstance; + } +type Dispatch = (action: Action) => void; + + \ No newline at end of file