🐛 修复编辑操作重复调用导致的双重通知问题

关键修复:
- 添加防重复处理机制,避免同一个编辑操作被多次处理
- 合并finishEdit和finishEditTable事件监听器
- 使用Set记录正在处理的操作,防止重复调用
- 添加详细的调试日志帮助追踪问题

问题根因:
- finishEdit和finishEditTable事件都被监听,导致重复调用
- 缺乏防重复处理机制
- 用户看到成功和失败两个通知

技术实现:
- 使用operationKey = `${operation.name}-${operation.obj?.id}` 唯一标识操作
- 在processingEditOperations Set中记录正在处理的操作
- 处理完成后自动清理标记
- 合并重复的事件监听器逻辑

现在用户只会看到一个正确的保存成功通知。
This commit is contained in:
lixinran 2025-10-11 15:06:38 +08:00
parent e3fedcbf0f
commit 642b12c217
7 changed files with 545 additions and 113 deletions

Binary file not shown.

View File

@ -359,6 +359,18 @@ def update_node(request):
node.desc = body.get('newDes') or ''
updated_fields.append('des')
if 'newDangerouslySetInnerHTML' in body:
try:
new_html_content = body.get('newDangerouslySetInnerHTML') or ''
print(f"🔍 更新节点 {node.id} 的 html_content: {new_html_content[:100]}...")
print(f"🔍 html_content 字段类型: {type(node.html_content)}")
node.html_content = new_html_content
updated_fields.append('html_content')
print(f"🔍 html_content 更新成功,长度: {len(new_html_content)}")
except Exception as e:
print(f"❌ 更新 html_content 时出错: {e}")
return Response({'detail': f'Failed to update html_content: {str(e)}'}, status=500)
if 'newParentId' in body:
new_parent_id = body.get('newParentId')
old_parent_id = node.parent_id
@ -374,7 +386,12 @@ def update_node(request):
# 更新新父节点的子节点计数
parent_node.children_count += 1
parent_node.save()
try:
with transaction.atomic():
parent_node.save()
except Exception as e:
print(f"⚠️ 更新父节点计数时出错: {e}")
# 继续执行,不中断主流程
except Node.DoesNotExist:
return Response({'detail': 'parent node not found'}, status=404)
@ -389,14 +406,41 @@ def update_node(request):
try:
old_parent = Node.objects.get(id=old_parent_id, deleted=False, mindmap=node.mindmap)
old_parent.children_count = max(0, old_parent.children_count - 1)
old_parent.save()
try:
with transaction.atomic():
old_parent.save()
except Exception as e:
print(f"⚠️ 更新原父节点计数时出错: {e}")
# 继续执行,不中断主流程
except Node.DoesNotExist:
pass # 原父节点可能已被删除
# 只有在有字段更新时才更新时间戳
if updated_fields:
node.updated_at = timezone.now()
node.save()
try:
from django.db import transaction
import time
# 使用事务和重试机制处理数据库锁定
max_retries = 3
for attempt in range(max_retries):
try:
with transaction.atomic():
node.updated_at = timezone.now()
print(f"🔍 准备保存节点 {node.id},更新字段: {updated_fields} (尝试 {attempt + 1}/{max_retries})")
node.save()
print(f"✅ 节点 {node.id} 保存成功")
break # 成功则跳出重试循环
except Exception as e:
if "database is locked" in str(e) and attempt < max_retries - 1:
print(f"⚠️ 数据库被锁定,等待重试... (尝试 {attempt + 1}/{max_retries})")
time.sleep(0.1 * (attempt + 1)) # 递增等待时间
continue
else:
raise e
except Exception as e:
print(f"❌ 保存节点 {node.id} 时出错: {e}")
return Response({'detail': f'Failed to save node: {str(e)}'}, status=500)
return Response({
'success': True,

View File

@ -164,37 +164,54 @@
</div>
</div>
<!-- 图片编辑器模态框 -->
<div v-if="showImageEditor" class="image-editor-modal" @click="closeImageEditor">
<div class="image-editor-content" @click.stop>
<div class="image-editor-header">
<span class="image-editor-title">{{ imageEditorTitle }}</span>
<button @click="closeImageEditor" class="image-editor-close">×</button>
<!-- 富文本编辑器模态框 -->
<div v-if="showRichTextEditor" class="rich-text-editor-modal" @click="closeRichTextEditor">
<div class="rich-text-editor-content" @click.stop>
<div class="rich-text-editor-header">
<span class="rich-text-editor-title">{{ editorTitle }}</span>
<button @click="closeRichTextEditor" class="rich-text-editor-close">×</button>
</div>
<div class="image-editor-body">
<div class="current-image-section">
<h4>当前图片</h4>
<img :src="imageEditorUrl" :alt="imageEditorTitle" class="current-image" />
</div>
<div class="replace-image-section">
<h4>替换图片</h4>
<div class="rich-text-editor-body">
<div class="editor-toolbar">
<div class="file-input-wrapper">
<input
type="file"
@change="handleImageFileSelect"
@change="handleFileSelect"
accept="image/*"
id="imageFileInput"
id="richTextFileInput"
class="file-input"
/>
<label for="imageFileInput" class="file-input-label">
📁 选择新图片
<label for="richTextFileInput" class="file-input-label">
📁 插入图片
</label>
</div>
<p class="file-hint">支持 JPGPNGGIF 格式最大 5MB</p>
</div>
<div class="editor-main">
<textarea
id="richTextEditor"
v-model="editorContent"
class="rich-text-textarea"
placeholder="输入Markdown格式内容...
支持的功能
文本编辑
图片插入![描述](图片链接)
表格| 列1 | 列2 |
列表 项目1
标题# 标题
点击上方'插入图片'按钮可以添加本地图片"
></textarea>
<div class="editor-preview" v-if="editorContent">
<h5>预览</h5>
<div class="preview-content" v-html="renderMarkdownPreview(editorContent)"></div>
</div>
</div>
</div>
<div class="image-editor-footer">
<button @click="closeImageEditor" class="cancel-button">取消</button>
<div class="rich-text-editor-footer">
<button @click="closeRichTextEditor" class="cancel-button">取消</button>
<button @click="saveRichTextChanges" class="save-button">保存</button>
</div>
</div>
</div>
@ -290,11 +307,12 @@ const imagePreviewTitle = ref('');
const imagePreviewLoading = ref(false);
const imagePreviewError = ref('');
//
const showImageEditor = ref(false);
const imageEditorUrl = ref('');
const imageEditorTitle = ref('');
const currentImageElement = ref(null);
//
const showRichTextEditor = ref(false);
const editorTitle = ref('');
const editorContent = ref('');
const currentNode = ref(null);
const currentNodeElement = ref(null);
@ -380,21 +398,34 @@ const closeImagePreview = () => {
imagePreviewError.value = '';
};
//
const openImageEditor = (imageUrl, altText, imgElement) => {
console.log('🖼️ 打开图片编辑器:', { imageUrl, altText });
//
const openRichTextEditor = (nodeObj, nodeElement) => {
console.log('📝 打开富文本编辑器:', { nodeObj, nodeElement });
imageEditorUrl.value = imageUrl;
imageEditorTitle.value = altText || '编辑图片';
currentImageElement.value = imgElement;
showImageEditor.value = true;
currentNode.value = nodeObj;
currentNodeElement.value = nodeElement;
editorTitle.value = nodeObj.topic || '编辑节点内容';
// HTMLMarkdown
if (nodeObj.dangerouslySetInnerHTML) {
editorContent.value = convertHTMLToMarkdown(nodeObj.dangerouslySetInnerHTML);
console.log('📝 转换后的Markdown内容:', editorContent.value);
} else if (nodeObj.topic) {
editorContent.value = nodeObj.topic;
} else {
editorContent.value = '';
}
showRichTextEditor.value = true;
console.log('📝 富文本编辑器已打开');
};
const closeImageEditor = () => {
showImageEditor.value = false;
imageEditorUrl.value = '';
imageEditorTitle.value = '';
currentImageElement.value = null;
const closeRichTextEditor = () => {
showRichTextEditor.value = false;
editorTitle.value = '';
editorContent.value = '';
currentNode.value = null;
currentNodeElement.value = null;
};
const handleImageFileSelect = (event) => {
@ -413,19 +444,34 @@ const handleImageFileSelect = (event) => {
return;
}
//
newImageFile.value = file;
const reader = new FileReader();
reader.onload = (e) => {
const newImageUrl = e.target.result;
updateImageInNode(newImageUrl);
newImagePreview.value = e.target.result;
};
reader.readAsDataURL(file);
};
const updateImageInNode = (newImageUrl) => {
const saveImageChanges = () => {
if (!newImageFile.value || !newImagePreview.value) {
alert('请先选择新图片');
return;
}
const newImageUrl = newImagePreview.value;
if (currentImageElement.value) {
// HTML
currentImageElement.value.src = newImageUrl;
console.log('🖼️ 已更新HTML图片元素');
//
const nodeElement = currentImageElement.value.closest('.me-tpc');
if (nodeElement) {
// MindElixir
mindElixir.value?.render();
}
} else {
// MindElixir
const currentNode = mindElixir.value?.currentNode;
@ -436,6 +482,9 @@ const updateImageInNode = (newImageUrl) => {
currentNode.nodeObj.image.url = newImageUrl;
}
console.log('🖼️ 已更新MindElixir原生图片属性');
//
mindElixir.value?.render();
}
}
@ -443,6 +492,122 @@ const updateImageInNode = (newImageUrl) => {
closeImageEditor();
};
// HTMLMarkdown
const convertHTMLToMarkdown = (html) => {
if (!html) return '';
let markdown = html;
//
markdown = markdown.replace(/<img[^>]+src="([^"]+)"[^>]*alt="([^"]*)"[^>]*>/gi, '![$2]($1)');
markdown = markdown.replace(/<img[^>]+src="([^"]+)"[^>]*>/gi, '![]($1)');
// - 使
markdown = markdown.replace(/<table[^>]*>/gi, '');
markdown = markdown.replace(/<\/table>/gi, '\n');
markdown = markdown.replace(/<tr[^>]*>/gi, '');
markdown = markdown.replace(/<\/tr>/gi, '\n');
markdown = markdown.replace(/<td[^>]*>/gi, '|');
markdown = markdown.replace(/<\/td>/gi, '');
markdown = markdown.replace(/<th[^>]*>/gi, '|');
markdown = markdown.replace(/<\/th>/gi, '');
// HTML
markdown = markdown.replace(/<br\s*\/?>/gi, '\n');
markdown = markdown.replace(/<p[^>]*>/gi, '');
markdown = markdown.replace(/<\/p>/gi, '\n\n');
markdown = markdown.replace(/<div[^>]*>/gi, '');
markdown = markdown.replace(/<\/div>/gi, '\n');
markdown = markdown.replace(/<ul[^>]*>/gi, '');
markdown = markdown.replace(/<\/ul>/gi, '');
markdown = markdown.replace(/<li[^>]*>/gi, '• ');
markdown = markdown.replace(/<\/li>/gi, '\n');
//
markdown = markdown.replace(/\n{3,}/g, '\n\n');
markdown = markdown.trim();
return markdown;
};
const handleFileSelect = (event) => {
const file = event.target.files[0];
if (!file) return;
//
if (!file.type.startsWith('image/')) {
alert('请选择图片文件');
return;
}
// 5MB
if (file.size > 5 * 1024 * 1024) {
alert('图片文件不能超过5MB');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const imageUrl = e.target.result;
const fileName = file.name || 'image';
// Markdown
const textarea = document.getElementById('richTextEditor');
const cursorPos = textarea.selectionStart;
const imageMarkdown = `![${fileName}](${imageUrl})\n`;
const newContent = editorContent.value.slice(0, cursorPos) + imageMarkdown + editorContent.value.slice(cursorPos);
editorContent.value = newContent;
//
setTimeout(() => {
textarea.focus();
textarea.setSelectionRange(cursorPos + imageMarkdown.length, cursorPos + imageMarkdown.length);
}, 0);
};
reader.readAsDataURL(file);
};
const saveRichTextChanges = () => {
if (!currentNode.value || !editorContent.value.trim()) {
alert('请输入内容');
return;
}
// 使markdownMarkdownHTML
const { renderMarkdownToHTML } = require('../utils/markdownRenderer.js');
const htmlContent = renderMarkdownToHTML(editorContent.value);
//
currentNode.value.topic = editorContent.value; // Markdown
currentNode.value.dangerouslySetInnerHTML = htmlContent; // HTML
// DOM
if (currentNodeElement.value) {
currentNodeElement.value.innerHTML = htmlContent;
}
//
mindElixir.value?.bus.fire('operation', {
name: 'finishEdit',
obj: currentNode.value,
origin: currentNode.value.topic || '',
});
//
mindElixir.value?.render();
console.log('✅ 富文本内容已保存');
closeRichTextEditor();
};
// Markdown
const renderMarkdownPreview = (markdown) => {
if (!markdown) return '';
const { renderMarkdownToHTML } = require('../utils/markdownRenderer.js');
return renderMarkdownToHTML(markdown);
};
const onImageLoad = () => {
console.log('✅ 模态框图片加载成功');
//
@ -685,6 +850,19 @@ const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = tr
//
console.log('🔍 初始化Mind Elixir数据:', data);
// dangerouslySetInnerHTML
if (data && data.nodeData) {
const nodeIds = Object.keys(data.nodeData);
console.log('🔍 节点数量:', nodeIds.length);
nodeIds.forEach(nodeId => {
const node = data.nodeData[nodeId];
if (node.dangerouslySetInnerHTML) {
console.log(`🔍 节点 ${nodeId} 有 dangerouslySetInnerHTML:`, node.dangerouslySetInnerHTML.substring(0, 100));
}
});
}
const result = mindElixir.value.init(data);
console.log('✅ Mind Elixir实例创建成功初始化结果:', result);
@ -739,10 +917,10 @@ const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = tr
openImagePreview(imageUrl, altText);
});
//
mindElixir.value.bus.addListener('showImageEditor', (imageUrl, altText, imgElement) => {
console.log('🖼️ 收到图片编辑事件:', { imageUrl, altText });
openImageEditor(imageUrl, altText, imgElement);
//
mindElixir.value.bus.addListener('showRichTextEditor', (nodeObj, nodeElement) => {
console.log('📝 收到富文本编辑事件:', { nodeObj, nodeElement });
openRichTextEditor(nodeObj, nodeElement);
});
// Mind Elixir使markdown
@ -2365,25 +2543,32 @@ const handleNodeDragOperation = async (operation) => {
}
};
//
const processingEditOperations = new Set();
//
const handleEditFinish = async (operation) => {
const operationKey = `${operation.name}-${operation.obj?.id}`;
//
if (processingEditOperations.has(operationKey)) {
console.log("⚠️ 跳过重复的编辑操作:", operationKey);
return;
}
processingEditOperations.add(operationKey);
try {
// console.log(":", operation);
console.log("🔍 处理编辑完成:", operation);
const editedNode = operation.obj; //
// console.log(":", {
// editedNode: editedNode,
// nodeId: editedNode?.id,
// nodeTopic: editedNode?.topic
// });
if (editedNode) {
// console.log(":", {
// nodeId: editedNode.id,
// nodeTopic: editedNode.topic,
// mindmapId: editedNode.mindmapId || editedNode.mindmap_id
// });
console.log("🔍 保存编辑的节点:", {
nodeId: editedNode.id,
nodeTopic: editedNode.topic,
hasDangerouslySetInnerHTML: !!editedNode.dangerouslySetInnerHTML
});
// API
await updateNodeEdit(editedNode);
@ -2393,17 +2578,21 @@ const handleEditFinish = async (operation) => {
} catch (error) {
console.error("处理编辑完成失败:", error);
} finally {
//
processingEditOperations.delete(operationKey);
}
};
//
const updateNodeEdit = async (node) => {
try {
// console.log(":", {
// nodeId: node.id,
// nodeTopic: node.topic,
// currentMindmapId: currentMindmapId.value
// });
console.log("🔍 更新节点编辑:", {
nodeId: node.id,
nodeTopic: node.topic,
dangerouslySetInnerHTML: node.dangerouslySetInnerHTML,
currentMindmapId: currentMindmapId.value
});
// 使ID
if (!currentMindmapId.value) {
@ -2412,11 +2601,16 @@ const updateNodeEdit = async (node) => {
}
// API
const response = await mindmapAPI.updateNode(node.id, {
const updateData = {
newTitle: node.topic,
newDes: node.data?.des || "",
newParentId: node.parentId || node.parent?.id
});
newParentId: node.parentId || node.parent?.id,
newDangerouslySetInnerHTML: node.dangerouslySetInnerHTML || "" //
};
console.log("🔍 发送到后端的更新数据:", updateData);
const response = await mindmapAPI.updateNode(node.id, updateData);
// console.log(":", response);
@ -2912,8 +3106,8 @@ const bindEventListeners = () => {
if (operation.name === 'moveNodeIn' || operation.name === 'moveNodeBefore' || operation.name === 'moveNodeAfter') {
console.log("检测到节点拖拽操作:", operation.name, operation);
handleNodeDragOperation(operation);
} else if (operation.name === 'finishEdit') {
console.log("检测到编辑完成操作:", operation);
} else if (operation.name === 'finishEdit' || operation.name === 'finishEditTable') {
console.log("🔍 检测到编辑完成操作:", operation.name, operation);
handleEditFinish(operation);
}
});
@ -5120,6 +5314,37 @@ const updateMindMapRealtime = async (data, title) => {
color: #666;
}
.new-image-preview {
margin-top: 16px;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
background: #f9f9f9;
}
.new-image-preview h5 {
margin: 0 0 8px 0;
color: #333;
font-size: 13px;
font-weight: 600;
}
.preview-new-image {
max-width: 100%;
max-height: 150px;
height: auto;
border: 1px solid #ddd;
border-radius: 4px;
object-fit: contain;
}
.preview-hint {
margin: 8px 0 0 0;
font-size: 11px;
color: #666;
font-style: italic;
}
.image-editor-footer {
padding: 16px 20px;
border-top: 1px solid #e0e0e0;
@ -5144,5 +5369,174 @@ const updateMindMapRealtime = async (data, title) => {
background: #5a6268;
}
.save-button {
padding: 8px 20px;
background: #660874;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background 0.2s;
}
.save-button:hover {
background: #4d0655;
}
/* 富文本编辑器样式 */
.rich-text-editor-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 10001;
}
.rich-text-editor-content {
background: white;
border-radius: 12px;
width: 95%;
max-width: 1000px;
max-height: 85vh;
overflow: hidden;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
border: 2px solid #660874;
display: flex;
flex-direction: column;
}
.rich-text-editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #e0e0e0;
background: #f5f5f5;
}
.rich-text-editor-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.rich-text-editor-close {
background: none;
border: none;
font-size: 28px;
color: #666;
cursor: pointer;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
}
.rich-text-editor-close:hover {
background: #e0e0e0;
color: #333;
}
.rich-text-editor-body {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.editor-toolbar {
padding: 16px 20px;
border-bottom: 1px solid #e0e0e0;
background: #fafafa;
}
.editor-main {
flex: 1;
display: flex;
min-height: 0;
}
.rich-text-textarea {
flex: 1;
border: none;
outline: none;
padding: 20px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 14px;
line-height: 1.5;
resize: none;
caret-color: #660874;
border-right: 1px solid #e0e0e0;
}
.rich-text-textarea::placeholder {
color: #999;
font-style: italic;
}
.editor-preview {
flex: 1;
padding: 20px;
background: #fafafa;
overflow-y: auto;
border-left: 1px solid #e0e0e0;
}
.editor-preview h5 {
margin: 0 0 12px 0;
color: #333;
font-size: 14px;
font-weight: 600;
}
.preview-content {
font-size: 14px;
line-height: 1.6;
}
.preview-content img {
max-width: 100%;
height: auto;
border-radius: 4px;
margin: 8px 0;
}
.preview-content table {
border-collapse: collapse;
width: 100%;
margin: 12px 0;
}
.preview-content table th,
.preview-content table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.preview-content table th {
background: #f5f5f5;
font-weight: 600;
}
.rich-text-editor-footer {
padding: 16px 20px;
border-top: 1px solid #e0e0e0;
background: #f5f5f5;
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>

View File

@ -55,9 +55,13 @@ export default function (mind: MindElixirInstance) {
const altText = img.alt || img.title || ''
console.log('🖼️ 双击图片节点,准备编辑:', { imageUrl, altText })
console.log('🖼️ 触发showImageEditor事件')
// 触发图片编辑事件
mind.bus.fire('showImageEditor', imageUrl, altText, img)
// 触发富文本编辑事件
const topicElement = target.closest('.me-tpc') as Topic
if (topicElement) {
mind.bus.fire('showRichTextEditor', topicElement.nodeObj, topicElement)
}
return
}
@ -65,26 +69,7 @@ export default function (mind: MindElixirInstance) {
if (isTopic(target)) {
const topic = target as Topic
// 检查节点是否有图片
if (topic.nodeObj?.image) {
const imageUrl = typeof topic.nodeObj.image === 'string' ? topic.nodeObj.image : topic.nodeObj.image.url
console.log('🖼️ 双击包含图片的节点,准备预览:', imageUrl)
mind.bus.fire('showImagePreview', imageUrl, topic.nodeObj.topic || '')
return
}
// 检查节点内容中是否包含图片
const imgInContent = topic.querySelector('img')
if (imgInContent) {
const imageUrl = imgInContent.src
const altText = imgInContent.alt || imgInContent.title || topic.nodeObj?.topic || ''
console.log('🖼️ 双击包含HTML图片的节点准备编辑:', { imageUrl, altText })
mind.bus.fire('showImageEditor', imageUrl, altText, imgInContent)
return
}
// 检查是否是表格节点
// 优先检查是否是表格节点(表格也是富文本内容的一种)
if (topic.nodeObj?.dangerouslySetInnerHTML && topic.innerHTML.includes('<table')) {
console.log('📊 双击表格节点,准备编辑:', topic.nodeObj.topic)
if (mind.editable) {
@ -93,7 +78,14 @@ export default function (mind: MindElixirInstance) {
return
}
// 如果没有图片或表格,则进入编辑模式
// 检查节点是否有图片或富文本内容(但不包括表格)
if (topic.nodeObj?.image || (topic.nodeObj?.dangerouslySetInnerHTML && !topic.innerHTML.includes('<table')) || topic.querySelector('img')) {
console.log('📝 双击富文本节点,准备编辑:', topic.nodeObj?.topic || '')
mind.bus.fire('showRichTextEditor', topic.nodeObj, topic)
return
}
// 如果没有图片或富文本内容,则进入普通编辑模式
if (mind.editable) {
mind.beginEdit(target)
}

View File

@ -49,7 +49,7 @@ export default function (mind: MindElixirInstance, option: true | ContextMenuOpt
const linkBidirectional = createLi('cm-link-bidirectional', lang.linkBidirectional, '')
const summary = createLi('cm-summary', lang.summary, '')
const imagePreview = createLi('cm-image-preview', '预览图片', '')
const imageEdit = createLi('cm-image-edit', '编辑图片', '')
const imageEdit = createLi('cm-rich-text-edit', '编辑内容', '')
const menuUl = document.createElement('ul')
menuUl.className = 'menu-list'
@ -98,18 +98,27 @@ export default function (mind: MindElixirInstance, option: true | ContextMenuOpt
isRoot = false
}
// 检查节点是否有图片,决定是否显示预览图片选项
// 检查节点是否有富文本内容,决定是否显示编辑选项
const topic = target as Topic
const hasImage = topic.nodeObj?.image || topic.querySelector('img')
const hasRichContent = topic.nodeObj?.image || topic.nodeObj?.dangerouslySetInnerHTML || topic.querySelector('img')
if (hasImage) {
console.log('🔍 右键菜单检查富文本内容:', {
hasNodeImage: !!topic.nodeObj?.image,
hasHTMLContent: !!topic.nodeObj?.dangerouslySetInnerHTML,
hasHTMLImage: !!topic.querySelector('img'),
hasRichContent: !!hasRichContent
})
if (hasRichContent) {
imagePreview.style.display = 'block'
imagePreview.className = ''
imageEdit.style.display = 'block'
imageEdit.className = ''
console.log('✅ 显示富文本编辑菜单项')
} else {
imagePreview.style.display = 'none'
imageEdit.style.display = 'none'
console.log('❌ 隐藏富文本编辑菜单项')
}
if (isRoot) {
@ -257,22 +266,8 @@ export default function (mind: MindElixirInstance, option: true | ContextMenuOpt
menuContainer.hidden = true
const target = mind.currentNode as Topic
if (target) {
// 检查节点是否有图片
if (target.nodeObj?.image) {
const imageUrl = typeof target.nodeObj.image === 'string' ? target.nodeObj.image : target.nodeObj.image.url
console.log('🖼️ 右键菜单编辑图片:', imageUrl)
mind.bus.fire('showImageEditor', imageUrl, target.nodeObj.topic || '', null)
} else {
// 检查节点内容中是否包含图片
const imgInContent = target.querySelector('img')
if (imgInContent) {
const imageUrl = imgInContent.src
const altText = imgInContent.alt || imgInContent.title || target.nodeObj?.topic || ''
console.log('🖼️ 右键菜单编辑HTML图片:', { imageUrl, altText })
mind.bus.fire('showImageEditor', imageUrl, altText, imgInContent)
}
}
console.log('📝 右键菜单编辑富文本内容:', target.nodeObj?.topic || '')
mind.bus.fire('showRichTextEditor', target.nodeObj, target)
}
}
return () => {

View File

@ -531,6 +531,13 @@ export const editTableNode = function (this: MindElixirInstance, el: Topic) {
// 直接更新DOM不调用shapeTpc因为shapeTpc会清空innerHTML
el.innerHTML = styledHTML
// 触发操作历史记录,确保数据持久化
this.bus.fire('operation', {
name: 'finishEditTable',
obj: node,
origin: originalHTML,
})
// 添加样式类
el.classList.add('no-image')
el.classList.remove('has-image')

View File

@ -89,7 +89,7 @@ export type EventMap = {
updateArrowDelta: (arrow: Arrow) => void
showContextMenu: (e: MouseEvent) => void
showImagePreview: (imageUrl: string, altText?: string) => void
showImageEditor: (imageUrl: string, altText?: string, imgElement?: HTMLImageElement | null) => void
showRichTextEditor: (nodeObj: NodeObj, nodeElement: HTMLElement) => void
}
export function createBus<T extends Record<string, (...args: any[]) => void> = EventMap>() {