import React, { useState, useRef, useEffect } from 'react';
import styled from 'styled-components';
import { marked } from 'marked';
import { useTranslation } from '../js/i18n';
import SEO from './SEO';
import DOMPurify from 'dompurify';
import { usePageLoading } from '../hooks/usePageLoading';
import LoadingOverlay from './LoadingOverlay';
// 更新预设模板
const templates = [
{
name: 'simple',
bgColor: 'linear-gradient(135deg, #ffffff 0%, #f5f7ff 100%)',
fallbackColor: '#ffffff',
textColor: '#2d3748',
font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
padding: '40px 45px'
},
{
name: 'ai-style',
bgColor: 'linear-gradient(120deg, #0A2463 0%, #3E92CC 100%)',
fallbackColor: '#0A2463',
textColor: '#ffffff',
font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
padding: '40px 45px'
},
{
name: 'dark',
bgColor: 'linear-gradient(135deg, #1a202c 0%, #2d3748 100%)',
fallbackColor: '#1a202c',
textColor: '#f7fafc',
font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
padding: '40px 45px'
},
{
name: 'paper',
bgColor: 'linear-gradient(135deg, #fdf6e3 0%, #f9f3db 100%)',
fallbackColor: '#fdf6e3',
textColor: '#433422',
font: 'Georgia, "Nimbus Roman No9 L", "Songti SC", serif',
padding: '40px 45px'
},
{
name: 'minimal',
bgColor: 'linear-gradient(135deg, #f8f9fa 0%, #edf2f7 100%)',
fallbackColor: '#f8f9fa',
textColor: '#1a202c',
font: '-apple-system, "SF Pro Text", sans-serif',
padding: '40px 45px'
},
{
name: 'tech',
bgColor: 'linear-gradient(135deg, #0f1b3d 0%, #1e293b 100%)',
fallbackColor: '#0f1b3d',
textColor: '#e2e8f0',
font: '"SF Mono", SFMono-Regular, Consolas, monospace',
padding: '40px 45px'
}
];
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 InputContainer = 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);
display: flex;
flex-direction: column;
gap: 1rem;
`;
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 Section = styled.div`
display: flex;
flex-direction: column;
gap: 0.5rem;
`;
const TemplateSection = styled(Section)`
margin-bottom: 1rem;
`;
const EditorSection = styled(Section)`
flex: 1;
display: flex;
flex-direction: column;
`;
const Label = styled.label`
font-size: 1rem;
color: #333333;
`;
const TemplateGrid = styled.div`
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
`;
const TemplateItem = styled.button`
padding: 0.5rem 1rem;
background: ${props => props.selected ?
'linear-gradient(135deg, #6366F1 0%, #4F46E5 100%)' :
'rgba(255, 255, 255, 0.8)'
};
color: ${props => props.selected ? '#ffffff' : '#333333'};
border: 2px solid ${props => props.selected ? '#4F46E5' : 'rgba(99, 102, 241, 0.1)'};
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15);
border-color: rgba(99, 102, 241, 0.3);
}
${props => props.selected && `
&::after {
content: '✓';
position: absolute;
top: 4px;
right: 4px;
font-size: 12px;
color: #ffffff;
}
`}
`;
const Editor = styled.textarea`
width: 100%;
height: 500px;
padding: 1rem;
border: none;
background: transparent;
font-family: 'SF Mono', monospace;
font-size: 14px;
line-height: 1.5;
resize: vertical;
color: #1a1a1a;
overflow-y: auto;
&:focus {
outline: none;
}
&::placeholder {
color: #64748b;
}
`;
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;
}
`;
const PreviewContainer = styled(InputContainer)`
overflow: auto;
position: relative;
min-height: 400px;
display: block;
img {
max-width: 100%;
height: auto;
display: block;
margin: 1em auto;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 1.5em;
margin-bottom: 0.5em;
font-weight: 600;
line-height: 1.3;
}
p {
margin: 1em 0;
line-height: 1.6;
}
ul, ol {
margin: 1em 0;
padding-left: 1em;
list-style-type: none;
}
li {
margin: 0.5em 0;
line-height: 1.6;
list-style-type: none;
position: relative;
padding-left: 1.2em;
}
/* 为无序列表项添加自定义标记 */
ul li::before {
content: "•";
position: absolute;
left: 0;
top: -0.25em; /* 使用更大的负值,进一步向上移动圆点 */
color: #4F46E5; /* 使用主题色 */
font-weight: bold;
font-size: 1.2em;
line-height: 1.6;
display: inline-block; /* 更好的对齐控制 */
}
/* 为有序列表项添加自定义标记 */
ol {
counter-reset: item;
}
ol li::before {
content: counter(item) ".";
counter-increment: item;
position: absolute;
left: 0;
color: #4F46E5; /* 使用主题色 */
font-weight: bold;
line-height: 1.6;
display: inline-block; /* 更好的对齐控制 */
}
pre, code {
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
padding: 0.2em 0.4em;
font-family: 'SF Mono', monospace;
}
pre code {
background: none;
padding: 0;
}
blockquote {
border-left: 4px solid #e2e8f0;
margin: 1em 0;
padding-left: 1em;
color: #64748b;
}
table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
}
th, td {
border: 1px solid #e2e8f0;
padding: 0.5em;
text-align: left;
}
th {
background: rgba(0, 0, 0, 0.05);
}
`;
function MarkdownToImage() {
const { t } = useTranslation();
const [text, setText] = useState('');
const [selectedTemplate, setSelectedTemplate] = useState(templates[0]);
const previewRef = useRef(null);
const isLoading = usePageLoading();
const handleDownload = async () => {
const previewElement = previewRef.current;
console.log('previewElement', previewElement);
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, reject) => {
img.onload = resolve;
img.onerror = reject;
// 确保图片使用完整的 URL
if (img.src.startsWith('/')) {
img.src = window.location.origin + img.src;
}
// 添加跨域属性
img.crossOrigin = 'anonymous';
});
});
return Promise.all(promises);
};
await waitForImages();
// 等待渲染完成
await new Promise(resolve => setTimeout(resolve, 500));
const html2canvas = (await import('html2canvas')).default;
const canvas = await html2canvas(previewElement, {
backgroundColor: selectedTemplate.fallbackColor,
scale: 2,
useCORS: true,
allowTaint: false,
logging: false,
onclone: (clonedDoc) => {
const clonedElement = clonedDoc.querySelector('.markdown-content');
console.log('clonedElement', clonedElement);
if (clonedElement) {
clonedElement.style.width = '100%';
clonedElement.style.position = 'relative';
clonedElement.style.transform = 'none';
clonedElement.style.transformOrigin = '0 0';
clonedElement.style.overflow = 'visible';
}
}
});
const link = document.createElement('a');
link.download = 'markdown-preview.png';
link.href = canvas.toDataURL('image/png');
link.click();
} catch (error) {
console.error('导出图片失败:', error);
}
};
const renderPreview = () => {
// 配置 marked 选项
marked.setOptions({
gfm: true, // 启用 GitHub 风格的 Markdown
breaks: true, // 启用换行符转换为
headerIds: true,
mangle: false,
pedantic: false,
smartLists: true, // 优化列表输出
smartypants: true, // 优化标点符号
});
// 使用 DOMPurify 清理 HTML
const cleanHtml = DOMPurify.sanitize(marked(text), {
ADD_TAGS: ['img'],
ADD_ATTR: ['src', 'alt'],
});
return (