Featured image of post 【Hugo】引入mermaid

【Hugo】引入mermaid

给 Hugo 引入 mermaid

|
2095 字
|

介绍Mermaid并分享优化配置的博客内容:


Mermaid 流程图优化配置指南

Mermaid 是一个强大的流程图绘制工具,支持多种图表类型,包括流程图、类图、状态图等。但在实际使用中,官方默认配置可能存在一些问题,比如节点文字过长导致显示不整齐、图表放大后文字模糊等。

经过反复研究和优化,我整理出一套 Mermaid 的优化解决方案,完美解决这些痛点问题。以下是主要的优化功能:

  1. 自动调整节点宽度:根据文字内容自动调整节点宽度,避免文字截断
  2. 文字自动换行:支持长文字自动换行显示
  3. 字体大小适配:不同的场景自动调整字体大小
  4. 流程图缩放:支持鼠标滚轮缩放,随时切换显示比例
  5. 偏移滚动:放大后支持拖拽查看图表任意区域
  6. 拖拽放大:点击进入放大模式,双击或 ESC 退出
  7. 移动端优化:自动适配手机和平板等移动设备

功能展示

flowchart TD A["程序启动"] A --> B["主线程"] A --> C["刷新线程 (refresh_video)"]
  1. 节点自动调整
    节点宽度根据内容自动调整,避免文字溢出或过于 crowed

  2. 文字换行支持
    长文字自动换行,保持内容清晰易读

  3. 平滑缩放
    支持滚轮缩放(Ctrl + 滚轮),放大后仍然保持清晰

  4. 拖拽查看
    放大后可以拖拽查看任意区域,操作流畅

  5. 快捷操作

    • 单击:进入放大模式
    • 滚轮:缩放图表
    • 拖拽:平移视角
    • 双击/ESC:退出放大模式

配置指南

我使用的是 stack 主题,不同主题文件结构可能不同

  1. 创建 layouts/_default/_markup/render-codeblock-mermaid.html
1
2
3
4
5
<div class="mermaid-container">
  <div class="mermaid">
    {{ .Inner | safeHTML }}
  </div>
</div>
  1. 修改 layouts/_default/baseof.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode }}" dir="{{ default `ltr` .Language.LanguageDirection }}">
    <head>
        {{- partial "head/head.html" . -}}
        {{- block "head" . -}}{{ end }}
    </head>
    <body class="{{ block `body-class` . }}{{ end }}">
        {{- partial "head/colorScheme" . -}}

        {{/* The container is wider when there's any activated widget */}}
        {{- $hasWidget := false -}}
        {{- range .Site.Params.widgets -}}
            {{- if gt (len .) 0 -}}
                {{- $hasWidget = true -}}
            {{- end -}}
        {{- end -}}
        <div class="container main-container flex on-phone--column {{ if $hasWidget }}extended{{ else }}compact{{ end }}">
            {{- block "left-sidebar" . -}}
                {{ partial "sidebar/left.html" . }}
            {{- end -}}
            {{- block "right-sidebar" . -}}{{ end }}
            <main class="main full-width">
                {{- block "main" . }}{{- end }}
            </main>
        </div>
        {{ partial "footer/include.html" . }}
        {{ partial "article/components/mermaid.html" . }}  <!-- 新增这一行 -->
    </body>
</html>

