# 使用 Pan/#_ @ om & 导出加强美人鱼图

<!--category-- Mermaid, Javascript, SVG, DaisyUI, Tailwind -->
<datetime class="hidden">2025-11-07T11:15</datetime>

# 一. 导言导言 导言 导言 导言

> **npm 软件包可用@: @%** 现已在 [美人鱼 -增强音量](https://www.npmjs.com/package/@mostlylucid/mermaid-enhancements) @ -} a production *% 1 check npm profession * MPK2* 见 [作为 npm 软件包的美人鱼增强](/blog/publishingmermaidenhancementsnpm) 有关如何在您的项目中使用它的细节@. @%

美人鱼是用文字创建图表的绝佳工具 @ , @ , 但默认翻譯可能限制复杂的图表@ MS K1 @ 用户可以 '} 很容易放大以查看大图周围的详细内容 @ I, 或导出用于文档@.{ 。 在此文章中 @ MOSK6 I' 会显示我如何加强这个网站上的美人魚圖与交互式 pançQK8 @ zoom control@ MPK9_ 全屏光箱查看 < k10\ > 和导出功能 @( both PNG 和 SVG 格式@).}

这是生产 - ready , 处理暗色模式的转换 优雅地@,\ 并且抗云雾火箭装载器干扰\ MS K3\ 适应性强

## 看起来像什么

因此,我们接下来要做的是这个“.”页面上的一个不错的插页 “(”和Popout)美人鱼.js展示了像GitHub'这样的''s sorta *更好*“.”意指图多纳什 ' T 使用SCREENS, 但仍然在easuu读@.}

![mermaid_pan_zoom.png](mermaid_pan_zoom.png)

[TOC]

# 问题

美人鱼图有几种局限性

1. **固定规模** - 大图表不是被切断就是缩小以适应
2. **无交互活动** -Can'T 放大以查看细节或环绕棋盘
3. **无导出** -用户可以 @'}t 为外部用途保存图表
4. **流动经验差** - 小屏幕使得复杂的图表无法使用
5. **主题转换问题** @ -_ diggrams do'}在光和暗模式之间切换时总是正确重覆@ MS K2 render

# 解决方案

I've实施了一个全面强化系统, 增加了#:

- **交互式的 Pan/zoom** 使用 svg-pan-zoom 库
- **浮动控控按钮** 缩放@ / @ off_ MS K1} 重设 < ,} _ span togggle=,}和导出
- **全屏灯框** 更好的查看模式
- **PNG和SVG出口** 功能功能
- **Auto - 附加装入** 所以整个图表可以通过默认查看可见
- **完整-{width 显示** 消除人为尺寸限制

## 建筑结构概览

解决方案由三个主要部分组成:

```mermaid
graph TB
    A[mermaid_theme_switch.js] -->|Initializes| B[Mermaid Diagrams]
    B -->|Renders SVG| C[mermaid_enhancements.js]
    C -->|Adds| D[Control Buttons]
    C -->|Initializes| E[svg-pan-zoom]
    D -->|Triggers| F[Pan/Zoom Actions]
    D -->|Triggers| G[Export Functions]
    D -->|Triggers| H[Fullscreen Lightbox]
```

# 执行 执行情况

## 安装依赖

首次 ,安裝要求的 npm 套件@:

```bash
npm install svg-pan-zoom html-to-image
```

这些图书馆提供#:

- `svg-pan-zoom` 用于 SVG 元素的交互式平板和缩放功能
- `html-to-image` - 导出 SVG/PNG 功能

## 核心增强模块

主增强模块 @ (`mermaid_enhancements.js`@) 处理所有交互式功能@.

### 创建控制按钮

每个图表都有一个带有所有动作按钮的浮动控制面板@ : @ {}

```javascript
function createControlButtons(container, diagramId) {
    // Check if controls already exist
    if (container.querySelector('.mermaid-controls')) {
        return;
    }

    const controlsDiv = document.createElement('div');
    controlsDiv.className = 'mermaid-controls';

    const buttons = [
        { icon: 'bx-fullscreen', title: 'Fullscreen', action: 'fullscreen' },
        { icon: 'bx-zoom-in', title: 'Zoom In', action: 'zoomIn' },
        { icon: 'bx-zoom-out', title: 'Zoom Out', action: 'zoomOut' },
        { icon: 'bx-reset', title: 'Reset View', action: 'reset' },
        { icon: 'bx-move', title: 'Pan', action: 'pan' },
        { icon: 'bx-image', title: 'Export as PNG', action: 'exportPng' },
        { icon: 'bx-code-alt', title: 'Export as SVG', action: 'exportSvg' }
    ];

    buttons.forEach(btn => {
        const button = document.createElement('button');
        button.className = `mermaid-control-btn bx ${btn.icon}`;
        button.setAttribute('title', btn.title);
        button.setAttribute('aria-label', btn.title);
        button.setAttribute('data-action', btn.action);
        button.setAttribute('data-diagram-id', diagramId);
        controlsDiv.appendChild(button);
    });

    container.appendChild(controlsDiv);
}
```

### 正在初始化 Pan/ @% @ om

svg-pan-zoom 库提供顺畅的@ ,}功能性互动=:

