【Mermaid本地实时渲染 单html本地直接运行】

  • 引入了 svg-pan-zoom 库:这是处理 SVG 交互的标准库,比手写 CSS 变换更流畅。
  • 鼠标滚轮缩放:现在你可以直接在右侧区域滚动鼠标滚轮来放大/缩小。
  • 鼠标拖拽移动:按住鼠标左键即可拖动流程图(平移)。
  • 无缝集成:当修改代码重新渲染时,会自动重置视图,确保体验流畅。
  • Mermaid官网 Examples

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mermaid 实时编辑器</title>
    <!-- Tailwind CSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- FontAwesome 图标 -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <!-- Mermaid.js -->
    <script src="https://cdn.jsdelivr.net/npm/mermaid@11.12.2/dist/mermaid.min.js"></script>
    <!-- svg-pan-zoom 库 -->
    <script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"></script>

    <style>
        ::-webkit-scrollbar { width: 8px; height: 8px; }
        ::-webkit-scrollbar-track { background: #f1f1f1; }
        ::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 4px; }
        ::-webkit-scrollbar-thumb:hover { background: #a8a8a8; }
        
        .code-font { font-family: 'Menlo', 'Monaco', 'Courier New', monospace; }
        body { background-color: #f5f7fa; }
        
        #error-container {
            display: none;
            color: #ef4444;
            background: #fee2e2;
            padding: 10px;
            border-radius: 6px;
            margin-top: 10px;
            font-size: 0.875rem;
            white-space: pre-wrap;
        }

        #graph-container {
            cursor: grab;
            background-image: radial-gradient(#e5e7eb 1px, transparent 1px);
            background-size: 20px 20px;
            position: relative;
        }
        #graph-container:active { cursor: grabbing; }
        
        #graphDiv { width: 100%; height: 100%; }

        /* 加载遮罩层 */
        #loading-overlay {
            position: absolute;
            inset: 0;
            background: rgba(255,255,255,0.8);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 50;
            backdrop-filter: blur(2px);
            display: none; /* 默认隐藏 */
        }
        .spinner {
            width: 30px; height: 30px;
            border: 3px solid #e5e7eb;
            border-top-color: #4f46e5;
            border-radius: 50%;
            animation: spin 0.8s linear infinite;
        }
        @keyframes spin { to { transform: rotate(360deg); } }

        /* 行号样式微调 */
        #line-numbers {
            min-width: 40px;
            text-align: right;
            user-select: none;
        }
    </style>
