实现Markdown文本转html并使用html2canvas导出图片
现需要实现Markdown文本转html并导出图片的功能。先使用html静态页面尝试完成。
页面代码
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Markdown编辑器</title><script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script><script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script><style>:root {--primary-color: #3498db;--secondary-color: #2ecc71;--danger-color: #e74c3c;--dark-color: #34495e;--light-color: #ecf0f1;--border-color: #dcdde1;--shadow-color: rgba(0, 0, 0, 0.1);--text-color: #2c3e50;--code-bg: #f8f9fa;}* {box-sizing: border-box;margin: 0;padding: 0;}body {font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;margin: 0;padding: 0;background-color: #f5f7fa;color: var(--text-color);line-height: 1.6;height: 100vh;overflow: hidden;display: flex;flex-direction: column;}.container {flex: 1;display: flex;flex-direction: column;margin: 0;padding: 1rem;background-color: white;overflow: hidden;}.editor-container {display: flex;gap: 1rem;flex: 1;overflow: hidden;min-height: 400px;/* 确保有足够的最小高度 */height: calc(100vh - 120px);/* 动态计算高度,减去其他元素的高度 */}@media (max-width: 992px) {.editor-container {flex-direction: column;}}.editor-section {flex: 1;min-width: 300px;min-height: 400px;display: flex;flex-direction: column;overflow: hidden;}/* 右侧预览区域的布局样式 */.preview-container {display: flex;flex-direction: column;flex: 1;overflow: hidden;}.tab-container {display: flex;flex-direction: row;gap: 0.5rem;margin-bottom: 0.5rem;}.editor-textarea {width: 100%;flex: 1;padding: 1rem;border: 1px solid var(--border-color);border-radius: 8px;resize: none;font-family: 'Consolas', 'Monaco', 'Courier New', monospace;font-size: 15px;line-height: 1.6;background-color: #fafafa;color: var(--dark-color);overflow-y: auto;}.editor-textarea:focus {outline: none;border-color: var(--primary-color);}.preview-section {flex: 1;border: 1px solid var(--border-color);border-radius: 8px;padding: 1rem;overflow-y: auto;background-color: white;min-height: 300px;/* 添加默认最小高度 */height: 100%;/* 确保填充父容器 */}.button-group {margin-bottom: 1rem;display: flex;gap: 0.5rem;flex-wrap: wrap;}.btn {padding: 0.5rem 1rem;border: none;border-radius: 4px;cursor: pointer;font-size: 0.9rem;font-weight: 500;transition: all 0.2s ease;background-color: var(--primary-color);color: white;}.btn-primary {background-color: var(--primary-color);}.btn-secondary {background-color: var(--secondary-color);}.btn-danger {background-color: var(--danger-color);}.btn:hover {opacity: 0.9;}.btn:active {opacity: 0.8;}.btn:disabled {background-color: #bdc3c7;cursor: not-allowed;opacity: 0.7;}/* 这个样式已经在上面定义过,这里只添加border-bottom属性 */.tab-container {border-bottom: 1px solid var(--border-color);}.tab {padding: 0.5rem 1rem;background-color: transparent;border: none;border-bottom: 2px solid transparent;margin-right: 0.5rem;cursor: pointer;font-family: inherit;font-size: inherit;color: var(--text-color);text-align: left;outline: none;transition: all 0.2s ease;}.tab.active {color: var(--primary-color);border-bottom: 2px solid var(--primary-color);}.tab:hover:not(.active) {border-bottom: 2px solid #ddd;}.tab-content {padding: 0;flex: 1;display: flex;overflow: hidden;height: 100%;/* 确保填充父容器高度 */min-height: 300px;/* 添加默认最小高度 */}.loading-indicator {display: none;position: fixed;top: 0;left: 0;right: 0;bottom: 0;background-color: rgba(255, 255, 255, 0.7);justify-content: center;align-items: center;z-index: 1000;}.spinner {width: 40px;height: 40px;border: 4px solid rgba(52, 152, 219, 0.2);border-top: 4px solid var(--primary-color);border-radius: 50%;animation: spin 1s linear infinite;}@keyframes spin {0% {transform: rotate(0deg);}100% {transform: rotate(360deg);}}.status-indicator {display: none;position: fixed;bottom: 20px;right: 20px;padding: 0.5rem 1rem;border-radius: 4px;color: white;z-index: 1000;animation: fadeIn 0.2s ease;}.status-indicator.success {background-color: var(--secondary-color);}.status-indicator.error {background-color: var(--danger-color);}.status-indicator.info {background-color: var(--primary-color);}.error-message {display: none;background-color: #fdedec;color: var(--danger-color);padding: 0.5rem 1rem;border-radius: 4px;margin-bottom: 1rem;border-left: 3px solid var(--danger-color);}@keyframes fadeIn {from {opacity: 0;}to {opacity: 1;}}/* 预览区域样式 */.preview-section h1 {font-size: 2em;margin-bottom: 0.7em;color: var(--dark-color);border-bottom: 1px solid var(--light-color);padding-bottom: 0.3em;text-align: center;}.preview-section h2 {font-size: 1.6em;margin-bottom: 0.6em;color: var(--dark-color);text-align: center;display: inline-block;padding: 2px 10px;}.preview-section h3 {font-size: 1.3em;margin-bottom: 0.5em;color: var(--dark-color);}.preview-section p {margin-bottom: 1em;line-height: 1.6;}.preview-section code {background-color: var(--code-bg);padding: 0.2em 0.4em;border-radius: 3px;font-family: "Consolas", "Monaco", "Courier New", monospace;color: #e74c3c;}.preview-section pre {background-color: var(--code-bg);padding: 1em;border-radius: 4px;overflow-x: auto;border: 1px solid var(--border-color);margin-bottom: 1em;}.preview-section pre code {background-color: transparent;color: var(--dark-color);padding: 0;}.preview-section blockquote {border-left: 3px solid var(--primary-color);margin-left: 0;padding: 0.5em 0 0.5em 1em;background-color: rgba(52, 152, 219, 0.05);margin-bottom: 1em;border-radius: 0 4px 4px 0;}.preview-section ul,.preview-section ol {margin-bottom: 1em;padding-left: 2em;}.preview-section li {margin-bottom: 0.3em;}.preview-section img {max-width: 100%;height: auto;border-radius: 4px;}.preview-section a {color: var(--primary-color);text-decoration: none;}.preview-section a:hover {text-decoration: underline;}.preview-section table {border-collapse: collapse;width: 100%;margin-bottom: 1em;overflow: hidden;}.preview-section th,.preview-section td {padding: 0.5rem;text-align: left;border: 1px solid var(--border-color);}.preview-section th {background-color: var(--light-color);font-weight: 600;}.preview-section tr:nth-child(even) {background-color: #f8f9fa;}</style>
</head><body><div class="container"><div class="button-group"><button id="convertBtn" class="btn btn-primary">转换</button><button id="exportBtn" class="btn btn-secondary" disabled>导出为图片</button><button id="clearBtn" class="btn btn-danger">清空</button><button id="copyHtmlBtn" class="btn btn-secondary">复制HTML</button></div><div class="editor-container"><div class="editor-section"><textarea id="markdownInput" class="editor-textarea" placeholder="在此输入Markdown文本..."></textarea></div><div class="editor-section"><div class="preview-container"><div class="tab-container"><button id="previewTab" class="tab active" role="tab" aria-selected="true"aria-controls="previewContent">预览</button><button id="codeTab" class="tab" role="tab" aria-selected="false"aria-controls="codeContent">HTML代码</button></div><div id="previewContent" class="tab-content" style="display: block;"><div id="htmlPreview" class="preview-section"></div></div><div id="codeContent" class="tab-content" style="display: none;"><pre id="htmlCode" class="preview-section"style="white-space: pre-wrap; word-break: break-all;"></pre></div></div></div></div></div><div id="loadingIndicator" class="loading-indicator"><div class="spinner"></div></div><div id="statusIndicator" class="status-indicator"></div><div id="errorMessage" class="error-message"></div><script>const markdownInput = document.getElementById('markdownInput');const htmlPreview = document.getElementById('htmlPreview');const htmlCode = document.getElementById('htmlCode');const convertBtn = document.getElementById('convertBtn');const exportBtn = document.getElementById('exportBtn');const clearBtn = document.getElementById('clearBtn');const copyHtmlBtn = document.getElementById('copyHtmlBtn');const previewTab = document.getElementById('previewTab');const codeTab = document.getElementById('codeTab');const previewContent = document.getElementById('previewContent');const codeContent = document.getElementById('codeContent');const loadingIndicator = document.getElementById('loadingIndicator');const statusIndicator = document.getElementById('statusIndicator');const errorMessage = document.getElementById('errorMessage');// 从本地存储加载保存的内容const savedContent = localStorage.getItem('markdownContent');if (savedContent) {markdownInput.value = savedContent;}// 自动保存功能markdownInput.addEventListener('input', function() {localStorage.setItem('markdownContent', markdownInput.value);});// 转换Markdown为HTMLfunction convertMarkdown() {const markdown = markdownInput.value;if (!markdown.trim()) {showError('请输入Markdown内容');return;}showLoading();try {// 使用marked库转换Markdown为HTMLconst html = marked.parse(markdown);htmlPreview.innerHTML = html;htmlCode.textContent = html;exportBtn.disabled = false;showStatus('转换成功', 'success');} catch (error) {showError('转换失败: ' + error.message);} finally {hideLoading();}}// 导出为图片function exportToImage() {showLoading();html2canvas(htmlPreview).then(canvas => {const link = document.createElement('a');link.download = 'markdown-export.png';link.href = canvas.toDataURL('image/png');link.click();showStatus('导出成功', 'success');}).catch(error => {showError('导出失败: ' + error.message);}).finally(() => {hideLoading();});}// 复制HTML代码function copyHtmlCode() {const html = htmlCode.textContent;if (!html.trim()) {showError('没有HTML代码可复制');return;}navigator.clipboard.writeText(html).then(() => {showStatus('HTML代码已复制到剪贴板', 'success');}).catch(error => {showError('复制失败: ' + error.message);});}// 清空编辑器function clearEditor() {if (confirm('确定要清空编辑器吗?此操作不可撤销。')) {markdownInput.value = '';htmlPreview.innerHTML = '';htmlCode.textContent = '';exportBtn.disabled = true;localStorage.removeItem('markdownContent');showStatus('编辑器已清空', 'info');}}// 切换标签页function switchTab(tab) {if (tab === 'preview') {previewTab.classList.add('active');codeTab.classList.remove('active');previewContent.style.display = 'block';codeContent.style.display = 'none';previewTab.setAttribute('aria-selected', 'true');codeTab.setAttribute('aria-selected', 'false');} else {previewTab.classList.remove('active');codeTab.classList.add('active');previewContent.style.display = 'none';codeContent.style.display = 'block';previewTab.setAttribute('aria-selected', 'false');codeTab.setAttribute('aria-selected', 'true');}}// 显示加载指示器function showLoading() {loadingIndicator.style.display = 'flex';}// 隐藏加载指示器function hideLoading() {loadingIndicator.style.display = 'none';}// 显示状态指示器function showStatus(message, type) {statusIndicator.textContent = message;statusIndicator.className = 'status-indicator ' + type;statusIndicator.style.display = 'block';setTimeout(() => {statusIndicator.style.display = 'none';}, 3000);}// 显示错误消息function showError(message) {errorMessage.textContent = message;errorMessage.style.display = 'block';setTimeout(() => {errorMessage.style.display = 'none';}, 5000);}// 绑定事件监听器convertBtn.addEventListener('click', convertMarkdown);exportBtn.addEventListener('click', exportToImage);clearBtn.addEventListener('click', clearEditor);copyHtmlBtn.addEventListener('click', copyHtmlCode);previewTab.addEventListener('click', () => switchTab('preview'));codeTab.addEventListener('click', () => switchTab('code'));// 初始转换(如果有保存的内容)if (savedContent && savedContent.trim()) {convertMarkdown();}</script>
</body></html>