```javascript
function initPanZoom(svgElement, diagramId) {
    // Clean up existing instance if present
    if (panZoomInstances.has(diagramId)) {
        try {
            panZoomInstances.get(diagramId).destroy();
        } catch (e) {
            console.warn('Failed to destroy existing pan-zoom instance:', e);
        }
        panZoomInstances.delete(diagramId);
    }

    try {
        const panZoomInstance = svgPanZoom(svgElement, {
            zoomEnabled: true,
            controlIconsEnabled: false, // We use custom controls
            fit: true,
            center: true,
            minZoom: 0.1,
            maxZoom: 10,
            zoomScaleSensitivity: 0.3,
            dblClickZoomEnabled: true,
            mouseWheelZoomEnabled: true,
            preventMouseEventsDefault: true,
            contain: false
        });

        panZoomInstances.set(diagramId, panZoomInstance);
        return panZoomInstance;
    } catch (error) {
        console.error('Failed to initialize pan-zoom:', error);
        return null;
    }
}
```

### 出口功能

出口系统保留图表质量,处理PNG和SVG格式“:”

```javascript
async function exportDiagram(container, format, diagramId) {
    try {
        const svgElement = container.querySelector('svg');
        if (!svgElement) {
            window.showToast && window.showToast('No diagram found to export', 3000, 'error');
            return;
        }

        // Clone the SVG to avoid modifying the original
        const clonedSvg = svgElement.cloneNode(true);

        // Get the viewBox or calculate from bounding box
        let viewBox = clonedSvg.getAttribute('viewBox');
        if (!viewBox) {
            const bbox = svgElement.getBBox();
            viewBox = `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`;
            clonedSvg.setAttribute('viewBox', viewBox);
        }

        // Parse viewBox to get dimensions
        const [, , vbWidth, vbHeight] = viewBox.split(' ').map(Number);

        // Set explicit dimensions based on viewBox for proper export
        clonedSvg.setAttribute('width', vbWidth);
        clonedSvg.setAttribute('height', vbHeight);

        // Remove inline styles but keep viewBox
        clonedSvg.removeAttribute('style');
        clonedSvg.style.backgroundColor = 'transparent';
        clonedSvg.style.maxWidth = 'none';

        // Create temporary container
        const tempDiv = document.createElement('div');
        tempDiv.style.position = 'absolute';
        tempDiv.style.left = '-9999px';
        tempDiv.appendChild(clonedSvg);
        document.body.appendChild(tempDiv);

        let dataUrl;
        const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
        const filename = `mermaid-diagram-${timestamp}`;

        if (format === 'png') {
            dataUrl = await toPng(clonedSvg, {
                backgroundColor: 'white',
                pixelRatio: 2 // Higher quality
            });
            downloadFile(dataUrl, `${filename}.png`);
        } else {
            dataUrl = await toSvg(clonedSvg, {
                backgroundColor: 'transparent'
            });
            downloadFile(dataUrl, `${filename}.svg`);
        }

        // Clean up
        document.body.removeChild(tempDiv);

        window.showToast && window.showToast(`Diagram exported as ${format.toUpperCase()}`, 3000, 'success');
    } catch (error) {
        console.error('Failed to export diagram:', error);
        window.showToast && window.showToast('Failed to export diagram', 3000, 'error');
    }
}
```

