Compare commits

..

No commits in common. "3693899a2b51baee940e6797ae4ee0da0d33847c" and "e3fedcbf0ff6f3a4a9392ebfd4fce1002bd9a282" have entirely different histories.

8 changed files with 139 additions and 1194 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
# 保留topic内容因为可能需要markdown原始内容 # 如果有HTML内容清空topic以避免冲突
# node_data["topic"] = "" node_data["topic"] = ""
# 添加图片信息 # 添加图片信息
if node.image_url: if node.image_url:
@ -359,18 +359,6 @@ 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
@ -386,12 +374,7 @@ def update_node(request):
# 更新新父节点的子节点计数 # 更新新父节点的子节点计数
parent_node.children_count += 1 parent_node.children_count += 1
try: parent_node.save()
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)
@ -406,41 +389,14 @@ 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)
try: old_parent.save()
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:
try: node.updated_at = timezone.now()
from django.db import transaction node.save()
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,14 +56,8 @@ export default function (mind: MindElixirInstance) {
console.log('🖼️ 双击图片节点,准备编辑:', { imageUrl, altText }) console.log('🖼️ 双击图片节点,准备编辑:', { imageUrl, altText })
// 双击图片触发富文本编辑事件(可以编辑图片) // 触发图片编辑事件
const topicElement = target.closest('.me-tpc') as Topic mind.bus.fire('showImageEditor', imageUrl, altText, img)
if (topicElement && topicElement.nodeObj) {
console.log('🖼️ 找到图片所在的节点:', topicElement.nodeObj)
mind.bus.fire('showRichTextEditor', topicElement.nodeObj, topicElement)
} else {
console.warn('🖼️ 未找到图片所在的节点')
}
return return
} }
@ -71,7 +65,26 @@ 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) {
@ -80,23 +93,7 @@ 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-rich-text-edit', '编辑内容', '') const imageEdit = createLi('cm-image-edit', '编辑图片', '')
const menuUl = document.createElement('ul') const menuUl = document.createElement('ul')
menuUl.className = 'menu-list' menuUl.className = 'menu-list'
@ -98,27 +98,18 @@ export default function (mind: MindElixirInstance, option: true | ContextMenuOpt
isRoot = false isRoot = false
} }
// 检查节点是否有富文本内容,决定是否显示编辑选项 // 检查节点是否有图片,决定是否显示预览图片选项
const topic = target as Topic const topic = target as Topic
const hasRichContent = topic.nodeObj?.image || topic.nodeObj?.dangerouslySetInnerHTML || topic.querySelector('img') const hasImage = topic.nodeObj?.image || topic.querySelector('img')
console.log('🔍 右键菜单检查富文本内容:', { if (hasImage) {
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) {
@ -266,8 +257,22 @@ 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 || '') // 检查节点是否有图片
mind.bus.fire('showRichTextEditor', target.nodeObj, 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)
}
}
} }
} }
return () => { return () => {

View File

@ -531,13 +531,6 @@ 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
showRichTextEditor: (nodeObj: NodeObj, nodeElement: HTMLElement) => void showImageEditor: (imageUrl: string, altText?: string, imgElement?: HTMLImageElement | null) => 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,42 +19,11 @@ 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 = hrefStr; let processedUrl = href;
if (hrefStr.includes('cdn-mineru.openxlab.org.cn')) { if (href.includes('cdn-mineru.openxlab.org.cn')) {
// 将外部CDN URL转换为代理URL // 将外部CDN URL转换为代理URL
const urlPath = hrefStr.replace('https://cdn-mineru.openxlab.org.cn', ''); const urlPath = href.replace('https://cdn-mineru.openxlab.org.cn', '');
processedUrl = `/proxy-image${urlPath}`; processedUrl = `/proxy-image${urlPath}`;
} }
@ -62,8 +31,6 @@ 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);" />`;
}; };