mirror of
https://github.com/csunny/DB-GPT.git
synced 2025-09-13 21:21:08 +00:00
feat(web): Support html/svg preview (#2518)
This commit is contained in:
@@ -16,6 +16,8 @@ import VisResponse from './VisResponse';
|
||||
import AgentMessages from './agent-messages';
|
||||
import AgentPlans from './agent-plans';
|
||||
import { CodePreview } from './code-preview';
|
||||
import HtmlPreview from './html-preview';
|
||||
import SvgPreview from './svg-preview';
|
||||
import VisChart from './vis-chart';
|
||||
import VisCode from './vis-code';
|
||||
import VisConvertError from './vis-convert-error';
|
||||
@@ -189,6 +191,33 @@ const codeComponents = {
|
||||
const _lang = className?.replace('language-', '') || 'javascript';
|
||||
return <VisThinking content={content} />;
|
||||
},
|
||||
// Add HTML language processor
|
||||
html: ({ className, children }) => {
|
||||
const content = String(children);
|
||||
const _lang = className;
|
||||
return <HtmlPreview code={content} language='html' />;
|
||||
},
|
||||
// Support for Web languages that mix HTML, CSS, and JS
|
||||
web: ({ className, children }) => {
|
||||
const content = String(children);
|
||||
const _lang = className;
|
||||
return <HtmlPreview code={content} language='html' />;
|
||||
},
|
||||
svg: ({ className, children }) => {
|
||||
const content = String(children);
|
||||
const _lang = className;
|
||||
return <SvgPreview code={content} language='svg' />;
|
||||
},
|
||||
xml: ({ className, children }) => {
|
||||
const content = String(children);
|
||||
const _lang = className;
|
||||
// Check if the content is SVG
|
||||
if (content.includes('<svg') && content.includes('</svg>')) {
|
||||
return <SvgPreview code={content} language='svg' />;
|
||||
}
|
||||
// If it is not SVG, use normal XML highlighting
|
||||
return <CodePreview code={content} language='xml' />;
|
||||
},
|
||||
},
|
||||
defaultRenderer({ node, className, children, style, ...props }) {
|
||||
const content = String(children);
|
||||
|
330
web/components/chat/chat-content/html-preview.tsx
Normal file
330
web/components/chat/chat-content/html-preview.tsx
Normal file
@@ -0,0 +1,330 @@
|
||||
import {
|
||||
CopyOutlined,
|
||||
DownloadOutlined,
|
||||
FullscreenExitOutlined,
|
||||
FullscreenOutlined,
|
||||
PlayCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Modal, Tabs } from 'antd';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CodePreview } from './code-preview';
|
||||
/**
|
||||
* The HTML preview component is used to display HTML code and provide run, download, and full-screen functionality
|
||||
* @param {Object} props The component props
|
||||
* @param {string} props.code HTML code content
|
||||
* @param {string} props.language Code language, default is html
|
||||
*/
|
||||
const HtmlPreview = ({ code, language = 'html' }) => {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const iframeRef = useRef(null);
|
||||
const { t } = useTranslation();
|
||||
const [parsedCode, setParsedCode] = useState({
|
||||
html: '',
|
||||
css: '',
|
||||
js: '',
|
||||
fullCode: '',
|
||||
});
|
||||
|
||||
// Parse the code and extract the HTML, CSS, and JS parts
|
||||
useEffect(() => {
|
||||
const parseCode = sourceCode => {
|
||||
let html = sourceCode;
|
||||
let css = '';
|
||||
let js = '';
|
||||
|
||||
// Parse the content inside the <style> tags
|
||||
const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
||||
const styleMatches = [...sourceCode.matchAll(styleRegex)];
|
||||
|
||||
if (styleMatches.length > 0) {
|
||||
// Remove the style tags
|
||||
styleMatches.forEach(match => {
|
||||
css += match[1] + '\n';
|
||||
html = html.replace(match[0], '');
|
||||
});
|
||||
}
|
||||
|
||||
// Parse the content inside the <script> tags
|
||||
const scriptRegex = /<script[^>]*>([\s\S]*?)<\/script>/gi;
|
||||
const scriptMatches = [...sourceCode.matchAll(scriptRegex)];
|
||||
|
||||
if (scriptMatches.length > 0) {
|
||||
// Remove the script tags
|
||||
scriptMatches.forEach(match => {
|
||||
js += match[1] + '\n';
|
||||
html = html.replace(match[0], '');
|
||||
});
|
||||
}
|
||||
|
||||
// Create the full HTML document
|
||||
let fullCode = sourceCode;
|
||||
|
||||
// If it's not a complete HTML document, wrap it
|
||||
if (!sourceCode.includes('<!DOCTYPE html>') && !sourceCode.includes('<html')) {
|
||||
fullCode = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HTML Preview</title>
|
||||
${styleMatches.length > 0 ? styleMatches.map(m => m[0]).join('\n') : ''}
|
||||
</head>
|
||||
<body>
|
||||
${html}
|
||||
${scriptMatches.length > 0 ? scriptMatches.map(m => m[0]).join('\n') : ''}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
return {
|
||||
html,
|
||||
css,
|
||||
js,
|
||||
fullCode,
|
||||
};
|
||||
};
|
||||
|
||||
setParsedCode(parseCode(code));
|
||||
}, [code]);
|
||||
|
||||
// Listen for fullscreen change events
|
||||
useEffect(() => {
|
||||
const handleFullscreenChange = () => {
|
||||
setIsFullscreen(
|
||||
document.fullscreenElement ||
|
||||
document.webkitFullscreenElement ||
|
||||
document.mozFullScreenElement ||
|
||||
document.msFullscreenElement,
|
||||
);
|
||||
};
|
||||
|
||||
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
||||
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
|
||||
document.addEventListener('mozfullscreenchange', handleFullscreenChange);
|
||||
document.addEventListener('MSFullscreenChange', handleFullscreenChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
||||
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
|
||||
document.removeEventListener('mozfullscreenchange', handleFullscreenChange);
|
||||
document.removeEventListener('MSFullscreenChange', handleFullscreenChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const showModal = () => {
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
// If in full screen mode, exit first
|
||||
if (isFullscreen) {
|
||||
exitFullscreen();
|
||||
}
|
||||
setIsModalVisible(false);
|
||||
};
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
// Download the HTML file
|
||||
const downloadHTML = () => {
|
||||
// Create a Blob object
|
||||
const blob = new Blob([parsedCode.fullCode], { type: 'text/html' });
|
||||
|
||||
// Create a URL object
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Create an a tag
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'preview.html'; // File name
|
||||
|
||||
// Add to body and trigger click
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Clean up
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// Toggle fullscreen mode
|
||||
const toggleFullscreen = () => {
|
||||
if (isFullscreen) {
|
||||
exitFullscreen();
|
||||
} else {
|
||||
enterFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
// Enter fullscreen mode
|
||||
const enterFullscreen = () => {
|
||||
const elem = iframeRef.current;
|
||||
if (!elem) return;
|
||||
|
||||
if (elem.requestFullscreen) {
|
||||
elem.requestFullscreen();
|
||||
} else if (elem.webkitRequestFullscreen) {
|
||||
/* Safari */
|
||||
elem.webkitRequestFullscreen();
|
||||
} else if (elem.msRequestFullscreen) {
|
||||
/* IE11 */
|
||||
elem.msRequestFullscreen();
|
||||
} else if (elem.mozRequestFullScreen) {
|
||||
/* Firefox */
|
||||
elem.mozRequestFullScreen();
|
||||
}
|
||||
};
|
||||
|
||||
// Exit fullscreen mode
|
||||
const exitFullscreen = () => {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
/* Safari */
|
||||
document.webkitExitFullscreen();
|
||||
} else if (document.msExitFullscreen) {
|
||||
/* IE11 */
|
||||
document.msExitFullscreen();
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
/* Firefox */
|
||||
document.mozCancelFullScreen();
|
||||
}
|
||||
};
|
||||
|
||||
// Create tab items
|
||||
const getTabItems = () => {
|
||||
const items = [
|
||||
{
|
||||
key: 'preview',
|
||||
label: t('code_preview'),
|
||||
children: (
|
||||
<div className='relative'>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
srcDoc={parsedCode.fullCode}
|
||||
style={{ width: '100%', height: '60vh', border: 'none' }}
|
||||
sandbox='allow-scripts allow-same-origin'
|
||||
title='HTML Preview'
|
||||
/>
|
||||
<Button
|
||||
type='primary'
|
||||
icon={isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||||
onClick={toggleFullscreen}
|
||||
className='absolute top-2 right-2 z-10'
|
||||
size='small'
|
||||
>
|
||||
{isFullscreen ? t('code_preview_exit_full_screen') : t('code_preview_full_screen')}
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// Only show the code tab when the code is parsed into multiple parts
|
||||
if (parsedCode.html || parsedCode.css || parsedCode.js) {
|
||||
items.push({
|
||||
key: 'code',
|
||||
label: t('code_preview_code'),
|
||||
children: (
|
||||
<div className='p-4'>
|
||||
{parsedCode.html && (
|
||||
<div className='mb-4'>
|
||||
<h3 className='text-lg font-medium mb-2'>HTML</h3>
|
||||
<CodePreview code={parsedCode.html} language='html' />
|
||||
</div>
|
||||
)}
|
||||
{parsedCode.css && (
|
||||
<div className='mb-4'>
|
||||
<h3 className='text-lg font-medium mb-2'>CSS</h3>
|
||||
<CodePreview code={parsedCode.css} language='css' />
|
||||
</div>
|
||||
)}
|
||||
{parsedCode.js && (
|
||||
<div className='mb-4'>
|
||||
<h3 className='text-lg font-medium mb-2'>JavaScript</h3>
|
||||
<CodePreview code={parsedCode.js} language='javascript' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
{/* Code preview section */}
|
||||
<CodePreview code={code} language={language} />
|
||||
|
||||
{/* Operation button */}
|
||||
<div className='absolute bottom-2 right-2 flex gap-2'>
|
||||
<Button
|
||||
type='text'
|
||||
icon={<CopyOutlined />}
|
||||
onClick={copyToClipboard}
|
||||
className='flex items-center justify-center bg-opacity-70 hover:bg-opacity-100 transition-all'
|
||||
size='small'
|
||||
>
|
||||
{isCopied ? t('code_preview_already_copied') : t('code_preview_copy')}
|
||||
</Button>
|
||||
<Button
|
||||
type='text'
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={downloadHTML}
|
||||
className='flex items-center justify-center bg-opacity-70 hover:bg-opacity-100 transition-all'
|
||||
size='small'
|
||||
>
|
||||
{t('code_preview_download')}
|
||||
</Button>
|
||||
<Button
|
||||
type='primary'
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={showModal}
|
||||
className='flex items-center justify-center bg-opacity-70 hover:bg-opacity-100 transition-all'
|
||||
size='small'
|
||||
>
|
||||
{t('code_preview_run')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Run preview modal */}
|
||||
<Modal
|
||||
title={'HTML ' + t('code_preview')}
|
||||
open={isModalVisible}
|
||||
onCancel={handleCancel}
|
||||
footer={[
|
||||
<Button key='download' icon={<DownloadOutlined />} onClick={downloadHTML}>
|
||||
{t('code_preview_download')} HTML
|
||||
</Button>,
|
||||
<Button
|
||||
key='fullscreen'
|
||||
icon={isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||||
onClick={toggleFullscreen}
|
||||
>
|
||||
{isFullscreen ? t('code_preview_exit_full_screen') : t('code_preview_full_screen')}
|
||||
</Button>,
|
||||
<Button key='close' onClick={handleCancel}>
|
||||
{t('code_preview_close')}
|
||||
</Button>,
|
||||
]}
|
||||
width={800}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<Tabs defaultActiveKey='preview' items={getTabItems()} />
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HtmlPreview;
|
299
web/components/chat/chat-content/svg-preview.tsx
Normal file
299
web/components/chat/chat-content/svg-preview.tsx
Normal file
@@ -0,0 +1,299 @@
|
||||
import {
|
||||
CopyOutlined,
|
||||
DownloadOutlined,
|
||||
PlayCircleOutlined,
|
||||
ReloadOutlined,
|
||||
ZoomInOutlined,
|
||||
ZoomOutOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Modal, Slider, Space, Tabs } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CodePreview } from './code-preview';
|
||||
|
||||
/**
|
||||
* SVG preview component is used to display SVG code and provide preview and download functionality
|
||||
* @param {Object} props The component props
|
||||
* @param {string} props.code SVG code content
|
||||
* @param {string} props.language Code language, default is svg
|
||||
*/
|
||||
const SvgPreview = ({ code, language = 'svg' }) => {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const [zoom, setZoom] = useState(100);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Clean up SVG code (remove XML declaration, etc.)
|
||||
const cleanSvgCode = svgCode => {
|
||||
// Remove XML declaration
|
||||
let cleaned = svgCode.replace(/<\?xml[^>]*\?>/g, '');
|
||||
|
||||
// Fix incomplete SVG tags
|
||||
if (!cleaned.includes('<svg')) {
|
||||
cleaned = `<svg xmlns="http://www.w3.org/2000/svg">${cleaned}</svg>`;
|
||||
}
|
||||
|
||||
// Make sure there is a correct xmlns attribute
|
||||
if (!cleaned.includes('xmlns=') && cleaned.includes('<svg')) {
|
||||
cleaned = cleaned.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
|
||||
}
|
||||
|
||||
// Make sure there is a viewBox attribute (if not)
|
||||
if (!cleaned.includes('viewBox') && cleaned.includes('<svg')) {
|
||||
// Try to create a viewBox from width and height
|
||||
const widthMatch = cleaned.match(/width=["']([^"']*)["']/);
|
||||
const heightMatch = cleaned.match(/height=["']([^"']*)["']/);
|
||||
|
||||
if (widthMatch && heightMatch) {
|
||||
const width = widthMatch[1].replace(/[^\d.]/g, '');
|
||||
const height = heightMatch[1].replace(/[^\d.]/g, '');
|
||||
|
||||
if (width && height) {
|
||||
cleaned = cleaned.replace('<svg', `<svg viewBox="0 0 ${width} ${height}"`);
|
||||
}
|
||||
} else {
|
||||
// If no width and height, add a default viewBox
|
||||
cleaned = cleaned.replace('<svg', '<svg viewBox="0 0 800 600"');
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the closing tag is complete
|
||||
if (!cleaned.includes('</svg>')) {
|
||||
cleaned = `${cleaned}</svg>`;
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
};
|
||||
|
||||
// Get SVG content
|
||||
const getSvgContent = () => {
|
||||
// Create a regex to match the SVG tag
|
||||
const svgRegex = /<svg[\s\S]*<\/svg>/im;
|
||||
const match = code.match(svgRegex);
|
||||
|
||||
if (match) {
|
||||
// If a complete SVG tag is found, use only that part
|
||||
return cleanSvgCode(match[0]);
|
||||
} else {
|
||||
// Otherwise, try to clean up the whole code
|
||||
return cleanSvgCode(code);
|
||||
}
|
||||
};
|
||||
|
||||
const showModal = () => {
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsModalVisible(false);
|
||||
// Reset zoom
|
||||
setZoom(100);
|
||||
};
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
// Download SVG file
|
||||
const downloadSVG = () => {
|
||||
// Create a Blob object
|
||||
const blob = new Blob([getSvgContent()], { type: 'image/svg+xml' });
|
||||
|
||||
// Create a URL object
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Create an a tag
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'image.svg'; // 文件名
|
||||
|
||||
// Add to body and trigger click
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Clean up
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// Download PNG file
|
||||
const downloadPNG = () => {
|
||||
// Create a canvas
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Create an image element
|
||||
const img = new Image();
|
||||
const svgBlob = new Blob([getSvgContent()], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(svgBlob);
|
||||
|
||||
img.onload = () => {
|
||||
// Resize the canvas to match the SVG size
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
|
||||
// Draw the SVG to the canvas
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
// Try to convert to PNG and download
|
||||
try {
|
||||
const pngUrl = canvas.toDataURL('image/png');
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = pngUrl;
|
||||
a.download = 'image.png';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
} catch (e) {
|
||||
console.error('PNG export failed:', e);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
img.src = url;
|
||||
};
|
||||
|
||||
// Control zoom
|
||||
const handleZoomChange = value => {
|
||||
setZoom(value);
|
||||
};
|
||||
|
||||
const zoomIn = () => {
|
||||
setZoom(Math.min(zoom + 10, 200));
|
||||
};
|
||||
|
||||
const zoomOut = () => {
|
||||
setZoom(Math.max(zoom - 10, 50));
|
||||
};
|
||||
|
||||
const resetZoom = () => {
|
||||
setZoom(100);
|
||||
};
|
||||
|
||||
// Create tab items
|
||||
const getTabItems = () => {
|
||||
const items = [
|
||||
{
|
||||
key: 'preview',
|
||||
label: t('code_preview'),
|
||||
children: (
|
||||
<div className='relative'>
|
||||
<div className='flex justify-center items-center p-4 bg-gray-100 dark:bg-gray-800 min-h-[60vh] overflow-auto'>
|
||||
<div className='relative bg-white dark:bg-gray-700 p-4 shadow-md rounded flex items-center justify-center'>
|
||||
<div
|
||||
className='transition-transform duration-200'
|
||||
style={{
|
||||
transform: `scale(${zoom / 100})`,
|
||||
transformOrigin: 'center center',
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className='svg-container'
|
||||
dangerouslySetInnerHTML={{ __html: getSvgContent() }}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
margin: '0 auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center justify-center p-4 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800'>
|
||||
<Space>
|
||||
<Button icon={<ZoomOutOutlined />} onClick={zoomOut} disabled={zoom <= 50} />
|
||||
<Slider min={50} max={200} value={zoom} onChange={handleZoomChange} style={{ width: 200 }} />
|
||||
<Button icon={<ZoomInOutlined />} onClick={zoomIn} disabled={zoom >= 200} />
|
||||
<Button icon={<ReloadOutlined />} onClick={resetZoom} disabled={zoom === 100} />
|
||||
<span className='text-sm text-gray-500 dark:text-gray-400 min-w-[50px]'>{zoom}%</span>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'code',
|
||||
label: t('code_preview_code'),
|
||||
children: (
|
||||
<div className='p-4'>
|
||||
<CodePreview code={code} language='svg' />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
{/* Code preview section */}
|
||||
<CodePreview code={code} language={language} />
|
||||
|
||||
{/* Operation button */}
|
||||
<div className='absolute bottom-2 right-2 flex gap-2'>
|
||||
<Button
|
||||
type='text'
|
||||
icon={<CopyOutlined />}
|
||||
onClick={copyToClipboard}
|
||||
className='flex items-center justify-center bg-opacity-70 hover:bg-opacity-100 transition-all'
|
||||
size='small'
|
||||
>
|
||||
{isCopied ? t('code_preview_already_copied') : t('code_preview_copy')}
|
||||
</Button>
|
||||
<Button
|
||||
type='text'
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={downloadSVG}
|
||||
className='flex items-center justify-center bg-opacity-70 hover:bg-opacity-100 transition-all'
|
||||
size='small'
|
||||
>
|
||||
{t('code_preview_download')}
|
||||
</Button>
|
||||
<Button
|
||||
type='primary'
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={showModal}
|
||||
className='flex items-center justify-center bg-opacity-70 hover:bg-opacity-100 transition-all'
|
||||
size='small'
|
||||
>
|
||||
{t('code_preview')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Preview modal */}
|
||||
<Modal
|
||||
title={'SVG ' + t('code_preview')}
|
||||
open={isModalVisible}
|
||||
onCancel={handleCancel}
|
||||
footer={[
|
||||
<Button key='svg' icon={<DownloadOutlined />} onClick={downloadSVG}>
|
||||
{t('code_preview_download')} SVG
|
||||
</Button>,
|
||||
<Button key='png' onClick={downloadPNG}>
|
||||
{t('code_preview_download')} PNG
|
||||
</Button>,
|
||||
<Button key='close' onClick={handleCancel}>
|
||||
{t('code_preview_close')}
|
||||
</Button>,
|
||||
]}
|
||||
width={800}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<Tabs defaultActiveKey='preview' items={getTabItems()} />
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SvgPreview;
|
Reference in New Issue
Block a user