**关键出口考虑因素 @:**

1. **保存保护视图框** @- 关键在于捕捉整个图表@,}不只是可见部分
2. **计算大小** \ - 明确设置宽度@ MS K1 @ h8, 从视图框中显示, 以一致导出
3. **删除变换** - 换成 pan-zoom 变换, 因此导出显示完整的图表
4. **背景处理** PNGQ,的白色背景SVG透明
5. **高分辨率** - 使用 `pixelRatio: 2` 巴布亚新几内亚出口

### 全屏灯框

光箱提供沉浸式浏览体验@: @

```javascript
function openFullscreenLightbox(container, diagramId) {
    const svgElement = container.querySelector('svg');
    if (!svgElement) return;

    // Create lightbox overlay
    const lightbox = document.createElement('div');
    lightbox.className = 'mermaid-lightbox';
    lightbox.innerHTML = `
        <div class="mermaid-lightbox-content">
            <button class="mermaid-lightbox-close bx bx-x" aria-label="Close"></button>
            <div class="mermaid-lightbox-diagram-wrapper">
                <div class="mermaid-lightbox-diagram"></div>
            </div>
        </div>
    `;

    // Clone and prepare SVG
    const clonedSvg = svgElement.cloneNode(true);
    clonedSvg.removeAttribute('width');
    clonedSvg.removeAttribute('height');
    clonedSvg.style.width = '100%';
    clonedSvg.style.height = '100%';

    const diagramContainer = lightbox.querySelector('.mermaid-lightbox-diagram');
    diagramContainer.appendChild(clonedSvg);

    // Add controls to lightbox
    const wrapper = lightbox.querySelector('.mermaid-lightbox-diagram-wrapper');
    const lightboxDiagramId = `${diagramId}-lightbox`;
    createControlButtons(wrapper, lightboxDiagramId);

    document.body.appendChild(lightbox);

    // Initialize pan-zoom after layout completes
    setTimeout(() => {
        const panZoom = initPanZoom(clonedSvg, lightboxDiagramId);
        if (panZoom) {
            panZoom.resize();
            panZoom.fit();
            panZoom.center();
        }
    }, 100);

    // Close handlers
    const closeLightbox = () => {
        if (panZoomInstances.has(lightboxDiagramId)) {
            try {
                panZoomInstances.get(lightboxDiagramId).destroy();
            } catch (e) {
                console.warn('Failed to destroy lightbox pan-zoom:', e);
            }
            panZoomInstances.delete(lightboxDiagramId);
        }
        lightbox.remove();
    };

    lightbox.querySelector('.mermaid-lightbox-close').addEventListener('click', closeLightbox);
    lightbox.addEventListener('click', (e) => {
        if (e.target === lightbox) closeLightbox();
    });

    // ESC key to close
    const escHandler = (e) => {
        if (e.key === 'Escape') {
            closeLightbox();
            document.removeEventListener('keydown', escHandler);
        }
    };
    document.addEventListener('keydown', escHandler);
}
```

### 主增强功能

这把一切连接在一起, 被称作美人鱼完成:

