markdown
This commit is contained in:
39
client/src/components/Markdown/img/Image.tsx
Normal file
39
client/src/components/Markdown/img/Image.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Image, Skeleton } from '@chakra-ui/react';
|
||||
|
||||
const MdImage = ({ src }: { src?: string }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [succeed, setSucceed] = useState(false);
|
||||
return (
|
||||
<Skeleton
|
||||
minH="100px"
|
||||
isLoaded={!isLoading}
|
||||
fadeDuration={2}
|
||||
display={'flex'}
|
||||
justifyContent={'center'}
|
||||
my={1}
|
||||
>
|
||||
<Image
|
||||
display={'inline-block'}
|
||||
borderRadius={'md'}
|
||||
src={src}
|
||||
alt={''}
|
||||
fallbackSrc={'/imgs/errImg.png'}
|
||||
fallbackStrategy={'onError'}
|
||||
cursor={succeed ? 'pointer' : 'default'}
|
||||
loading="eager"
|
||||
onLoad={() => {
|
||||
setIsLoading(false);
|
||||
setSucceed(true);
|
||||
}}
|
||||
onError={() => setIsLoading(false)}
|
||||
onClick={() => {
|
||||
if (!succeed) return;
|
||||
window.open(src, '_blank');
|
||||
}}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(MdImage);
|
||||
25
client/src/components/Markdown/img/Loading.tsx
Normal file
25
client/src/components/Markdown/img/Loading.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React, { memo } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
const Loading = ({ text }: { text?: string }) => {
|
||||
return (
|
||||
<Box>
|
||||
<Box
|
||||
minW={'100px'}
|
||||
w={'100%'}
|
||||
h={'80px'}
|
||||
backgroundImage={'url("/imgs/loading.gif")'}
|
||||
backgroundSize={'contain'}
|
||||
backgroundRepeat={'no-repeat'}
|
||||
backgroundPosition={'center'}
|
||||
/>
|
||||
{text && (
|
||||
<Box mt={1} textAlign={'center'} fontSize={'sm'} color={'myGray.600'}>
|
||||
{text}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Loading);
|
||||
137
client/src/components/Markdown/img/MermaidCodeBlock.tsx
Normal file
137
client/src/components/Markdown/img/MermaidCodeBlock.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import React, { useEffect, useRef, memo, useCallback, useState, useMemo } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
// @ts-ignore
|
||||
import mermaid from 'mermaid';
|
||||
import MyIcon from '../../Icon';
|
||||
|
||||
const mermaidAPI = mermaid.mermaidAPI;
|
||||
mermaidAPI.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'base',
|
||||
flowchart: {
|
||||
useMaxWidth: false
|
||||
},
|
||||
themeVariables: {
|
||||
fontSize: '14px',
|
||||
primaryColor: '#d6e8ff',
|
||||
primaryTextColor: '#485058',
|
||||
primaryBorderColor: '#fff',
|
||||
lineColor: '#5A646E',
|
||||
secondaryColor: '#B5E9E5',
|
||||
tertiaryColor: '#485058'
|
||||
}
|
||||
});
|
||||
|
||||
const punctuationMap: Record<string, string> = {
|
||||
',': ',',
|
||||
';': ';',
|
||||
'。': '.',
|
||||
':': ':',
|
||||
'!': '!',
|
||||
'?': '?',
|
||||
'“': '"',
|
||||
'”': '"',
|
||||
'‘': "'",
|
||||
'’': "'",
|
||||
'【': '[',
|
||||
'】': ']',
|
||||
'(': '(',
|
||||
')': ')',
|
||||
'《': '<',
|
||||
'》': '>',
|
||||
'、': ','
|
||||
};
|
||||
|
||||
const MermaidBlock = ({ code }: { code: string }) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [svg, setSvg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!code) return;
|
||||
try {
|
||||
const formatCode = code.replace(
|
||||
new RegExp(`[${Object.keys(punctuationMap).join('')}]`, 'g'),
|
||||
(match) => punctuationMap[match]
|
||||
);
|
||||
const { svg } = await mermaid.render(`mermaid-${Date.now()}`, formatCode);
|
||||
setSvg(svg);
|
||||
} catch (e: any) {
|
||||
console.log('[Mermaid] ', e?.message);
|
||||
}
|
||||
})();
|
||||
}, [code]);
|
||||
|
||||
const onclickExport = useCallback(() => {
|
||||
const svg = ref.current?.children[0];
|
||||
if (!svg) return;
|
||||
|
||||
const rate = svg.clientHeight / svg.clientWidth;
|
||||
const w = 3000;
|
||||
const h = rate * w;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
// 绘制白色背景
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
const img = new Image();
|
||||
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(ref.current.innerHTML)}`;
|
||||
|
||||
img.onload = () => {
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
|
||||
const jpgDataUrl = canvas.toDataURL('image/jpeg', 1);
|
||||
const a = document.createElement('a');
|
||||
a.href = jpgDataUrl;
|
||||
a.download = 'mermaid.jpg';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
};
|
||||
img.onerror = (e) => {
|
||||
console.log(e);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
position={'relative'}
|
||||
_hover={{
|
||||
'& > .export': {
|
||||
display: 'block'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
overflowX={'auto'}
|
||||
ref={ref}
|
||||
minW={'100px'}
|
||||
minH={'50px'}
|
||||
py={4}
|
||||
dangerouslySetInnerHTML={{ __html: svg }}
|
||||
/>
|
||||
<MyIcon
|
||||
className="export"
|
||||
display={'none'}
|
||||
name={'export'}
|
||||
w={'20px'}
|
||||
position={'absolute'}
|
||||
color={'myGray.600'}
|
||||
_hover={{
|
||||
color: 'myBlue.700'
|
||||
}}
|
||||
right={0}
|
||||
top={0}
|
||||
cursor={'pointer'}
|
||||
onClick={onclickExport}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MermaidBlock;
|
||||
Reference in New Issue
Block a user