实现Markdown文本转html并使用html2canvas导出图片

发布于:2025-06-24 ⋅ 阅读:(18) ⋅ 点赞:(0)

现需要实现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>

实现效果:

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到