```javascript
export function enhanceMermaidDiagrams() {
    const diagrams = document.querySelectorAll('.mermaid[data-processed="true"]');

    diagrams.forEach(diagram => {
        const svgElement = diagram.querySelector('svg');
        if (!svgElement) return;

        // CRITICAL: Remove inline max-width constraint that Mermaid adds
        svgElement.style.maxWidth = 'none';

        // Wrap diagram with controls
        const diagramId = wrapDiagramWithControls(diagram);

        // Initialize pan/zoom and auto-fit
        const panZoom = initPanZoom(svgElement, diagramId);
        if (panZoom) {
            // Fit diagram to container by default
            setTimeout(() => {
                panZoom.resize();
                panZoom.fit();
                panZoom.center();
            }, 100);
        }
    });

    // Set up event delegation for control buttons (only once)
    if (!document.body.hasAttribute('data-mermaid-controls-initialized')) {
        document.body.addEventListener('click', handleControlClick);
        document.body.setAttribute('data-mermaid-controls-initialized', 'true');
    }
}
```

**关键修正@: @%** 美人鱼适用内线 `style="max-width: 1020px"` 到 SVG 元素@,, 防止全@ -_width 显示 @. 移除此功能对于正确反应行为来说是不可或缺的 @ MS K3

## 主题整合

主题切换器确保了在光和暗模式之间切换时的图表重新 - render 正确\ MPK1\ {}

```javascript
import { enhanceMermaidDiagrams } from './mermaid_enhancements';

const loadMermaid = async (theme) => {
    if (!window.mermaid) return;
    try {
        window.mermaid.initialize({
            startOnLoad: false,
            theme,
            themeVariables: {
                background: 'transparent'
            }
        });
        await window.mermaid.run({
            querySelector: elementSelector,
        });

        // Enhance diagrams after rendering completes
        // Use requestAnimationFrame for better timing
        await new Promise(resolve => {
            requestAnimationFrame(() => {
                requestAnimationFrame(() => {
                    enhanceMermaidDiagrams();
                    resolve();
                });
            });
        });
    } catch (err) {
        console.error('Mermaid render error:', err);
    }
};
```

使用 `requestAnimationFrame` 两次确保浏览器完成 SVG 的绘画后, 我们才尝试加强它@. @ @

## Cloudflare 火箭装载器兼容性

Cloudflare'火箭装载器可延缓 JavaScript 执行@,破碎初始化 @. 这里='防弹解决方案_:

```javascript
// Wait for all dependencies to load with exponential backoff
function waitForDependencies(maxAttempts = 50) {
    return new Promise((resolve) => {
        let attempts = 0;

        const checkDependencies = () => {
            attempts++;

            const depsReady =
                typeof window.hljs !== 'undefined' &&
                typeof window.mermaid !== 'undefined' &&
                typeof window.Alpine !== 'undefined' &&
                typeof window.htmx !== 'undefined';

            if (depsReady) {
                console.log('All dependencies loaded after', attempts, 'attempts');

                // Start Alpine.js now that it's loaded
                if (window.Alpine && !window.Alpine.version) {
                    try {
                        window.Alpine.start();
                        console.log('Alpine.js started');
                    } catch (err) {
                        console.error('Failed to start Alpine:', err);
                    }
                }

                resolve();
            } else if (attempts >= maxAttempts) {
                console.warn('Timeout waiting for dependencies');
                resolve(); // Continue anyway
            } else {
                // Retry with exponential backoff
                const delay = Math.min(50 * Math.pow(1.2, attempts), 500);
                setTimeout(checkDependencies, delay);
            }
        };

        checkDependencies();
    });
}

// Robust initialization
async function safeInitialize() {
    try {
        await waitForDependencies();

        if (document.readyState === 'loading') {
            await new Promise(resolve => {
                document.addEventListener('DOMContentLoaded', resolve, { once: true });
            });
        }

        await initializePage();
    } catch (err) {
        console.error('Failed to initialize page:', err);
        // Retry once after delay
        setTimeout(() => {
            initializePage().catch(e => console.error('Retry failed:', e));
        }, 1000);
    }
}

safeInitialize();
```

也确保你的主要脚本有 `data-cfasync="false"` 将它从“火箭加载器”中排除的属性@:

```html
<script src="~/js/dist/main.js" type="module" asp-append-version="true" data-cfasync="false"></script>
```

## 与尾风/#DaisyUI的风格

CSS 使用尾风公用事业类和自定义的抛光样式

```css
/* Mermaid diagram wrapper */
.mermaid-wrapper {
    @apply relative rounded-lg overflow-hidden w-full;
    margin: 1rem 0;
}

.mermaid-wrapper .mermaid {
    @apply m-0 w-full;
    min-height: 500px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
}

.mermaid-wrapper .mermaid svg {
    width: 100% !important;
    height: auto !important;
    min-height: 450px;
}

/* Control buttons */
.mermaid-controls {
    @apply absolute top-2 right-2 flex gap-1 z-10;
    background: rgba(255, 255, 255, 0.9);
    border-radius: 0.5rem;
    padding: 0.25rem;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.dark .mermaid-controls {
    background: rgba(31, 41, 55, 0.95);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}

.mermaid-control-btn {
    @apply p-2 rounded cursor-pointer transition-all duration-200;
    background: transparent;
    border: none;
    color: #4b5563;
    font-size: 1.25rem;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
}

.mermaid-control-btn:hover {
    background: rgba(37, 99, 235, 0.1);
    color: #2563eb;
    transform: scale(1.1);
}

.dark .mermaid-control-btn {
    color: #9ca3af;
}

.dark .mermaid-control-btn:hover {
    background: rgba(55, 65, 81, 0.8);
    color: #60a5fa;
}

/* Lightbox */
.mermaid-lightbox {
    @apply fixed inset-0 z-50 flex items-center justify-center;
    background: rgba(0, 0, 0, 0.85);
    backdrop-filter: blur(4px);
    animation: fadeIn 0.2s ease-out;
}

.dark .mermaid-lightbox {
    background: rgba(0, 0, 0, 0.95);
}

.mermaid-lightbox-content {
    @apply relative w-11/12 h-5/6 bg-white rounded-lg shadow-2xl;
    max-width: 1400px;
}

.dark .mermaid-lightbox-content {
    @apply bg-gray-800;
}

.mermaid-lightbox-close {
    @apply absolute top-4 right-4 z-10 p-2 rounded-full cursor-pointer transition-all;
    background: rgba(0, 0, 0, 0.5);
    border: none;
    color: white;
    font-size: 2rem;
    width: 3rem;
    height: 3rem;
    display: flex;
    align-items: center;
    justify-content: center;
}

.mermaid-lightbox-close:hover {
    background: rgba(220, 38, 38, 0.8);
    transform: scale(1.1);
}

@keyframes fadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}
```

# 测试和调试

这里的' 如何校验一切工作?

## 控制台输出

使用正确的初始化@ , 您应该看到 @ MS K1

```
All dependencies loaded after 1 attempts
Alpine.js started
Highlight.js copy plugin registered
Highlight.js initialized on page load
Mermaid initialized on page load
Document is ready - all initializations complete
HTMX event listener registered successfully
```

## HTMX 动态内容

HTMX 互换后 @ , 您应该看到@ MS K1

```
HTMX afterSettle triggered for: contentcontainer
Highlight.js applied after HTMX swap
Mermaid initialized
Mermaid applied after HTMX swap
HTMX afterSettle complete for: contentcontainer
```

## 测试核对清单

- [ ] 初始页面负载时提供的图表
- [ ]Pan/Zzoom控制工作
- [ @ ]_ 全屏灯光箱打开并关闭@ MS K1}X button *,} 点击外面 @,} ESC 键=)}
- [ ] PNG 导出捕捉完整图表 @(}不只是角落@)
- [ ]SVG出口保留区矢量
- [ ]在 HTMX 内容互换后起作用
- [ ] 主题切换 MS K1 @ renders 图正确
- [ @移动响应@(}控制器保持可见 *,* 图表缩放 *)*
- [ ] 暗色模式的外观使用正确
- [ ] 键盘可访问性 @(}%Tab to control {,} 输入以激活@)}

