现需要实现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为HTML
function convertMarkdown() {
const markdown = markdownInput.value;
if (!markdown.trim()) {
showError('请输入Markdown内容');
return;
}
showLoading();
try {
// 使用marked库转换Markdown为HTML
const 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>