import React, { useState, useMemo } from 'react'; import styled from 'styled-components'; import katex from 'katex'; import 'katex/dist/katex.min.css'; import DOMPurify from 'dompurify'; import SEO from './SEO'; import { useTranslation } from '../js/i18n'; import html2canvas from 'html2canvas'; import { usePageLoading } from '../hooks/usePageLoading'; import LoadingOverlay from './LoadingOverlay'; // 容器样式 const Container = styled.div` min-height: 100vh; background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%); padding: 4rem 2rem 2rem; position: relative; &::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(90deg, rgba(99, 102, 241, 0.05) 1px, transparent 1px), linear-gradient(rgba(99, 102, 241, 0.05) 1px, transparent 1px); background-size: 20px 20px; pointer-events: none; } `; const ContentWrapper = styled.div` display: flex; gap: 2rem; max-width: 1400px; margin: 0 auto; position: relative; z-index: 1; @media (max-width: 768px) { flex-direction: column; } `; const EditorContainer = styled.div` flex: 1; background: rgba(255, 255, 255, 0.8); backdrop-filter: blur(10px); border-radius: 16px; padding: 1.5rem; box-shadow: 0 8px 32px rgba(99, 102, 241, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); `; const PreviewContainer = styled(EditorContainer)` overflow: auto; position: relative; `; const Editor = styled.textarea` width: 100%; min-height: 400px; padding: 1rem; border: none; background: transparent; font-family: 'SF Mono', monospace; font-size: 14px; line-height: 1.5; resize: vertical; color: #1a1a1a; &:focus { outline: none; } `; const Preview = styled.div` font-family: -apple-system, system-ui, sans-serif; color: #1a1a1a; line-height: 1.6; h1, h2, h3, h4, h5, h6 { margin-top: 1.5em; margin-bottom: 0.5em; font-weight: 600; } p { margin: 1em 0; } img { max-width: 100%; height: auto; } table { border-collapse: collapse; width: 100%; margin: 1em 0; } th, td { border: 1px solid rgba(99, 102, 241, 0.2); padding: 0.5em; } .katex { font-size: 1.1em; } `; const TitleLabel = styled.h2` font-size: 1.8rem; margin-bottom: 1.5rem; background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: 700; letter-spacing: -0.02em; `; const DownloadButton = styled.button` background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%); color: white; padding: 0.5rem 1rem; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; transition: opacity 0.2s; font-size: 0.9rem; position: absolute; top: 1.5rem; right: 1.5rem; opacity: ${props => props.visible ? 1 : 0}; pointer-events: ${props => props.visible ? 'auto' : 'none'}; &:hover { opacity: 0.9; } `; function HtmlPreview() { const [html, setHtml] = useState(''); const { t } = useTranslation(); const isLoading = usePageLoading(); // 处理 LaTeX 公式 const processLatex = (content) => { return content.replace(/\$\$([\s\S]*?)\$\$|\\\[([\s\S]*?)\\\]|\\\(([\s\S]*?)\\\)|\$(.*?)\$/g, (match, p1, p2, p3, p4) => { const formula = p1 || p2 || p3 || p4; const displayMode = match.startsWith('$$') || match.startsWith('\\['); try { return katex.renderToString(formula.trim(), { displayMode, throwOnError: false }); } catch (e) { console.error('LaTeX解析错误:', e); return match; } }); }; // 使用 useMemo 缓存处理后的 HTML const renderedContent = useMemo(() => { if (!html) return ''; try { // 处理 LaTeX 公式 let processedHtml = processLatex(html); // 清理 HTML,防止 XSS 攻击 const sanitizedHtml = DOMPurify.sanitize(processedHtml, { ADD_TAGS: ['math', 'annotation', 'semantics', 'mrow', 'mi', 'mo', 'mn', 'msup', 'mfrac'], ADD_ATTR: ['display', 'mode', 'class'] }); return sanitizedHtml; } catch (e) { console.error('渲染错误:', e); return `渲染错误: ${e.message}`; } }, [html]); const handleDownload = async () => { const previewElement = document.querySelector('.preview-content'); if (!previewElement) return; try { // 等待图片加载 const waitForImages = () => { const images = previewElement.getElementsByTagName('img'); const promises = Array.from(images).map(img => { if (img.complete) return Promise.resolve(); return new Promise((resolve) => { img.onload = resolve; img.onerror = resolve; }); }); return Promise.all(promises); }; await waitForImages(); // 等待 LaTeX 渲染 await new Promise(resolve => setTimeout(resolve, 500)); // 计算实际内容高度(包括所有子元素) const computeActualHeight = (element) => { const style = window.getComputedStyle(element); const marginTop = parseInt(style.marginTop); const marginBottom = parseInt(style.marginBottom); let height = element.offsetHeight + marginTop + marginBottom; // 获取所有子元素的位置信息 const children = element.children; if (children.length > 0) { const lastChild = children[children.length - 1]; const lastChildRect = lastChild.getBoundingClientRect(); const elementRect = element.getBoundingClientRect(); height = Math.max(height, lastChildRect.bottom - elementRect.top + marginBottom + 50); } return height; }; const actualHeight = computeActualHeight(previewElement); const actualWidth = previewElement.offsetWidth; const canvas = await html2canvas(previewElement, { backgroundColor: '#ffffff', scale: 2, width: actualWidth, height: actualHeight, windowWidth: actualWidth, windowHeight: actualHeight, useCORS: true, logging: false, onclone: (clonedDoc) => { const clonedElement = clonedDoc.querySelector('.preview-content'); if (clonedElement) { // 设置固定尺寸 clonedElement.style.width = `${actualWidth}px`; clonedElement.style.height = `${actualHeight}px`; clonedElement.style.position = 'relative'; clonedElement.style.transform = 'none'; clonedElement.style.transformOrigin = '0 0'; // 确保内容不会溢出 clonedElement.style.overflow = 'visible'; clonedElement.style.padding = '20px'; clonedElement.style.boxSizing = 'border-box'; // 强制重新计算布局 clonedElement.style.display = 'block'; } } }); const link = document.createElement('a'); link.download = 'latex-preview.png'; link.href = canvas.toDataURL('image/png'); link.click(); } catch (error) { console.error('导出图片失败:', error); } }; return ( <> {isLoading && } {t('tools.latex2image.title')} setHtml(e.target.value)} placeholder={t('tools.latex2image.placeholder')} /> {t('tools.latex2image.preview')} 0} > {t('tools.latex2image.download')} ); } export default HtmlPreview;