Compare commits

..

3 Commits

Author SHA1 Message Date
lixinran 3693899a2b 修复图片渲染器href属性检查逻辑,支持href.href属性
- 修复markdownRenderer.js中图片渲染器的href属性检查
- 添加对href.href属性的支持,解决src='[object Object]'错误
- 修复saveRichTextChanges函数中的async/await语法错误
- 优化模态框标题显示逻辑,确保标题和内容区域文字一致
- 修复节点标题同步问题,直接更新MindElixir的topic字段
- 智能更新topic字段,保留图片Markdown语法的同时更新标题文本
2025-10-11 17:41:12 +08:00
lixinran 4f072de2ee 🐛 修复图片节点双击预览功能
关键修复:
- 恢复双击图片节点的预览功能
- 双击图片元素触发showImagePreview事件
- 双击包含原生图片的节点触发showImagePreview事件
- 保持富文本节点的编辑功能

交互逻辑:
- 双击图片 → 预览图片
- 双击包含原生图片的节点 → 预览图片
- 双击富文本节点 → 编辑内容
- 双击表格节点 → 编辑表格
- 右键节点 → 显示菜单(包含预览和编辑选项)

技术实现:
- 在mouse.ts中区分不同类型的双击事件
- 优先处理图片预览,再处理富文本编辑
- 保持原有的表格编辑功能

现在图片预览功能已经恢复正常。
2025-10-11 15:09:29 +08:00
lixinran 642b12c217 🐛 修复编辑操作重复调用导致的双重通知问题
关键修复:
- 添加防重复处理机制,避免同一个编辑操作被多次处理
- 合并finishEdit和finishEditTable事件监听器
- 使用Set记录正在处理的操作,防止重复调用
- 添加详细的调试日志帮助追踪问题

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

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

现在用户只会看到一个正确的保存成功通知。
2025-10-11 15:06:38 +08:00
8 changed files with 1197 additions and 142 deletions

Binary file not shown.

View File