具体的路径主要看你放在哪,我就放在文章文件夹下了
3. 创建 layouts/partials/article/components/mermaid.html,:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
{{- if .Page.Params.mermaid -}}
  <link rel="stylesheet" href="{{ "css/mermaid.css" | relURL }}">
  <script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>

  <script>
    mermaid.initialize({
      startOnLoad: true,
      flowchart: {
        htmlLabels: false,
        useMaxWidth: false,
        nodeSpacing: 50,
        rankSpacing: 50,
        defaultRenderer: 'svg'
      },
      theme: 'default',
      securityLevel: 'loose',
      themeVariables: {
        nodeBorder: '#000',
        mainBkg: '#fff',
        nodeTextColor: '#000',
        fontSize: '14px'
      }
    });
  </script>

  <!-- 遮罩层 -->
  <div class="mermaid-overlay" id="mermaidOverlay"></div>

  <script>
    document.addEventListener('DOMContentLoaded', () => {
      // 自动调整节点尺寸
      const adjustNodeSize = () => {
        document.querySelectorAll('.mermaid .node').forEach(node => {
          const rect = node.querySelector('rect');
          const text = node.querySelector('text');
          if (rect && text) {
            const bbox = text.getBBox();
            const padding = 16;
            const minWidth = parseFloat(rect.getAttribute('width')) || 160;
            const newWidth = Math.max(bbox.width + padding, minWidth);
            rect.setAttribute('width', newWidth);
          }
        });
      };

      // 初始化调整
      adjustNodeSize();
      window.addEventListener('resize', adjustNodeSize);

      // 为每个mermaid图表创建独立控制器
      document.querySelectorAll('.mermaid-container').forEach(container => {
        let currentScale = 1;
        let initialClickX = null, initialClickY = null;
        let dragOffsetX = 0, dragOffsetY = 0;
        let isDragging = false;
        let dragStartX = 0, dragStartY = 0;
        let isZoomed = false;

        const mermaidElement = container.querySelector('.mermaid');
        const overlay = document.getElementById('mermaidOverlay');
        const originalParent = container.parentElement;
        const originalNextSibling = container.nextElementSibling;

        // 应用变换
        const applyScale = () => {
          if (isZoomed && initialClickX !== null && initialClickY !== null) {
            const containerRect = container.getBoundingClientRect();
            const centerX = containerRect.width / 2;
            const centerY = containerRect.height / 2;
            const baseTranslateX = centerX - initialClickX * currentScale;
            const baseTranslateY = centerY - initialClickY * currentScale;
            const translateX = baseTranslateX + dragOffsetX;
            const translateY = baseTranslateY + dragOffsetY;
            mermaidElement.style.transform = `translate(${translateX}px, ${translateY}px) scale(${currentScale})`;
          } else {
            mermaidElement.style.transform = `scale(${currentScale})`;
          }
        };

        // 进入放大模式
        const handleContainerClick = (e) => {
          if (!isZoomed) {
            isZoomed = true;
            container.classList.add('zoomed');
            overlay.style.display = 'flex';
            overlay.appendChild(container);

            const containerRect = container.getBoundingClientRect();
            initialClickX = e.clientX - containerRect.left;
            initialClickY = e.clientY - containerRect.top;
            dragOffsetX = 0;
            dragOffsetY = 0;

            const scaleX = window.innerWidth / containerRect.width;
            const scaleY = window.innerHeight / containerRect.height;
            currentScale = Math.min(scaleX, scaleY, 5);
            applyScale();
          }
        };

        // 退出放大模式
        const exitZoom = () => {
          if (isZoomed) {
            isZoomed = false;
            container.classList.remove('zoomed');
            overlay.style.display = 'none';
            if (originalNextSibling) {
              originalParent.insertBefore(container, originalNextSibling);
            } else {
              originalParent.appendChild(container);
            }
            currentScale = 1;
            initialClickX = null;
            initialClickY = null;
            dragOffsetX = 0;
            dragOffsetY = 0;
            applyScale();
          }
        };

        // 滚轮事件处理
        const handleWheel = (e) => {
          if (isZoomed) {
            e.preventDefault();
            const sensitivity = 0.0006;
            const delta = e.deltaY || e.wheelDelta;
            const scaleDelta = 1 + (-delta * sensitivity);
            currentScale = Math.min(Math.max(0.3, currentScale * scaleDelta), 5);
            applyScale();
          } else if (e.ctrlKey) {
            e.preventDefault();
            const sensitivity = 0.0006;
            const delta = e.deltaY || e.wheelDelta;
            const scaleDelta = 1 + (-delta * sensitivity);
            currentScale = Math.min(Math.max(0.3, currentScale * scaleDelta), 5);
            applyScale();
          }
        };

        // 事件绑定
        container.addEventListener('click', (e) => {
          if (!isZoomed) handleContainerClick(e);
        });

        container.addEventListener('dblclick', exitZoom);
        container.addEventListener('wheel', handleWheel);

        // 拖拽处理
        container.addEventListener('mousedown', (e) => {
          if (isZoomed) {
            isDragging = true;
            dragStartX = e.clientX;
            dragStartY = e.clientY;
            container.style.cursor = 'grabbing';
          }
        });

        const handleMouseMove = (e) => {
          if (isDragging) {
            const deltaX = e.clientX - dragStartX;
            const deltaY = e.clientY - dragStartY;
            dragOffsetX += deltaX;
            dragOffsetY += deltaY;
            dragStartX = e.clientX;
            dragStartY = e.clientY;
            applyScale();
          }
        };

        const handleMouseUp = () => {
          isDragging = false;
          container.style.cursor = 'grab';
        };

        document.addEventListener('mousemove', handleMouseMove);
        document.addEventListener('mouseup', handleMouseUp);
      });

      // 全局ESC按键监听
      document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape') {
          document.querySelectorAll('.mermaid-container').forEach(container => {
            if (container.classList.contains('zoomed')) {
              container.dispatchEvent(new Event('dblclick'));
            }
          });
        }
      });
    });
  </script>
{{- end -}}
  1. 修改 assets/scss/style.scss
