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: 横屏模式优化 - 自动调整图表布局 */