From 9f31bbe37208f8658d896ab165d4e0393a55b176 Mon Sep 17 00:00:00 2001
From: nanhaoluo <3075912108@qq.com>
Date: Sun, 25 Jan 2026 01:59:27 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=20Mermaid=20?=
=?UTF-8?q?=E5=AF=BC=E5=87=BA=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 添加导出按钮到工具栏,支持 PNG 和 SVG 格式导出
- 实现导出菜单,点击导出按钮显示格式选项
- PNG 导出:将 SVG 转换为 PNG 图片并下载
- SVG 导出:保存 SVG 代码为文件并下载
- 导出时保持图表当前的缩放级别和样式
- 添加导出错误处理,显示友好的错误提示
- 导出菜单支持点击外部关闭
- 添加导出菜单样式,支持夜间模式
- 移动端导出菜单适配,调整按钮大小和位置
- 错误提示自动消失(3秒后)
需求:15.1, 15.2, 15.3, 15.4, 15.5
---
argontheme.js | 198 ++++++++++++++++++++++++++++++++++++++++++++++++++
style.css | 109 +++++++++++++++++++++++++++
2 files changed, 307 insertions(+)
diff --git a/argontheme.js b/argontheme.js
index 4dca0d7..fa486d1 100644
--- a/argontheme.js
+++ b/argontheme.js
@@ -5688,6 +5688,7 @@ void 0;
// 创建缩放控制
// 需求 14.1: 添加全屏按钮
+ // 需求 15.1: 添加导出按钮和菜单
const controls = document.createElement('div');
controls.className = 'mermaid-zoom-controls';
controls.innerHTML = `
@@ -5696,10 +5697,20 @@ void 0;
+
`;
container.appendChild(controls);
+ // 需求 15.1: 创建导出菜单
+ const exportMenu = document.createElement('div');
+ exportMenu.className = 'mermaid-export-menu';
+ exportMenu.innerHTML = `
+
+
+ `;
+ container.appendChild(exportMenu);
+
// 创建提示文本
const hint = document.createElement('div');
hint.className = 'mermaid-hint';
@@ -5762,6 +5773,7 @@ void 0;
// 绑定按钮事件
// 需求 14.1, 14.2, 14.3: 全屏按钮功能
+ // 需求 15.1: 导出按钮功能
controls.addEventListener('click', (e) => {
const btn = e.target.closest('.mermaid-zoom-btn');
if (!btn || btn.disabled) return; // 忽略禁用的按钮
@@ -5780,9 +5792,195 @@ void 0;
} else if (action === 'fullscreen') {
// 需求 14.1: 点击全屏按钮
toggleFullscreen();
+ } else if (action === 'export') {
+ // 需求 15.1: 点击导出按钮,显示/隐藏导出菜单
+ toggleExportMenu();
}
});
+ // ==========================================================================
+ // 需求 15: Mermaid 导出功能
+ // ==========================================================================
+
+ /**
+ * 切换导出菜单显示/隐藏
+ * 需求 15.1: 点击导出按钮显示导出选项
+ */
+ const toggleExportMenu = () => {
+ const isVisible = exportMenu.classList.contains('visible');
+ if (isVisible) {
+ exportMenu.classList.remove('visible');
+ } else {
+ exportMenu.classList.add('visible');
+ }
+ };
+
+ // 点击导出选项
+ exportMenu.addEventListener('click', (e) => {
+ const option = e.target.closest('.mermaid-export-option');
+ if (!option) return;
+
+ const format = option.dataset.format;
+ exportMenu.classList.remove('visible');
+
+ if (format === 'png') {
+ exportAsPNG();
+ } else if (format === 'svg') {
+ exportAsSVG();
+ }
+ });
+
+ // 点击容器外部关闭导出菜单
+ document.addEventListener('click', (e) => {
+ if (!container.contains(e.target)) {
+ exportMenu.classList.remove('visible');
+ }
+ });
+
+ /**
+ * 导出为 PNG
+ * 需求 15.2: 将图表转换为 PNG 图片并下载
+ * 需求 15.4: 保持图表当前的缩放级别和样式
+ */
+ const exportAsPNG = async () => {
+ try {
+ const svgElement = inner.querySelector('svg');
+ if (!svgElement) {
+ throw new Error('未找到 SVG 元素');
+ }
+
+ // 克隆 SVG 以避免修改原始元素
+ const clonedSvg = svgElement.cloneNode(true);
+
+ // 需求 15.4: 应用当前缩放级别
+ const svgWidth = svgElement.getBoundingClientRect().width * scale;
+ const svgHeight = svgElement.getBoundingClientRect().height * scale;
+
+ clonedSvg.setAttribute('width', svgWidth);
+ clonedSvg.setAttribute('height', svgHeight);
+
+ // 将 SVG 转换为 data URL
+ const svgData = new XMLSerializer().serializeToString(clonedSvg);
+ const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
+ const svgUrl = URL.createObjectURL(svgBlob);
+
+ // 创建图片元素
+ const img = new Image();
+ img.onload = () => {
+ // 创建 canvas
+ const canvas = document.createElement('canvas');
+ canvas.width = svgWidth;
+ canvas.height = svgHeight;
+
+ const ctx = canvas.getContext('2d');
+
+ // 设置白色背景(PNG 默认透明)
+ ctx.fillStyle = '#ffffff';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ // 绘制图片
+ ctx.drawImage(img, 0, 0, svgWidth, svgHeight);
+
+ // 转换为 PNG 并下载
+ canvas.toBlob((blob) => {
+ if (blob) {
+ downloadFile(blob, 'mermaid-chart.png');
+ }
+ URL.revokeObjectURL(svgUrl);
+ }, 'image/png');
+ };
+
+ // 需求 15.5: 添加导出错误处理
+ img.onerror = () => {
+ URL.revokeObjectURL(svgUrl);
+ throw new Error('图片加载失败');
+ };
+
+ img.src = svgUrl;
+
+ } catch (error) {
+ // 需求 15.5: 显示友好的错误提示
+ handleExportError('PNG', error);
+ }
+ };
+
+ /**
+ * 导出为 SVG
+ * 需求 15.3: 将 SVG 代码保存为文件并下载
+ * 需求 15.4: 保持图表当前的缩放级别和样式
+ */
+ const exportAsSVG = () => {
+ try {
+ const svgElement = inner.querySelector('svg');
+ if (!svgElement) {
+ throw new Error('未找到 SVG 元素');
+ }
+
+ // 克隆 SVG
+ const clonedSvg = svgElement.cloneNode(true);
+
+ // 需求 15.4: 应用当前缩放级别
+ if (scale !== 1) {
+ const svgWidth = svgElement.getBoundingClientRect().width * scale;
+ const svgHeight = svgElement.getBoundingClientRect().height * scale;
+
+ clonedSvg.setAttribute('width', svgWidth);
+ clonedSvg.setAttribute('height', svgHeight);
+ }
+
+ // 添加 XML 声明和命名空间
+ const svgData = new XMLSerializer().serializeToString(clonedSvg);
+ const svgWithDeclaration = '\n' + svgData;
+
+ // 创建 Blob 并下载
+ const blob = new Blob([svgWithDeclaration], { type: 'image/svg+xml;charset=utf-8' });
+ downloadFile(blob, 'mermaid-chart.svg');
+
+ } catch (error) {
+ // 需求 15.5: 显示友好的错误提示
+ handleExportError('SVG', error);
+ }
+ };
+
+ /**
+ * 下载文件
+ * @param {Blob} blob - 文件内容
+ * @param {string} filename - 文件名
+ */
+ const downloadFile = (blob, filename) => {
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ };
+
+ /**
+ * 处理导出错误
+ * 需求 15.5: 显示友好的错误提示
+ * @param {string} format - 导出格式
+ * @param {Error} error - 错误对象
+ */
+ const handleExportError = (format, error) => {
+ console.error(`导出 ${format} 失败:`, error);
+
+ // 显示错误提示
+ const errorMsg = document.createElement('div');
+ errorMsg.className = 'mermaid-export-error';
+ errorMsg.textContent = `导出 ${format} 失败: ${error.message}`;
+ container.appendChild(errorMsg);
+
+ // 3秒后自动移除错误提示
+ setTimeout(() => {
+ if (errorMsg.parentNode) {
+ errorMsg.parentNode.removeChild(errorMsg);
+ }
+ }, 3000);
+ };
+
// 鼠标滚轮缩放(按住 Ctrl)
// 需求 12.1: 以鼠标为中心进行缩放
container.addEventListener('wheel', (e) => {
diff --git a/style.css b/style.css
index 87401db..13612dc 100644
--- a/style.css
+++ b/style.css
@@ -1176,6 +1176,103 @@ html.darkmode .mermaid-fullscreen .mermaid-zoom-controls {
color: var(--themecolor);
}
+/* 需求 15: Mermaid 导出功能样式 */
+/* 导出按钮 */
+.mermaid-export-btn {
+ position: relative;
+}
+
+/* 导出菜单 */
+.mermaid-export-menu {
+ position: absolute;
+ top: 45px;
+ right: 10px;
+ background: white;
+ border-radius: 6px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ padding: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ opacity: 0;
+ visibility: hidden;
+ transform: translateY(-10px);
+ transition: all 0.2s ease;
+ z-index: 11;
+ min-width: 140px;
+}
+
+.mermaid-export-menu.visible {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+}
+
+html.darkmode .mermaid-export-menu {
+ background: #2a2a2a;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
+}
+
+/* 导出选项按钮 */
+.mermaid-export-option {
+ padding: 8px 12px;
+ border: none;
+ background: transparent;
+ color: #333;
+ text-align: left;
+ cursor: pointer;
+ border-radius: 4px;
+ font-size: 14px;
+ transition: background 0.2s;
+ white-space: nowrap;
+}
+
+.mermaid-export-option:hover {
+ background: rgba(94, 114, 228, 0.1);
+ color: var(--themecolor);
+}
+
+html.darkmode .mermaid-export-option {
+ color: #ddd;
+}
+
+html.darkmode .mermaid-export-option:hover {
+ background: rgba(94, 114, 228, 0.2);
+}
+
+/* 需求 15.5: 导出错误提示 */
+.mermaid-export-error {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background: rgba(220, 53, 69, 0.95);
+ color: white;
+ padding: 12px 20px;
+ border-radius: 6px;
+ font-size: 14px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+ z-index: 100;
+ animation: slideInDown 0.3s ease;
+ max-width: 80%;
+ text-align: center;
+}
+
+@keyframes slideInDown {
+ from {
+ opacity: 0;
+ transform: translate(-50%, -60%);
+ }
+ to {
+ opacity: 1;
+ transform: translate(-50%, -50%);
+ }
+}
+
+html.darkmode .mermaid-export-error {
+ background: rgba(200, 35, 51, 0.95);
+}
+
/* 全屏模式下的内部容器 */
.mermaid-fullscreen .mermaid-container-inner {
max-height: calc(100vh - 100px);
@@ -1243,6 +1340,18 @@ article.card .mermaid-container {
padding: 5px 10px;
bottom: 8px;
}
+
+ /* 移动端导出菜单调整 */
+ .mermaid-export-menu {
+ right: 8px;
+ top: 50px;
+ min-width: 150px;
+ }
+
+ .mermaid-export-option {
+ padding: 10px 14px;
+ font-size: 15px;
+ }
}
/* 需求 16.5: 横屏模式优化 - 自动调整图表布局 */