feat: 实现 Mermaid 导出功能
- 添加导出按钮到工具栏,支持 PNG 和 SVG 格式导出 - 实现导出菜单,点击导出按钮显示格式选项 - PNG 导出:将 SVG 转换为 PNG 图片并下载 - SVG 导出:保存 SVG 代码为文件并下载 - 导出时保持图表当前的缩放级别和样式 - 添加导出错误处理,显示友好的错误提示 - 导出菜单支持点击外部关闭 - 添加导出菜单样式,支持夜间模式 - 移动端导出菜单适配,调整按钮大小和位置 - 错误提示自动消失(3秒后) 需求:15.1, 15.2, 15.3, 15.4, 15.5
This commit is contained in:
198
argontheme.js
198
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;
|
||||
<button class="mermaid-zoom-btn" data-action="zoom-in" title="放大">+</button>
|
||||
<button class="mermaid-zoom-btn" data-action="zoom-reset" title="重置">⟲</button>
|
||||
<button class="mermaid-zoom-btn" data-action="fullscreen" title="全屏">⛶</button>
|
||||
<button class="mermaid-zoom-btn mermaid-export-btn" data-action="export" title="导出">⬇</button>
|
||||
`;
|
||||
|
||||
container.appendChild(controls);
|
||||
|
||||
// 需求 15.1: 创建导出菜单
|
||||
const exportMenu = document.createElement('div');
|
||||
exportMenu.className = 'mermaid-export-menu';
|
||||
exportMenu.innerHTML = `
|
||||
<button class="mermaid-export-option" data-format="png">导出为 PNG</button>
|
||||
<button class="mermaid-export-option" data-format="svg">导出为 SVG</button>
|
||||
`;
|
||||
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 = '<?xml version="1.0" encoding="UTF-8"?>\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) => {
|
||||
|
||||
109
style.css
109
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: 横屏模式优化 - 自动调整图表布局 */
|
||||
|
||||
Reference in New Issue
Block a user