diff --git a/backend/mindmap.db b/backend/mindmap.db index eecb48a..ca6428c 100644 Binary files a/backend/mindmap.db and b/backend/mindmap.db differ diff --git a/frontend/src/components/MindMap.vue b/frontend/src/components/MindMap.vue index aaf4ad1..0de8429 100644 --- a/frontend/src/components/MindMap.vue +++ b/frontend/src/components/MindMap.vue @@ -209,6 +209,18 @@ import '../lib/mind-elixir/dist/style.css'; import { renderMarkdownToHTML } from '../utils/markdownRenderer.js'; import Vditor from 'vditor'; import 'vditor/dist/index.css'; +import { marked } from 'marked'; + +// 配置marked库 +marked.setOptions({ + breaks: true, // 支持换行 + gfm: true, // 支持GitHub风格的Markdown + tables: true, // 支持表格 + pedantic: false, // 不严格模式 + sanitize: false, // 不过滤HTML标签 + smartLists: true, // 智能列表 + smartypants: false // 不转换引号 +}); // 使用Vditor渲染Markdown内容 const renderMarkdownWithVditor = async (markdown) => { @@ -445,41 +457,233 @@ const closeImagePreview = () => { imagePreviewError.value = ''; }; -// HTML转Markdown函数 -const convertHTMLToMarkdown = (html) => { +/** + * 从Markdown内容生成简短标题 + * 用于节点显示 + */ +const generateTopicFromMarkdown = (markdown) => { + if (!markdown || typeof markdown !== 'string') { + return ''; + } + + try { + // 移除Markdown语法,提取纯文本 + let text = markdown + // 移除标题标记 + .replace(/^#{1,6}\s+/gm, '') + // 移除加粗和斜体 + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/\*(.*?)\*/g, '$1') + // 移除代码块 + .replace(/```[\s\S]*?```/g, '') + .replace(/`([^`]*)`/g, '$1') + // 移除链接,保留文本 + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') + // 移除图片 + .replace(/!\[([^\]]*)\]\([^)]+\)/g, '') + // 移除表格分隔符 + .replace(/^\|.*\|$/gm, '') + .replace(/^[-|:\s]+$/gm, '') + // 移除列表标记 + .replace(/^[-*+]\s+/gm, '') + .replace(/^\d+\.\s+/gm, '') + // 移除引用标记 + .replace(/^>\s*/gm, '') + // 移除多余空白 + .replace(/\n+/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + + // 限制长度,取前50个字符 + if (text.length > 50) { + text = text.substring(0, 50) + '...'; + } + + return text || '无标题'; + } catch (error) { + console.error('❌ 生成标题失败:', error); + return '无标题'; + } +}; + +/** + * Markdown转HTML转换函数 + * 支持表格、图片等复杂内容的转换 + */ +const convertMarkdownToHTML = (markdown) => { + if (!markdown || typeof markdown !== 'string') { + return ''; + } + + try { + let html = markdown; + + // 处理表格 - 简化版本,处理更灵活 + // 先尝试处理标准Markdown表格 + html = html.replace(/\|(.+)\|\n\|[-\s|]+\|\n((?:\|.+\|\n?)*)/g, (match, header, rows) => { + const headers = header.split('|').map(h => h.trim()).filter(h => h); + const rowLines = rows.trim().split('\n').filter(line => line.trim()); + + let tableHTML = '\n'; + + // 表头 + if (headers.length > 0) { + tableHTML += ''; + headers.forEach(header => { + tableHTML += ``; + }); + tableHTML += '\n'; + } + + // 表体 + if (rowLines.length > 0) { + tableHTML += ''; + rowLines.forEach(row => { + const cells = row.split('|').map(c => c.trim()).filter(c => c); + if (cells.length > 0) { + tableHTML += ''; + cells.forEach(cell => { + tableHTML += ``; + }); + tableHTML += ''; + } + }); + tableHTML += ''; + } + + tableHTML += '
${header}
${cell}
'; + return tableHTML; + }); + + // 如果没有找到标准表格,尝试处理简单的列表格式 + if (!html.includes('
$1:
$2
'); + } + + // 处理图片 + html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1'); + + // 处理标题 + html = html.replace(/^### (.*$)/gim, '

$1

'); + html = html.replace(/^## (.*$)/gim, '

$1

'); + html = html.replace(/^# (.*$)/gim, '

$1

'); + + // 处理加粗 + html = html.replace(/\*\*(.*?)\*\*/g, '$1'); + + // 处理斜体 + html = html.replace(/\*(.*?)\*/g, '$1'); + + // 处理代码块 + html = html.replace(/```([^`]*)```/g, '
$1
'); + + // 处理行内代码 + html = html.replace(/`([^`]*)`/g, '$1'); + + // 处理链接 + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + + // 处理列表 + html = html.replace(/^- (.*$)/gim, '
  • $1
  • '); + html = html.replace(/(
  • .*<\/li>)/s, ''); + + // 处理换行 + html = html.replace(/\n\n/g, '

    '); + html = html.replace(/\n/g, '
    '); + + // 包装段落 + if (!html.startsWith('<')) { + html = '

    ' + html + '

    '; + } + + return html; + + } catch (error) { + console.error('❌ Markdown转HTML转换失败:', error); + return markdown; // 回退到原始Markdown + } +}; + +/** + * 改进的HTML转Markdown转换函数 + * 支持表格、图片等复杂内容的转换 + */ +const convertHTMLToMarkdownImproved = (html) => { if (!html || typeof html !== 'string') { return ''; } try { - console.log('🔄 开始转换HTML到Markdown:', html.substring(0, 100) + '...'); - - // 使用Vditor的html2md功能(如果可用) + // 优先使用Vditor的html2md功能 if (typeof Vditor?.html2md === 'function') { - const markdown = Vditor.html2md(html); - console.log('✅ 使用Vditor.html2md转换成功:', markdown.substring(0, 100) + '...'); - return markdown; + console.log('✅ 使用Vditor.html2md转换HTML'); + return Vditor.html2md(html); } - // 回退到简单转换逻辑 - console.log('⚠️ Vditor.html2md不可用,使用简单转换逻辑'); - let markdown = html - // 处理表格 - 保持表格结构 - .replace(/]*>/gi, '\n') - .replace(/<\/table>/gi, '\n') - .replace(/]*>/gi, '') - .replace(/<\/thead>/gi, '') - .replace(/]*>/gi, '') - .replace(/<\/tbody>/gi, '') - .replace(/]*>/gi, '') - .replace(/<\/tr>/gi, ' |\n') - .replace(/]*>/gi, '| ') - .replace(/<\/th>/gi, ' ') - .replace(/]*>/gi, '| ') - .replace(/<\/td>/gi, ' ') - // 图片处理 - .replace(/]*src="([^"]+)"[^>]*alt="([^"]*)"[^>]*>/gi, '![$2]($1)') - .replace(/]*src="([^"]+)"[^>]*>/gi, '![]($1)') + console.log('⚠️ Vditor.html2md不可用,使用改进的转换逻辑'); + + // 创建临时DOM元素进行解析 + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = html; + + let markdown = ''; + + // 处理表格 + const tables = tempDiv.querySelectorAll('table'); + tables.forEach((table, tableIndex) => { + console.log(`📊 处理表格 ${tableIndex + 1}`); + + // 处理表头 + const thead = table.querySelector('thead'); + if (thead) { + const headerRow = thead.querySelector('tr'); + if (headerRow) { + const headers = Array.from(headerRow.querySelectorAll('th, td')).map(cell => + (cell.textContent || '').trim().replace(/\|/g, '\\|') + ); + markdown += '| ' + headers.join(' | ') + ' |\n'; + markdown += '| ' + headers.map(() => '---').join(' | ') + ' |\n'; + } + } + + // 处理表体 + const tbody = table.querySelector('tbody') || table; + const rows = tbody.querySelectorAll('tr'); + rows.forEach(row => { + // 跳过表头行(如果已经在thead中处理过) + if (thead && thead.contains(row)) return; + + const cells = Array.from(row.querySelectorAll('td, th')).map(cell => + (cell.textContent || '').trim().replace(/\|/g, '\\|') + ); + if (cells.length > 0) { + markdown += '| ' + cells.join(' | ') + ' |\n'; + } + }); + + markdown += '\n'; + }); + + // 处理图片 + const images = tempDiv.querySelectorAll('img'); + images.forEach(img => { + const src = img.src || img.getAttribute('src') || ''; + const alt = img.alt || img.getAttribute('alt') || ''; + if (src) { + markdown += `![${alt}](${src})\n\n`; + } + }); + + // 处理其他内容(移除已处理的表格和图片) + const processedDiv = tempDiv.cloneNode(true); + processedDiv.querySelectorAll('table, img').forEach(el => el.remove()); + + // 转换剩余内容 + let remainingContent = processedDiv.innerHTML; + + // 基本HTML到Markdown转换 + remainingContent = remainingContent // 换行与段落 .replace(//gi, '\n') .replace(/<\/p>/gi, '\n\n') @@ -494,48 +698,32 @@ const convertHTMLToMarkdown = (html) => { .replace(/<\/ul>/gi, '\n') .replace(/]*>/gi, '- ') .replace(/<\/li>/gi, '\n') + // 标题 + .replace(/]*>(.*?)<\/h1>/gi, '# $1\n\n') + .replace(/]*>(.*?)<\/h2>/gi, '## $1\n\n') + .replace(/]*>(.*?)<\/h3>/gi, '### $1\n\n') + .replace(/]*>(.*?)<\/h4>/gi, '#### $1\n\n') + .replace(/]*>(.*?)<\/h5>/gi, '##### $1\n\n') + .replace(/]*>(.*?)<\/h6>/gi, '###### $1\n\n') + // 链接 + .replace(/]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)') + // 代码 + .replace(/]*>(.*?)<\/code>/gi, '`$1`') + .replace(/]*>(.*?)<\/pre>/gi, '```\n$1\n```\n\n') // 清除剩余标签 .replace(/<[^>]+>/g, '') // 规范换行 .replace(/\n{3,}/g, '\n\n') .trim(); - // 处理表格格式 - 添加表头分隔线 - const lines = markdown.split('\n'); - const processedLines = []; - let inTable = false; + markdown += remainingContent; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.includes('|') && line.trim().length > 0) { - if (!inTable) { - inTable = true; - processedLines.push(line); - // 添加表头分隔线 - const headerCells = line.split('|').filter(cell => cell.trim()); - if (headerCells.length > 0) { - const separator = '| ' + headerCells.map(() => '---').join(' | ') + ' |'; - processedLines.push(separator); - } - } else { - processedLines.push(line); - } - } else { - if (inTable) { - inTable = false; - } - processedLines.push(line); - } - } - - markdown = processedLines.join('\n'); - - console.log('✅ 简单转换逻辑完成:', markdown.substring(0, 100) + '...'); + console.log('✅ HTML转Markdown转换完成:', markdown.substring(0, 200) + '...'); return markdown; } catch (error) { - console.error('❌ HTML转Markdown失败:', error); - return html; // 转换失败时返回原始HTML + console.error('❌ HTML转Markdown转换失败:', error); + return html; // 回退到原始HTML } }; @@ -565,15 +753,55 @@ const openRichTextEditor = (nodeObj, nodeElement) => { allKeys: Object.keys(nodeObj) }); - // 将HTML内容转换为Markdown格式供编辑 + // 使用新的节点结构逻辑 + let htmlContent = ''; + + // 优先使用dangerouslySetInnerHTML(用于WYSIWYG编辑) if (nodeObj.dangerouslySetInnerHTML) { - editorContent.value = convertHTMLToMarkdown(nodeObj.dangerouslySetInnerHTML); - console.log('📝 转换后的Markdown内容:', editorContent.value); - } else if (nodeObj.topic) { - editorContent.value = nodeObj.topic; - } else { - editorContent.value = ''; + htmlContent = nodeObj.dangerouslySetInnerHTML; + console.log('📝 使用dangerouslySetInnerHTML内容:', htmlContent.substring(0, 200) + '...'); + } + // 其次使用markdown字段(如果存在) + else if (nodeObj.markdown) { + console.log('📝 使用markdown内容,转换为HTML:', nodeObj.markdown.substring(0, 200) + '...'); + try { + htmlContent = marked.parse(nodeObj.markdown); + // 更新dangerouslySetInnerHTML + nodeObj.dangerouslySetInnerHTML = htmlContent; + } catch (error) { + console.error('❌ Markdown转HTML失败:', error); + htmlContent = nodeObj.markdown; + } } + // 最后回退到data或topic(兼容旧数据) + else { + const markdownContent = nodeObj.data || nodeObj.topic || ''; + console.log('📝 使用兼容模式,从data/topic获取内容:', markdownContent.substring(0, 200) + '...'); + + // 检查是否包含Markdown格式内容 + const hasMarkdownContent = markdownContent.includes('|') && markdownContent.includes('-') || // 表格 + markdownContent.includes('![') || // 图片 + markdownContent.includes('#') || // 标题 + markdownContent.includes('**') || // 粗体 + markdownContent.includes('`'); // 代码 + + if (hasMarkdownContent) { + try { + htmlContent = marked.parse(markdownContent); + // 更新节点结构 + nodeObj.markdown = markdownContent; + nodeObj.dangerouslySetInnerHTML = htmlContent; + } catch (error) { + console.error('❌ Markdown转HTML失败:', error); + htmlContent = markdownContent; + } + } else { + htmlContent = markdownContent; + } + } + + editorContent.value = htmlContent; + console.log('🎯 WYSIWYG模式HTML内容:', htmlContent.substring(0, 200) + '...'); // 检查是否有MindElixir原生图片 if (nodeObj.image && !editorContent.value.includes('![')) { @@ -622,13 +850,6 @@ const initVditor = async () => { return; } - console.log('🎯 开始初始化Vditor编辑器...'); - console.log('🔍 vditorContainer元素:', vditorContainer.value); - console.log('🔍 容器可见性:', vditorContainer.value.offsetParent !== null); - console.log('🔍 容器尺寸:', { - width: vditorContainer.value.offsetWidth, - height: vditorContainer.value.offsetHeight - }); try { // 先清理容器 @@ -638,7 +859,7 @@ const initVditor = async () => { vditorInstance = new Vditor(vditorContainer.value, { height: 400, placeholder: '请输入节点内容...', - mode: 'wysiwyg', // 所见即所得模式 + mode: 'wysiwyg', // 所见即所得模式,使用HTML内容 theme: 'classic', toolbarConfig: { pin: true // 固定工具栏 @@ -691,17 +912,12 @@ const initVditor = async () => { } }, after: () => { - console.log('✅ Vditor编辑器初始化完成'); - // 等待编辑器完全渲染后再设置内容 setTimeout(() => { // ✅ 直接设置内容,让Vditor自动处理Markdown/HTML转换 if (editorContent.value && editorContent.value.trim()) { vditorInstance.setValue(editorContent.value); - console.log('✅ 内容已设置到WYSIWYG编辑器:', editorContent.value.substring(0, 100) + '...'); - console.log('✅ 编辑器当前模式: wysiwyg (所见即所得模式)'); } else { - console.warn('⚠️ editorContent.value为空,设置空内容'); vditorInstance.setValue(''); } @@ -714,34 +930,18 @@ const initVditor = async () => { // 获取Markdown格式的内容 const markdownContent = vditorInstance.getValue(); editorContent.value = markdownContent; - console.log('📝 编辑器内容变化(Markdown):', markdownContent.substring(0, 50) + '...'); }); - } else { - console.warn('⚠️ Vditor实例没有on方法'); } // 检查工具栏是否正确渲染 setTimeout(() => { const toolbar = document.querySelector('.vditor-toolbar'); const editor = document.querySelector('.vditor-wysiwyg'); - console.log('🔍 Vditor工具栏:', toolbar); - console.log('🔍 Vditor编辑器区域:', editor); - - if (!toolbar || !editor) { - console.error('❌ Vditor组件没有正确渲染'); - console.error('工具栏状态:', toolbar ? '存在' : '不存在'); - console.error('编辑器状态:', editor ? '存在' : '不存在'); - } else { - console.log('✅ Vditor组件渲染成功'); - console.log('工具栏按钮数量:', toolbar.querySelectorAll('button').length); - console.log('工具栏高度:', toolbar.offsetHeight); - } }, 100); }, 100); } }); - console.log('✅ Vditor编辑器创建成功'); } catch (error) { console.error('❌ Vditor编辑器初始化失败:', error); console.error('错误详情:', error.stack); @@ -820,14 +1020,13 @@ const saveRichTextChanges = async () => { } // ✅ 使用Vditor内置API获取内容 - const markdownContent = vditorInstance ? vditorInstance.getValue() : editorContent.value; - const htmlContent = vditorInstance ? vditorInstance.getHTML() : editorContent.value; - const contentToSave = markdownContent.trim() || ''; + const htmlContent = vditorInstance ? vditorInstance.getValue() : editorContent.value; + const markdownContent = vditorInstance ? vditorInstance.getMarkdown() : ''; - console.log('📝 获取到的Markdown内容:', contentToSave.substring(0, 100) + '...'); console.log('📝 获取到的HTML内容:', htmlContent.substring(0, 100) + '...'); + console.log('📝 获取到的Markdown内容:', markdownContent.substring(0, 100) + '...'); - // 更新节点数据 + // 更新节点数据 - 使用新的节点结构 const titleChanged = editorTitle.value !== (currentNode.value.title || ''); console.log('🔍 标题变化检查:', { editorTitle: editorTitle.value, @@ -835,10 +1034,11 @@ const saveRichTextChanges = async () => { titleChanged: titleChanged }); - // 更新节点内容 - currentNode.value.topic = contentToSave; // 保存原始Markdown内容 + // 更新节点内容 - 新的节点结构 + currentNode.value.dangerouslySetInnerHTML = htmlContent; // 保存HTML内容(用于渲染) + currentNode.value.markdown = markdownContent || htmlContent; // 保存Markdown内容(用于导出) + currentNode.value.topic = generateTopicFromMarkdown(markdownContent || htmlContent); // 生成简短标题 currentNode.value.title = editorTitle.value; // 更新标题 - currentNode.value.dangerouslySetInnerHTML = htmlContent; // 保存渲染后的HTML // 更新DOM元素 if (currentNodeElement.value) { @@ -864,7 +1064,9 @@ const saveRichTextChanges = async () => { newTitle: editorTitle.value, // 使用更新后的标题 newDes: currentNode.value.data?.des || "", newParentId: currentNode.value.parentId || currentNode.value.parent?.id, - newDangerouslySetInnerHTML: htmlContent || "" // 保存富文本内容 + newDangerouslySetInnerHTML: htmlContent || "", // 保存HTML内容(用于渲染) + newMarkdown: markdownContent || htmlContent, // 保存Markdown内容(用于导出) + newTopic: currentNode.value.topic // 保存生成的简短标题 }; console.log("🔍 直接发送到后端的更新数据:", updateData); @@ -3656,24 +3858,18 @@ const openCustomEditModal = (nodeObj, nodeElement) => { // 确保模态框完全渲染后再初始化Vditor编辑器 nextTick(() => { setTimeout(() => { - console.log('🔄 准备初始化Vditor,容器状态:', vditorContainer.value); - // 检查容器是否真正可见 if (vditorContainer.value && vditorContainer.value.offsetParent !== null) { - console.log('✅ 容器已可见,开始初始化Vditor'); initVditor(); } else { - console.warn('⚠️ 容器未准备好,延迟初始化'); // 多次尝试,确保容器准备好 let attempts = 0; const maxAttempts = 10; const checkContainer = () => { attempts++; if (vditorContainer.value && vditorContainer.value.offsetParent !== null) { - console.log('✅ 容器在第', attempts, '次尝试后可见'); initVditor(); } else if (attempts < maxAttempts) { - console.log('🔄 第', attempts, '次检查,容器仍未准备好,继续等待...'); setTimeout(checkContainer, 100); } else { console.error('❌ 容器在', maxAttempts, '次尝试后仍未准备好'); @@ -4785,6 +4981,7 @@ const updateMindMapRealtime = async (data, title, eventDetail = null) => { infinite: true, maxScale: 5, minScale: 0.1, + theme: customTheme, // 使用自定义紫色主题 markdown: (text, nodeObj) => { // 检查内容是否包含markdown语法(包括图片和数学公式) if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#') || text.includes('$') || text.includes('![')) { @@ -4869,36 +5066,73 @@ const updateMindMapRealtime = async (data, title, eventDetail = null) => { } } else { - // 完整更新:仅在必要时重新初始化 - console.log('🔄 执行完整更新'); + // 优化:避免不必要的完整更新,优先使用增量更新 + console.log('🔄 尝试增量更新替代完整更新'); - const currentId = String(currentMindmapId.value || ''); - const tempId = currentId && currentId.startsWith('temp-') - ? currentId - : `temp-${Date.now()}`; - - const mindElixirData = { - nodeData: data, - mindmapId: tempId, - id: tempId, - title: title || 'AI生成中...' - }; - - // 确保当前思维导图ID是临时ID - if (!currentId || !currentId.startsWith('temp-')) { - currentMindmapId.value = tempId; - console.log('🆔 更新临时思维导图ID:', tempId); - } - - // 重新初始化数据 - const result = mindElixir.value.init(mindElixirData); - - // 恢复位置 - if (currentPosition) { - setTimeout(() => { - restorePosition(currentPosition); - console.log('📍 完整更新后恢复位置'); - }, 100); + // 检查是否可以增量更新 + if (mindElixir.value && mindElixir.value.data && mindElixir.value.data.nodeData) { + console.log('✅ 使用增量更新,避免页面空白'); + + // 更新根节点标题 + if (mindElixir.value.data.nodeData && data.topic) { + const rootNode = Object.values(mindElixir.value.data.nodeData)[0]; + if (rootNode && rootNode.topic !== data.topic) { + rootNode.topic = data.topic; + // 更新DOM中的标题显示 + const rootElement = mindmapEl.value.querySelector(`[data-id="${rootNode.id}"]`); + if (rootElement) { + const textElement = rootElement.querySelector('.topic-text'); + if (textElement) { + textElement.textContent = data.topic; + } + } + } + } + + // 增量更新子节点 + if (data.children && data.children.length > 0) { + updateNodesIncremental(data.children, mindElixir.value.data.nodeData); + } + + // 恢复位置,避免视图跳动 + if (currentPosition) { + setTimeout(() => { + restorePosition(currentPosition); + console.log('📍 优化增量更新后恢复位置'); + }, 50); + } + } else { + // 只有在确实无法增量更新时才执行完整更新 + console.log('⚠️ 无法增量更新,执行完整更新'); + + const currentId = String(currentMindmapId.value || ''); + const tempId = currentId && currentId.startsWith('temp-') + ? currentId + : `temp-${Date.now()}`; + + const mindElixirData = { + nodeData: data, + mindmapId: tempId, + id: tempId, + title: title || 'AI生成中...' + }; + + // 确保当前思维导图ID是临时ID + if (!currentId || !currentId.startsWith('temp-')) { + currentMindmapId.value = tempId; + console.log('🆔 更新临时思维导图ID:', tempId); + } + + // 重新初始化数据 + const result = mindElixir.value.init(mindElixirData); + + // 恢复位置 + if (currentPosition) { + setTimeout(() => { + restorePosition(currentPosition); + console.log('📍 完整更新后恢复位置'); + }, 100); + } } } diff --git a/frontend/src/utils/markdownRenderer.js b/frontend/src/utils/markdownRenderer.js index ed0fff4..2831ff9 100644 --- a/frontend/src/utils/markdownRenderer.js +++ b/frontend/src/utils/markdownRenderer.js @@ -53,13 +53,10 @@ renderer.image = function(href, title, text) { // 处理图片URL,确保能正确显示 let processedUrl = hrefStr; - - // 暂时禁用代理URL转换,直接使用原始URL - // 这样可以避免代理服务配置问题导致的图片显示异常 if (hrefStr.includes('cdn-mineru.openxlab.org.cn')) { - // 直接使用原始URL,不进行代理转换 - processedUrl = hrefStr; - console.log('🖼️ 使用原始CDN URL:', processedUrl); + // 将外部CDN URL转换为代理URL + const urlPath = hrefStr.replace('https://cdn-mineru.openxlab.org.cn', ''); + processedUrl = `/proxy-image${urlPath}`; } // 生成图片HTML