# 业绩考量

1. **懒惰初始化** - 仅增强页面上的图表
2. **清理事件** - 销毁图表被删除时的 pan-çzoom 实例
3. **活动代表团** - 单点击收听器处理所有控制按钮
4. **动画** 与任意设定时间值相比,时机更合适
5. **下降的出口出口** @ - @ 防止快速@ MS K1 @ fire 导出点击

# 浏览器兼容性

测试和研究“:”

- 希罗梅/ 爱奇 90+
- 消防员 @88+#
- 萨法里
- 移动浏览器 @(iOSSafari_,Chrome Moved)

**不支持 I11\ {}** 由于现代 JavaScript 特性, @_const,}箭头函数 @ ,_ async=/}#await@,}请求動畫Frame @MS K5}

# 结论结论

此全面增强将静态美人鱼图表转换为互动@,导出可视化 @.。 执行是制作_-{ready},适应边缘情况}{,},并提供了极好的用户体验}.}

密钥取走@: @%

- 删除内嵌的美人鱼@ ' @%s `max-width` 用于完整@- @width 图的制约
- 导出以捕捉整个图表时保存视图框
- 选择时间,而不是任意延误
- 配有依赖性检查和重试逻辑的Landle Cloudflare 火箭装载器
- 为光箱 @ ( @ button@ MS K1} 提供多重近距离方法, 请在外点击 @ I, ESC @ MPK3}
- 使用包含许多图表的性能授权活动

## 使用 npm 软件包

您现在可以将此功能安装为 npm 软件包@ :}

```bash
npm install @mostlylucid/mermaid-enhancements
```

```typescript
import { init } from '@mostlylucid/mermaid-enhancements';
import '@mostlylucid/mermaid-enhancements/styles.css';

await init();
```

见见 [作为 npm 软件包的美人鱼增强](/blog/publishingmermaidenhancementsnpm) 和高级配置选项@.

完整的源代码也可以在这个博客“ '” 库中找到。 `Mostlylucid/src/js/mermaid_enhancements.js` 并用作打开@-}源 npm 软件包 [美人鱼](https://github.com/scottgal/mostlylucidweb/tree/main/mostlylucid-mermaid).

## 示例图示

这里的'是一个复杂的例子, 显示了这个博客的架构@'}内容系统 @:

```mermaid
graph TB
    subgraph Client["Client Browser"]
        A[User Request] -->|HTMX| B[Blog Controller]
        B -->|Cache Miss| C[Blog Service]
        C -->|File Mode| D[Markdown Service]
        C -->|DB Mode| E[EF Core Context]
        D -->|Parse| F[Markdig Pipeline]
        F -->|Render| G[HTML + Mermaid]
        E -->|Query| H[PostgreSQL]
        H -->|Full-Text Search| I[GIN Index]
        G -->|Enhance| J[mermaid_enhancements.js]
        J -->|Initialize| K[svg-pan-zoom]
        J -->|Add| L[Control Buttons]
        L -->|Export| M[html-to-image]
    end

    subgraph Background["Background Services"]
        N[File Watcher] -->|Change Detected| O[Saves to DB]
        O -->|Trigger| P[Translation Service]
        P -->|Batch| Q[EasyNMT API]
        Q -->|12 Languages| R[Translated Files]
    end 

    style A stroke:#22c55e,stroke-width:3px,color:#4ade80
    style G stroke:#3b82f6,stroke-width:3px,color:#60a5fa
    style J stroke:#f59e0b,stroke-width:3px,color:#f59e0b
    style K stroke:#ec4899,stroke-width:3px,color:#ec4899
    style M stroke:#8b5cf6,stroke-width:3px,color:#8b5cf6
```

尝试单击上方图表的控件@! @%