1
@import "partials/mermaid.scss";

具体还是看你把 css 文件放在哪
5. 创建 assets/scss/partials/mermaid.scss

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
/* 核心容器 */
.mermaid-container {
  position: relative;
  margin: 2rem 0;
  border-radius: 12px;
  overflow: auto;
  padding: 1.5rem;
  transition: transform 0.3s cubic-bezier(0.22, 0.61, 0.36, 1);
  user-select: none;
}

/* Mermaid 图表样式 */
.mermaid {
  transition: transform 0.25s cubic-bezier(0.22, 0.61, 0.36, 1);
  will-change: transform;
  backface-visibility: hidden;
  transform-origin: top left;
  text-align: center;
  line-height: 1.5;
}

/* 节点样式增强 */
.mermaid .node rect {
  rx: 8px;
  ry: 8px;
  filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.1));
  min-width: 160px !important; /* 强制最小宽度 */
  min-height: 60px !important; /* 强制最小高度 */
}

.mermaid .node text {
  pointer-events: none;
  alignment-baseline: middle;
  text-anchor: middle;
  white-space: pre-wrap !important; /* 允许换行 */
  word-wrap: break-word !important;
  overflow: visible !important;
  font-size: 14px !important;
  max-width: 150px !important; /* 限制文字最大宽度 */
  padding: 8px !important; /* 添加内边距 */
}

/* 边标签样式 */
.mermaid .edgeLabels foreignObject {
  background: var(--card, #fff);
  border-radius: 4px;
  padding: 2px 8px;
  display: block;
  text-align: center;
  min-width: 80px !important;
  white-space: pre-wrap !important;
  word-wrap: break-word !important;
}

/* 移动端优化 */
@media (max-width: 768px) {
  .mermaid-container {
    margin: 1rem -1rem;
    border-radius: 0;
  }
  .mermaid .node rect {
    min-width: 120px !important;
    min-height: 40px !important;
  }
  .mermaid .node text {
    font-size: 12px !important;
    max-width: 100px !important;
  }
}

/* 遮罩层 */
.mermaid-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(69, 71, 71, 0.5);
  backdrop-filter: blur(4px);
  z-index: 1000;
  display: none;
  justify-content: center;
  align-items: center;
  cursor: zoom-out;
}

/* 放大状态下的容器 */
.mermaid-container.zoomed {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100vw;
  height: 100vh;
  margin: 0;
  padding: 1.5rem;
  transform: translate(-50%, -50%);
  cursor: grab;
  transition: transform 0.3s cubic-bezier(0.22, 0.61, 0.36, 1);
  z-index: 1001;
}

.mermaid-container.zoomed:active {
  cursor: grabbing;
}

.mermaid-container.zoomed .mermaid {
  transform-origin: center center;
  cursor: default;
  overflow: visible;
}
使用 Hugo 构建
主题 StackJimmy 设计