diff --git a/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx b/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx index b0179e8b..c9dc60b7 100644 --- a/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx +++ b/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx @@ -55,6 +55,7 @@ const NotificationButton: React.FC = ({ className = '' ].filter(Boolean).join(' ')} onClick={() => notificationService.toggleCenter()} type="button" + data-testid="notification-button" > {activeNotification ? ( <> diff --git a/src/web-ui/src/locales/en-US/mermaid-editor.json b/src/web-ui/src/locales/en-US/mermaid-editor.json index d23dc7ef..87b65529 100644 --- a/src/web-ui/src/locales/en-US/mermaid-editor.json +++ b/src/web-ui/src/locales/en-US/mermaid-editor.json @@ -97,6 +97,18 @@ "loading": { "processing": "Processing..." }, + "export": { + "loading": { + "svgTitle": "Exporting SVG", + "svgMessage": "Generating SVG file...", + "pngTitle": "Exporting PNG", + "pngMessage": "Rendering diagram and generating PNG..." + }, + "success": { + "svg": "SVG exported successfully", + "png": "PNG exported successfully" + } + }, "panel": { "diagramName": "Mermaid Diagram" }, diff --git a/src/web-ui/src/locales/zh-CN/mermaid-editor.json b/src/web-ui/src/locales/zh-CN/mermaid-editor.json index 6c5cfb45..23bd608c 100644 --- a/src/web-ui/src/locales/zh-CN/mermaid-editor.json +++ b/src/web-ui/src/locales/zh-CN/mermaid-editor.json @@ -97,6 +97,18 @@ "loading": { "processing": "处理中..." }, + "export": { + "loading": { + "svgTitle": "正在导出 SVG", + "svgMessage": "正在生成 SVG 文件...", + "pngTitle": "正在导出 PNG", + "pngMessage": "正在渲染图表并生成 PNG..." + }, + "success": { + "svg": "SVG 导出成功", + "png": "PNG 导出成功" + } + }, "panel": { "diagramName": "Mermaid图表" }, diff --git a/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx b/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx index cbbbd76f..48a7af46 100644 --- a/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx +++ b/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx @@ -329,7 +329,7 @@ export const NotificationCenter: React.FC = () => { showCloseButton={false} size="large" > -
+

{t('components:notificationCenter.title')}

@@ -401,7 +401,7 @@ export const NotificationCenter: React.FC = () => {
{activeTaskNotifications.length > 0 && ( -
+
{t('components:notificationCenter.activeTasks.title', { count: activeTaskNotifications.length })}
diff --git a/src/web-ui/src/tools/mermaid-editor/components/MermaidEditor.tsx b/src/web-ui/src/tools/mermaid-editor/components/MermaidEditor.tsx index 10b171a3..aec1ab3d 100644 --- a/src/web-ui/src/tools/mermaid-editor/components/MermaidEditor.tsx +++ b/src/web-ui/src/tools/mermaid-editor/components/MermaidEditor.tsx @@ -12,6 +12,10 @@ import { CubeLoading } from '@/component-library/components/CubeLoading'; import { Sparkles } from 'lucide-react'; import { aiApi } from '../../../infrastructure/api'; import { useI18n } from '@/infrastructure/i18n'; +import { notificationService } from '@/shared/notification-system'; + +import { downloadDir, join } from '@tauri-apps/api/path'; +import { writeFile } from '@tauri-apps/plugin-fs'; import './MermaidEditor.css'; /** Escape regex special characters. */ @@ -29,6 +33,130 @@ const detectDiagramType = (code: string): MermaidDiagramContext['diagramType'] = return 'other'; }; +const SVG_NS = 'http://www.w3.org/2000/svg'; +const XHTML_NS = 'http://www.w3.org/1999/xhtml'; + +const sanitizeFileName = (name: string) => name.replace(/[<>:"/\\|?*\u0000-\u001F]/g, '_').trim() || 'diagram'; + +const createTimestampSuffix = () => { + const now = new Date(); + const pad = (value: number) => String(value).padStart(2, '0'); + return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`; +}; + +const saveExportBlob = async (blob: Blob, baseName: string, extension: 'svg' | 'png') => { + const downloadsPath = await downloadDir(); + const fileName = `${sanitizeFileName(baseName)}_${createTimestampSuffix()}.${extension}`; + const filePath = await join(downloadsPath, fileName); + const bytes = new Uint8Array(await blob.arrayBuffer()); + await writeFile(filePath, bytes); + return filePath; +}; + +/** + * Prepare a self-contained SVG element for export. + * + * cloneNode(true) already preserves: + * - Mermaid's internal