@ -54,8 +54,8 @@ def convert_to_mindelixir_format(mindmap, nodes):
# 添加HTML内容如果存在 # 添加HTML内容如果存在
if node.html_content: if node.html_content:
node_data["dangerouslySetInnerHTML"] = node.html_content node_data["dangerouslySetInnerHTML"] = node.html_content
# 如果有HTML内容清空topic以避免冲突 # 保留topic内容因为可能需要markdown原始内容
node_data["topic"] = "" # node_data["topic"] = ""
# 添加图片信息 # 添加图片信息
if node.image_url: if node.image_url:
@ -359,6 +359,18 @@ def update_node(request):
node.desc = body.get('newDes') or '' node.desc = body.get('newDes') or ''
updated_fields.append('des') 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: if 'newParentId' in body:
new_parent_id = body.get('newParentId') new_parent_id = body.get('newParentId')
old_parent_id = node.parent_id old_parent_id = node.parent_id
@ -374,7 +386,12 @@ def update_node(request):
# 更新新父节点的子节点计数 # 更新新父节点的子节点计数
parent_node.children_count += 1 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: except Node.DoesNotExist:
return Response({'detail': 'parent node not found'}, status=404) return Response({'detail': 'parent node not found'}, status=404)
@ -389,14 +406,41 @@ def update_node(request):
try: try:
old_parent = Node.objects.get(id=old_parent_id, deleted=False, mindmap=node.mindmap) 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.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: except Node.DoesNotExist:
pass # 原父节点可能已被删除 pass # 原父节点可能已被删除
# 只有在有字段更新时才更新时间戳 # 只有在有字段更新时才更新时间戳
if updated_fields: if updated_fields:
node.updated_at = timezone.now() try:
node.save() 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({ return Response({
'success': True, 'success': True,

File diff suppressed because it is too large Load Diff

View File

@ -56,8 +56,14 @@ export default function (mind: MindElixirInstance) {
console.log('🖼️ 双击图片节点,准备编辑:', { imageUrl, altText }) console.log('🖼️ 双击图片节点,准备编辑:', { imageUrl, altText })
// 触发图片编辑事件 // 双击图片触发富文本编辑事件(可以编辑图片)
mind.bus.fire('showImageEditor', imageUrl, altText, img) const topicElement = target.closest('.me-tpc') as Topic
if (topicElement && topicElement.nodeObj) {
console.log('🖼️ 找到图片所在的节点:', topicElement.nodeObj)
mind.bus.fire('showRichTextEditor', topicElement.nodeObj, topicElement)
} else {
console.warn('🖼️ 未找到图片所在的节点')
}
return return
} }
@ -65,26 +71,7 @@ export default function (mind: MindElixirInstance) {
if (isTopic(target)) { if (isTopic(target)) {
const topic = target as Topic 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')) { if (topic.nodeObj?.dangerouslySetInnerHTML && topic.innerHTML.includes('<table')) {
console.log('📊 双击表格节点,准备编辑:', topic.nodeObj.topic) console.log('📊 双击表格节点,准备编辑:', topic.nodeObj.topic)
if (mind.editable) { if (mind.editable) {
@ -93,7 +80,23 @@ export default function (mind: MindElixirInstance) {
return return
} }
// 如果没有图片或表格,则进入编辑模式 // 检查节点是否有MindElixir原生图片
if (topic.nodeObj?.image) {
const imageUrl = typeof topic.nodeObj.image === 'string' ? topic.nodeObj.image : topic.nodeObj.image.url
console.log('🖼️ 双击包含原生图片的节点,准备编辑:', imageUrl)
// 双击包含原生图片的节点触发富文本编辑事件
mind.bus.fire('showRichTextEditor', topic.nodeObj, topic)
return
}
// 检查节点是否有富文本内容(但不包括表格)
if (topic.nodeObj?.dangerouslySetInnerHTML && !topic.innerHTML.includes('<table')) {
console.log('📝 双击富文本节点,准备编辑:', topic.nodeObj?.topic || '')
mind.bus.fire('showRichTextEditor', topic.nodeObj, topic)
return
}
// 如果没有图片或富文本内容,则进入普通编辑模式
if (mind.editable) { if (mind.editable) {
mind.beginEdit(target) 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 linkBidirectional = createLi('cm-link-bidirectional', lang.linkBidirectional, '')
const summary = createLi('cm-summary', lang.summary, '') const summary = createLi('cm-summary', lang.summary, '')
const imagePreview = createLi('cm-image-preview', '预览图片', '') const imagePreview = createLi('cm-image-preview', '预览图片', '')
const imageEdit = createLi('cm-image-edit', '编辑图片', '') const imageEdit = createLi('cm-rich-text-edit', '编辑内容', '')
const menuUl = document.createElement('ul') const menuUl = document.createElement('ul')
menuUl.className = 'menu-list' menuUl.className = 'menu-list'
@ -98,18 +98,27 @@ export default function (mind: MindElixirInstance, option: true | ContextMenuOpt
isRoot = false isRoot = false
} }
// 检查节点是否有图片,决定是否显示预览图片选项 // 检查节点是否有富文本内容,决定是否显示编辑选项
const topic = target as Topic 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.style.display = 'block'
imagePreview.className = '' imagePreview.className = ''
imageEdit.style.display = 'block' imageEdit.style.display = 'block'
imageEdit.className = '' imageEdit.className = ''
console.log('✅ 显示富文本编辑菜单项')
} else { } else {
imagePreview.style.display = 'none' imagePreview.style.display = 'none'
imageEdit.style.display = 'none' imageEdit.style.display = 'none'
console.log('❌ 隐藏富文本编辑菜单项')
} }
if (isRoot) { if (isRoot) {
@ -257,22 +266,8 @@ export default function (mind: MindElixirInstance, option: true | ContextMenuOpt
menuContainer.hidden = true menuContainer.hidden = true
const target = mind.currentNode as Topic const target = mind.currentNode as Topic
if (target) { if (target) {
// 检查节点是否有图片 console.log('📝 右键菜单编辑富文本内容:', target.nodeObj?.topic || '')
if (target.nodeObj?.image) { mind.bus.fire('showRichTextEditor', target.nodeObj, target)
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)
}
}
} }
} }
return () => { return () => {

View File

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

View File

@ -89,7 +89,7 @@ export type EventMap = {
updateArrowDelta: (arrow: Arrow) => void updateArrowDelta: (arrow: Arrow) => void
showContextMenu: (e: MouseEvent) => void showContextMenu: (e: MouseEvent) => void
showImagePreview: (imageUrl: string, altText?: string) => 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>() { export function createBus<T extends Record<string, (...args: any[]) => void> = EventMap>() {

View File

@ -19,11 +19,42 @@ const renderer = new marked.Renderer();
// 自定义图片渲染器 // 自定义图片渲染器
renderer.image = function(href, title, text) { renderer.image = function(href, title, text) {
console.log('🖼️ 图片渲染器被调用:', { href, title, text, hrefType: typeof href });
// 确保href是字符串类型处理各种异常情况
let hrefStr = '';
if (typeof href === 'string') {
hrefStr = href;
} else if (href === null || href === undefined) {
console.warn('图片href为null或undefined');
return `<div class="markdown-error">图片链接为空</div>`;
} else if (typeof href === 'object') {
// 如果是对象尝试提取URL
if (href.href) {
hrefStr = String(href.href);
} else if (href.url) {
hrefStr = String(href.url);
} else if (href.src) {
hrefStr = String(href.src);
} else {
console.warn('图片href是对象但没有href、url或src属性:', href);
return `<div class="markdown-error">图片链接格式错误</div>`;
}
} else if (typeof href.toString === 'function') {
hrefStr = href.toString();
} else {
console.warn('图片href无法转换为字符串:', typeof href, href);
return `<div class="markdown-error">图片链接格式错误</div>`;
}
console.log('🖼️ 处理后的hrefStr:', hrefStr);
// 处理图片URL确保能正确显示 // 处理图片URL确保能正确显示
let processedUrl = href; let processedUrl = hrefStr;
if (href.includes('cdn-mineru.openxlab.org.cn')) { if (hrefStr.includes('cdn-mineru.openxlab.org.cn')) {
// 将外部CDN URL转换为代理URL // 将外部CDN URL转换为代理URL
const urlPath = href.replace('https://cdn-mineru.openxlab.org.cn', ''); const urlPath = hrefStr.replace('https://cdn-mineru.openxlab.org.cn', '');
processedUrl = `/proxy-image${urlPath}`; processedUrl = `/proxy-image${urlPath}`;
} }
@ -31,6 +62,8 @@ renderer.image = function(href, title, text) {
const altText = text || '图片'; const altText = text || '图片';
const titleAttr = title ? ` title="${title}"` : ''; const titleAttr = title ? ` title="${title}"` : '';
console.log('🖼️ 最终生成的图片HTML:', `<img src="${processedUrl}" alt="${altText}"${titleAttr} class="markdown-image" />`);
return `<img src="${processedUrl}" alt="${altText}"${titleAttr} class="markdown-image" style="max-width: 200px; max-height: 150px; object-fit: contain; display: block; margin: 4px auto; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);" />`; return `<img src="${processedUrl}" alt="${altText}"${titleAttr} class="markdown-image" style="max-width: 200px; max-height: 150px; object-fit: contain; display: block; margin: 4px auto; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);" />`;
}; };