🐛 修复编辑操作重复调用导致的双重通知问题
关键修复:
- 添加防重复处理机制,避免同一个编辑操作被多次处理
- 合并finishEdit和finishEditTable事件监听器
- 使用Set记录正在处理的操作,防止重复调用
- 添加详细的调试日志帮助追踪问题
问题根因:
- finishEdit和finishEditTable事件都被监听,导致重复调用
- 缺乏防重复处理机制
- 用户看到成功和失败两个通知
技术实现:
- 使用operationKey = `${operation.name}-${operation.obj?.id}` 唯一标识操作
- 在processingEditOperations Set中记录正在处理的操作
- 处理完成后自动清理标记
- 合并重复的事件监听器逻辑
现在用户只会看到一个正确的保存成功通知。
This commit is contained in:
parent
e3fedcbf0f
commit
642b12c217
Binary file not shown.
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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">支持 JPG、PNG、GIF 格式,最大 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 || '编辑节点内容';
|
||||
|
||||
// 将HTML内容转换为Markdown格式供编辑
|
||||
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();
|
||||
};
|
||||
|
||||
// HTML转Markdown的简单转换函数
|
||||
const convertHTMLToMarkdown = (html) => {
|
||||
if (!html) return '';
|
||||
|
||||
let markdown = html;
|
||||
|
||||
// 转换图片标签
|
||||
markdown = markdown.replace(/<img[^>]+src="([^"]+)"[^>]*alt="([^"]*)"[^>]*>/gi, '');
|
||||
markdown = markdown.replace(/<img[^>]+src="([^"]+)"[^>]*>/gi, '');
|
||||
|
||||
// 转换表格标签 - 使用更准确的转换逻辑
|
||||
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 = `\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;
|
||||
}
|
||||
|
||||
// 使用markdown渲染引擎将Markdown转换为HTML
|
||||
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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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>() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue