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