</head>
<body class="h-screen flex flex-col overflow-hidden">

    <!-- 顶部导航 -->
    <div class="bg-white border-b border-gray-200 px-6 py-3 flex justify-between items-center shrink-0 shadow-sm z-10">
        <div class="flex items-center gap-2">
            <span class="font-bold text-xl text-indigo-600">Mermaid</span>
            <span class="text-xs text-gray-400 bg-gray-100 px-2 py-0.5 rounded">Live Editor</span>
        </div>
        
        <div class="flex space-x-3 text-gray-500">
            <button onclick="downloadSVG()" title="下载 SVG" class="hover:text-indigo-600 hover:bg-indigo-50 px-3 py-1.5 rounded transition flex items-center gap-2 text-sm font-medium">
                <i class="fas fa-download"></i> 导出 SVG
            </button>
        </div>
    </div>

    <!-- 主体内容 -->
    <div class="flex flex-1 overflow-hidden p-4 gap-4">
        
        <!-- 左侧:代码编辑 -->
        <div class="w-1/3 flex flex-col min-w-[300px]">
            <div class="bg-white rounded-t-lg border border-gray-200 border-b-0 px-4 py-2 text-xs font-bold text-gray-500 uppercase flex justify-between items-center">
                <span>Source Code</span>
            </div>
            
            <!-- 修改:添加包裹容器以实现行号布局 -->
            <div class="flex-1 flex overflow-hidden relative border border-gray-200 shadow-sm bg-white focus-within:border-indigo-400 transition-colors">
                <!-- 行号栏 -->
                <div id="line-numbers" class="bg-gray-50 text-gray-400 pt-4 pr-2 code-font text-sm leading-relaxed border-r border-gray-100 overflow-hidden">
                    1
                </div>
                <!-- 编辑框 (修改了 padding 和 whitespace) -->
                <textarea id="inputCode" class="flex-1 w-full p-4 pl-3 resize-none outline-none code-font text-sm text-gray-700 bg-transparent border-none shadow-none focus:ring-0 leading-relaxed whitespace-pre" spellcheck="false" placeholder="在此输入 Mermaid 代码..."></textarea>
            </div>

            <div id="error-container" class="shadow-sm"></div>
        </div>

        <!-- 右侧:预览画布 -->
        <div class="flex-1 bg-white rounded-lg shadow-sm border border-gray-200 relative overflow-hidden flex flex-col">
            <div class="bg-gray-50 px-4 py-2 border-b border-gray-200 text-xs font-bold text-gray-500 uppercase flex justify-between items-center z-10">
                <span>Preview</span>
                <span class="text-gray-400 text-[10px] flex items-center gap-1">
                    <i class="fas fa-mouse"></i> 滚轮缩放 / 拖拽移动
                </span>
            </div>
            
            <!-- 绘图容器 -->
            <div id="graph-container" class="flex-1 overflow-hidden relative">
                <!-- 加载动画 -->
                <div id="loading-overlay"><div class="spinner"></div></div>
                <!-- SVG 容器 -->
                <div id="graphDiv" class="h-full w-full"></div>
            </div>

            <!-- 右下角控制栏 -->
            <div class="absolute bottom-6 right-6 flex bg-white border border-gray-200 rounded-lg shadow-lg z-20">
                <button onclick="zoomAct('in')" class="p-2 w-10 hover:bg-gray-50 text-gray-600 border-r border-gray-200 transition" title="放大"><i class="fas fa-plus"></i></button>
                <button onclick="zoomAct('reset')" class="p-2 w-10 hover:bg-gray-50 text-gray-600 border-r border-gray-200 transition" title="自适应"><i class="fas fa-compress-arrows-alt"></i></button>
                <button onclick="zoomAct('out')" class="p-2 w-10 hover:bg-gray-50 text-gray-600 transition" title="缩小"><i class="fas fa-minus"></i></button>
            </div>
        </div>
    </div>

    <script>
        // 1. 初始化 Mermaid
        mermaid.initialize({ 
            startOnLoad: false,
            theme: 'default',
            securityLevel: 'loose',
            logLevel: 'error',
            er: {
                useMaxWidth: false // 尝试禁用最大宽度(部分版本有效)
            }
        });

        // 默认演示代码
        const defaultCode = ``;

        const inputEl = document.getElementById('inputCode');
        const lineNumEl = document.getElementById('line-numbers');
        const graphDiv = document.getElementById('graphDiv');
        const errorContainer = document.getElementById('error-container');
        const loadingOverlay = document.getElementById('loading-overlay');
        
        let panZoomInstance = null;

        inputEl.value = defaultCode;

        // 新增:更新行号逻辑
        const updateLineNumbers = () => {
            const lines = inputEl.value.split('\n').length;
            // 为了保证性能,如果行数没变可以不做操作,这里简单处理全量更新
            // 使用 <br> 保证换行与 textarea 一致
            lineNumEl.innerHTML = Array(lines).fill(0).map((_, i) => i + 1).join('<br>');
        };

        // 新增:行号同步滚动
        inputEl.addEventListener('scroll', () => {
            lineNumEl.scrollTop = inputEl.scrollTop;
        });

        // 2. 渲染核心函数
        const renderDiagram = async () => {
            const code = inputEl.value.trim();
            if (!code) return;

            // 显示加载状态
            loadingOverlay.style.display = 'flex';
            errorContainer.style.display = 'none';

            try {
                // 生成唯一ID
                const id = 'mermaid-svg-' + Date.now();
                
                // 验证语法
                if (!await mermaid.parse(code)) {
                    throw new Error('Syntax Error');
                }

                // 渲染 SVG
                const { svg } = await mermaid.render(id, code);
                graphDiv.innerHTML = svg;
                
                // 清理 Mermaid 的默认样式以兼容 svg-pan-zoom
                const svgElement = graphDiv.querySelector('svg');
                if (svgElement) {
                    // 移除 style 属性(特别是 max-width),否则 pan-zoom 会失效
                    svgElement.removeAttribute('style');
                    // 显式设置宽高为 100% 以填满容器
                    svgElement.setAttribute('width', '100%');
                    svgElement.setAttribute('height', '100%');
                }

                setupPanZoom();

            } catch (error) {
                console.error(error);
                errorContainer.style.display = 'block';
                // 简单的错误信息格式化
                const msg = error.message || error.str || '语法错误,请检查代码';
                errorContainer.innerText = msg;
            } finally {
                // 隐藏加载状态
                loadingOverlay.style.display = 'none';
            }
        };

        // 3. 配置 svg-pan-zoom
        function setupPanZoom() {
            if (panZoomInstance) {
                panZoomInstance.destroy();
                panZoomInstance = null;
            }

            const svgElement = graphDiv.querySelector('svg');
            if (!svgElement) return;

            panZoomInstance = svgPanZoom(svgElement, {
                zoomEnabled: true,
                controlIconsEnabled: false,
                fit: true,
                center: true,
                minZoom: 0.1,
                maxZoom: 10,
                dblClickZoomEnabled: false
            });
        }

        // 4. 防抖处理
        let debounceTimer;
        inputEl.addEventListener('input', () => {
            updateLineNumbers(); // 输入时立即更新行号
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(renderDiagram, 500);
        });

        // 5. 缩放控制
        function zoomAct(action) {
            if (!panZoomInstance) return;
            switch(action) {
                case 'in': panZoomInstance.zoomIn(); break;
                case 'out': panZoomInstance.zoomOut(); break;
                case 'reset': 
                    panZoomInstance.reset(); 
                    panZoomInstance.fit();
                    panZoomInstance.center();
                    break;
            }
        }

        // 6. 下载 SVG (增强版:清理 PanZoom 注入的属性)
        function downloadSVG() {
            const svgEl = graphDiv.querySelector('svg');
            if (!svgEl) return;
            
            // 克隆节点以免影响当前视图
            const clone = svgEl.cloneNode(true);
            
            // 1. 移除 style 和 class
            clone.removeAttribute('style');
            clone.removeAttribute('class');
            clone.setAttribute('width', '100%'); // 或者设为具体的 viewBox 尺寸
            
            // 2. 尝试解包 svg-pan-zoom 创建的 <g class="svg-pan-zoom_viewport">
            // 这样下载的文件在 Illustrator 中打开结构更干净
            const viewportGroup = clone.querySelector('.svg-pan-zoom_viewport');
            if (viewportGroup) {
                // 将 viewport 内部的内容移到 SVG 根节点下,并移除 transform
                // 注意:这会重置缩放,下载的是全图
                const innerContents = Array.from(viewportGroup.childNodes);
                innerContents.forEach(node => clone.appendChild(node));
                clone.removeChild(viewportGroup);
            }

            // 添加 XML 声明
            const svgData = '<?xml version="1.0" encoding="UTF-8"?>' + clone.outerHTML;
            const blob = new Blob([svgData], {type: 'image/svg+xml;charset=utf-8'});
            const url = URL.createObjectURL(blob);
            
            const link = document.createElement('a');
            link.href = url;
            link.download = `mermaid-diagram-${Date.now()}.svg`;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        }

        // 首次加载
        window.addEventListener('load', () => {
            updateLineNumbers(); // 初始化行号
            renderDiagram();
        });

        // 窗口大小改变时重置
        window.addEventListener('resize', () => {
            if(panZoomInstance) panZoomInstance.resize();
        });

    </script>
</body>
</html>
相关推荐
共享家95277 小时前
打造AI智能”成语接龙“游戏
前端·javascript·人工智能·python·游戏·html
AAA阿giao7 小时前
HTML5模块化开发:结构、样式与交互分离
前端·html·html5
顾西爵霞7 小时前
HTML 零基础入门:像搭积木一样建网页
前端·html
顾西爵霞8 小时前
HTML 进阶:给房子装窗户、通道和前台系统
前端·javascript·html
weixin_456907411 天前
2026+:html+css 生态的成型之年与平台化跃迁
前端·css·html
weixin_456907411 天前
【html+Tss 故障排查】链20230304 最详细解析之像素已拉取,容器仍起不来(含命令清单)
前端·html
Zhu_S W1 天前
基于Java和Redis实现排行榜功能
前端·bootstrap·html
Eiceblue1 天前
通过 C# 解析 HTML:文本提取 + 结构化数据获取
c#·html·.net·visual studio
weixin_456907411 天前
使用 html为 ppt 文档添加文本像素格的实用方法
html·tensorflow·powerpoint