实现图片编辑功能,优化图片交互体验

核心功能:
- 双击图片 → 进入编辑模式(替换图片)
- 右键图片 → 显示菜单(预览/编辑选项)
- 图片编辑器支持文件选择和替换
- 支持HTML图片和MindElixir原生图片两种类型

技术实现:
- 修改mouse.ts,双击图片触发showImageEditor事件
- 扩展contextMenu.ts,添加图片编辑菜单项
- 更新pubsub.ts,添加showImageEditor事件类型
- 在MindMap.vue中实现图片编辑器UI和逻辑
- 添加文件选择、格式验证、大小限制(5MB)
- 支持JPG/PNG/GIF格式

UI设计:
- 紫色主题编辑器(与品牌色一致)
- 当前图片预览 + 文件选择界面
- 响应式设计,支持大图片显示
- 优雅的模态框和交互体验
This commit is contained in:
lixinran 2025-10-11 14:19:41 +08:00
parent 1719ad4c8a
commit e3fedcbf0f
4 changed files with 505 additions and 212 deletions

View File

@ -164,6 +164,40 @@
</div> </div>
</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>
<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="file-input-wrapper">
<input
type="file"
@change="handleImageFileSelect"
accept="image/*"
id="imageFileInput"
class="file-input"
/>
<label for="imageFileInput" class="file-input-label">
📁 选择新图片
</label>
</div>
<p class="file-hint">支持 JPGPNGGIF 格式最大 5MB</p>
</div>
</div>
<div class="image-editor-footer">
<button @click="closeImageEditor" class="cancel-button">取消</button>
</div>
</div>
</div>
</div> </div>
</template> </template>
@ -256,6 +290,12 @@ const imagePreviewTitle = ref('');
const imagePreviewLoading = ref(false); const imagePreviewLoading = ref(false);
const imagePreviewError = ref(''); const imagePreviewError = ref('');
//
const showImageEditor = ref(false);
const imageEditorUrl = ref('');
const imageEditorTitle = ref('');
const currentImageElement = ref(null);
@ -340,6 +380,69 @@ const closeImagePreview = () => {
imagePreviewError.value = ''; imagePreviewError.value = '';
}; };
//
const openImageEditor = (imageUrl, altText, imgElement) => {
console.log('🖼️ 打开图片编辑器:', { imageUrl, altText });
imageEditorUrl.value = imageUrl;
imageEditorTitle.value = altText || '编辑图片';
currentImageElement.value = imgElement;
showImageEditor.value = true;
};
const closeImageEditor = () => {
showImageEditor.value = false;
imageEditorUrl.value = '';
imageEditorTitle.value = '';
currentImageElement.value = null;
};
const handleImageFileSelect = (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 newImageUrl = e.target.result;
updateImageInNode(newImageUrl);
};
reader.readAsDataURL(file);
};
const updateImageInNode = (newImageUrl) => {
if (currentImageElement.value) {
// HTML
currentImageElement.value.src = newImageUrl;
console.log('🖼️ 已更新HTML图片元素');
} else {
// MindElixir
const currentNode = mindElixir.value?.currentNode;
if (currentNode && currentNode.nodeObj) {
if (typeof currentNode.nodeObj.image === 'string') {
currentNode.nodeObj.image = newImageUrl;
} else if (currentNode.nodeObj.image && typeof currentNode.nodeObj.image === 'object') {
currentNode.nodeObj.image.url = newImageUrl;
}
console.log('🖼️ 已更新MindElixir原生图片属性');
}
}
//
closeImageEditor();
};
const onImageLoad = () => { const onImageLoad = () => {
console.log('✅ 模态框图片加载成功'); console.log('✅ 模态框图片加载成功');
// //
@ -636,6 +739,12 @@ const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = tr
openImagePreview(imageUrl, altText); openImagePreview(imageUrl, altText);
}); });
//
mindElixir.value.bus.addListener('showImageEditor', (imageUrl, altText, imgElement) => {
console.log('🖼️ 收到图片编辑事件:', { imageUrl, altText });
openImageEditor(imageUrl, altText, imgElement);
});
// Mind Elixir使markdown // Mind Elixir使markdown
} else { } else {
@ -724,6 +833,12 @@ const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = tr
openImagePreview(imageUrl, altText); openImagePreview(imageUrl, altText);
}); });
//
mindElixir.value.bus.addListener('showImageEditor', (imageUrl, altText, imgElement) => {
console.log('🖼️ 收到图片编辑事件(延迟创建):', { imageUrl, altText });
openImageEditor(imageUrl, altText, imgElement);
});
// //
setTimeout(() => { setTimeout(() => {
// Mind Elixir使markdown // Mind Elixir使markdown
@ -4881,5 +4996,153 @@ const updateMindMapRealtime = async (data, title) => {
background: #7d0a8e; background: #7d0a8e;
} }
/* 图片编辑器模态框样式 */
.image-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;
}
.image-editor-content {
background: white;
border-radius: 12px;
width: 600px;
max-width: 90vw;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
border: 2px solid #660874;
}
.image-editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #e0e0e0;
background: #f5f5f5;
}
.image-editor-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.image-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;
}
.image-editor-close:hover {
background: #e0e0e0;
color: #333;
}
.image-editor-body {
padding: 20px;
max-height: 60vh;
overflow-y: auto;
}
.current-image-section {
margin-bottom: 24px;
}
.current-image-section h4 {
margin: 0 0 12px 0;
color: #333;
font-size: 14px;
font-weight: 600;
}
.current-image {
max-width: 100%;
max-height: 200px;
height: auto;
border: 1px solid #ddd;
border-radius: 6px;
object-fit: contain;
}
.replace-image-section h4 {
margin: 0 0 12px 0;
color: #333;
font-size: 14px;
font-weight: 600;
}
.file-input-wrapper {
margin-bottom: 8px;
}
.file-input {
display: none;
}
.file-input-label {
display: inline-block;
padding: 10px 20px;
background: #660874;
color: white;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background 0.2s;
border: none;
}
.file-input-label:hover {
background: #4d0655;
}
.file-hint {
margin: 0;
font-size: 12px;
color: #666;
}
.image-editor-footer {
padding: 16px 20px;
border-top: 1px solid #e0e0e0;
background: #f9f9f9;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.cancel-button {
padding: 8px 20px;
background: #6c757d;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
}
.cancel-button:hover {
background: #5a6268;
}
</style> </style>

View File

@ -1,212 +1,212 @@
import { handleZoom } from './plugin/keypress' import { handleZoom } from './plugin/keypress'
import type { SummarySvgGroup } from './summary' import type { SummarySvgGroup } from './summary'
import type { Expander, CustomSvg, Topic } from './types/dom' import type { Expander, CustomSvg, Topic } from './types/dom'
import type { MindElixirInstance } from './types/index' import type { MindElixirInstance } from './types/index'
import { isTopic, on } from './utils' import { isTopic, on } from './utils'
export default function (mind: MindElixirInstance) { export default function (mind: MindElixirInstance) {
const { dragMoveHelper } = mind const { dragMoveHelper } = mind
const handleClick = (e: MouseEvent) => { const handleClick = (e: MouseEvent) => {
console.log('handleClick', e) console.log('handleClick', e)
// Only handle primary button clicks // Only handle primary button clicks
if (e.button !== 0) return if (e.button !== 0) return
if (mind.helper1?.moved) { if (mind.helper1?.moved) {
mind.helper1.clear() mind.helper1.clear()
return return
} }
if (mind.helper2?.moved) { if (mind.helper2?.moved) {
mind.helper2.clear() mind.helper2.clear()
return return
} }
if (dragMoveHelper.moved) { if (dragMoveHelper.moved) {
dragMoveHelper.clear() dragMoveHelper.clear()
return return
} }
const target = e.target as HTMLElement const target = e.target as HTMLElement
if (target.tagName === 'ME-EPD') { if (target.tagName === 'ME-EPD') {
if (e.ctrlKey || e.metaKey) { if (e.ctrlKey || e.metaKey) {
mind.expandNodeAll((target as Expander).previousSibling) mind.expandNodeAll((target as Expander).previousSibling)
} else { } else {
mind.expandNode((target as Expander).previousSibling) mind.expandNode((target as Expander).previousSibling)
} }
} else if (target.tagName === 'ME-TPC' && mind.currentNodes.length > 1) { } else if (target.tagName === 'ME-TPC' && mind.currentNodes.length > 1) {
// This is a bit complex, intertwined with selection and nodeDraggable // This is a bit complex, intertwined with selection and nodeDraggable
// The main conflict is between multi-node dragging and selecting a single node when multiple nodes are already selected // The main conflict is between multi-node dragging and selecting a single node when multiple nodes are already selected
mind.selectNode(target as Topic) mind.selectNode(target as Topic)
} else if (!mind.editable) { } else if (!mind.editable) {
return return
} }
const trySvg = target.parentElement?.parentElement as unknown as SVGElement const trySvg = target.parentElement?.parentElement as unknown as SVGElement
if (trySvg.getAttribute('class') === 'topiclinks') { if (trySvg.getAttribute('class') === 'topiclinks') {
mind.selectArrow(target.parentElement as unknown as CustomSvg) mind.selectArrow(target.parentElement as unknown as CustomSvg)
} else if (trySvg.getAttribute('class') === 'summary') { } else if (trySvg.getAttribute('class') === 'summary') {
mind.selectSummary(target.parentElement as unknown as SummarySvgGroup) mind.selectSummary(target.parentElement as unknown as SummarySvgGroup)
} }
} }
const handleDblClick = (e: MouseEvent) => { const handleDblClick = (e: MouseEvent) => {
const target = e.target as HTMLElement const target = e.target as HTMLElement
// 检查是否双击了图片 // 检查是否双击了图片
if (target.tagName === 'IMG') { if (target.tagName === 'IMG') {
const img = target as HTMLImageElement const img = target as HTMLImageElement
const imageUrl = img.src const imageUrl = img.src
const altText = img.alt || img.title || '' const altText = img.alt || img.title || ''
console.log('🖼️ 双击图片节点,准备预览:', { imageUrl, altText }) console.log('🖼️ 双击图片节点,准备编辑:', { imageUrl, altText })
// 触发图片预览事件 // 触发图片编辑事件
mind.bus.fire('showImagePreview', imageUrl, altText) mind.bus.fire('showImageEditor', imageUrl, altText, img)
return return
} }
// 检查是否双击了包含图片的节点 // 检查是否双击了包含图片的节点
if (isTopic(target)) { if (isTopic(target)) {
const topic = target as Topic const topic = target as Topic
// 检查节点是否有图片 // 检查节点是否有图片
if (topic.nodeObj?.image) { if (topic.nodeObj?.image) {
const imageUrl = typeof topic.nodeObj.image === 'string' ? topic.nodeObj.image : topic.nodeObj.image.url const imageUrl = typeof topic.nodeObj.image === 'string' ? topic.nodeObj.image : topic.nodeObj.image.url
console.log('🖼️ 双击包含图片的节点,准备预览:', imageUrl) console.log('🖼️ 双击包含图片的节点,准备预览:', imageUrl)
mind.bus.fire('showImagePreview', imageUrl, topic.nodeObj.topic || '') mind.bus.fire('showImagePreview', imageUrl, topic.nodeObj.topic || '')
return return
} }
// 检查节点内容中是否包含图片 // 检查节点内容中是否包含图片
const imgInContent = topic.querySelector('img') const imgInContent = topic.querySelector('img')
if (imgInContent) { if (imgInContent) {
const imageUrl = imgInContent.src const imageUrl = imgInContent.src
const altText = imgInContent.alt || imgInContent.title || topic.nodeObj?.topic || '' const altText = imgInContent.alt || imgInContent.title || topic.nodeObj?.topic || ''
console.log('🖼️ 双击包含HTML图片的节点准备预览:', { imageUrl, altText }) console.log('🖼️ 双击包含HTML图片的节点准备编辑:', { imageUrl, altText })
mind.bus.fire('showImagePreview', imageUrl, altText) mind.bus.fire('showImageEditor', imageUrl, altText, imgInContent)
return 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) {
mind.beginEdit(target) mind.beginEdit(target)
} }
return return
} }
// 如果没有图片或表格,则进入编辑模式 // 如果没有图片或表格,则进入编辑模式
if (mind.editable) { if (mind.editable) {
mind.beginEdit(target) mind.beginEdit(target)
} }
} }
// 处理其他双击事件 // 处理其他双击事件
if (mind.editable) { if (mind.editable) {
const trySvg = target.parentElement?.parentElement as unknown as SVGElement const trySvg = target.parentElement?.parentElement as unknown as SVGElement
if (trySvg.getAttribute('class') === 'topiclinks') { if (trySvg.getAttribute('class') === 'topiclinks') {
mind.editArrowLabel(target.parentElement as unknown as CustomSvg) mind.editArrowLabel(target.parentElement as unknown as CustomSvg)
} else if (trySvg.getAttribute('class') === 'summary') { } else if (trySvg.getAttribute('class') === 'summary') {
mind.editSummary(target.parentElement as unknown as SummarySvgGroup) mind.editSummary(target.parentElement as unknown as SummarySvgGroup)
} }
} }
} }
let lastTap = 0 let lastTap = 0
const handleTouchDblClick = (e: PointerEvent) => { const handleTouchDblClick = (e: PointerEvent) => {
if (e.pointerType === 'mouse') return if (e.pointerType === 'mouse') return
const currentTime = new Date().getTime() const currentTime = new Date().getTime()
const tapLength = currentTime - lastTap const tapLength = currentTime - lastTap
console.log('tapLength', tapLength) console.log('tapLength', tapLength)
if (tapLength < 300 && tapLength > 0) { if (tapLength < 300 && tapLength > 0) {
handleDblClick(e) handleDblClick(e)
} }
lastTap = currentTime lastTap = currentTime
} }
const handlePointerDown = (e: PointerEvent) => { const handlePointerDown = (e: PointerEvent) => {
dragMoveHelper.moved = false dragMoveHelper.moved = false
const mouseMoveButton = mind.mouseSelectionButton === 0 ? 2 : 0 const mouseMoveButton = mind.mouseSelectionButton === 0 ? 2 : 0
if (e.button !== mouseMoveButton && e.pointerType === 'mouse') return if (e.button !== mouseMoveButton && e.pointerType === 'mouse') return
// Store initial position for movement calculation // Store initial position for movement calculation
dragMoveHelper.x = e.clientX dragMoveHelper.x = e.clientX
dragMoveHelper.y = e.clientY dragMoveHelper.y = e.clientY
const target = e.target as HTMLElement const target = e.target as HTMLElement
if (target.className === 'circle') return if (target.className === 'circle') return
if (target.contentEditable !== 'plaintext-only') { if (target.contentEditable !== 'plaintext-only') {
dragMoveHelper.mousedown = true dragMoveHelper.mousedown = true
// Capture pointer to ensure we receive all pointer events even if pointer moves outside the element // Capture pointer to ensure we receive all pointer events even if pointer moves outside the element
target.setPointerCapture(e.pointerId) target.setPointerCapture(e.pointerId)
} }
} }
const handlePointerMove = (e: PointerEvent) => { const handlePointerMove = (e: PointerEvent) => {
// click trigger pointermove in windows chrome // click trigger pointermove in windows chrome
if ((e.target as HTMLElement).contentEditable !== 'plaintext-only') { if ((e.target as HTMLElement).contentEditable !== 'plaintext-only') {
// drag and move the map // drag and move the map
// Calculate movement delta manually since pointer events don't have movementX/Y // Calculate movement delta manually since pointer events don't have movementX/Y
const movementX = e.clientX - dragMoveHelper.x const movementX = e.clientX - dragMoveHelper.x
const movementY = e.clientY - dragMoveHelper.y const movementY = e.clientY - dragMoveHelper.y
dragMoveHelper.onMove(movementX, movementY) dragMoveHelper.onMove(movementX, movementY)
} }
dragMoveHelper.x = e.clientX dragMoveHelper.x = e.clientX
dragMoveHelper.y = e.clientY dragMoveHelper.y = e.clientY
} }
const handlePointerUp = (e: PointerEvent) => { const handlePointerUp = (e: PointerEvent) => {
const mouseMoveButton = mind.mouseSelectionButton === 0 ? 2 : 0 const mouseMoveButton = mind.mouseSelectionButton === 0 ? 2 : 0
if (e.button !== mouseMoveButton && e.pointerType === 'mouse') return if (e.button !== mouseMoveButton && e.pointerType === 'mouse') return
const target = e.target as HTMLElement const target = e.target as HTMLElement
// Release pointer capture // Release pointer capture
if (target.hasPointerCapture && target.hasPointerCapture(e.pointerId)) { if (target.hasPointerCapture && target.hasPointerCapture(e.pointerId)) {
target.releasePointerCapture(e.pointerId) target.releasePointerCapture(e.pointerId)
} }
dragMoveHelper.clear() dragMoveHelper.clear()
} }
const handleContextMenu = (e: MouseEvent) => { const handleContextMenu = (e: MouseEvent) => {
console.log('handleContextMenu', e) console.log('handleContextMenu', e)
e.preventDefault() e.preventDefault()
// Only handle right-click for context menu // Only handle right-click for context menu
if (e.button !== 2) return if (e.button !== 2) return
if (!mind.editable) return if (!mind.editable) return
const target = e.target as HTMLElement const target = e.target as HTMLElement
if (isTopic(target) && !target.classList.contains('selected')) { if (isTopic(target) && !target.classList.contains('selected')) {
mind.selectNode(target) mind.selectNode(target)
} }
setTimeout(() => { setTimeout(() => {
// delay to avoid conflict with click event on Mac // delay to avoid conflict with click event on Mac
if (mind.dragMoveHelper.moved) return if (mind.dragMoveHelper.moved) return
mind.bus.fire('showContextMenu', e) mind.bus.fire('showContextMenu', e)
}, 200) }, 200)
} }
const handleWheel = (e: WheelEvent) => { const handleWheel = (e: WheelEvent) => {
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()
if (e.ctrlKey || e.metaKey) { if (e.ctrlKey || e.metaKey) {
if (e.deltaY < 0) handleZoom(mind, 'in', mind.dragMoveHelper) if (e.deltaY < 0) handleZoom(mind, 'in', mind.dragMoveHelper)
else if (mind.scaleVal - mind.scaleSensitivity > 0) handleZoom(mind, 'out', mind.dragMoveHelper) else if (mind.scaleVal - mind.scaleSensitivity > 0) handleZoom(mind, 'out', mind.dragMoveHelper)
} else if (e.shiftKey) { } else if (e.shiftKey) {
mind.move(-e.deltaY, 0) mind.move(-e.deltaY, 0)
} else { } else {
mind.move(-e.deltaX, -e.deltaY) mind.move(-e.deltaX, -e.deltaY)
} }
} }
const { container } = mind const { container } = mind
const off = on([ const off = on([
{ dom: container, evt: 'pointerdown', func: handlePointerDown }, { dom: container, evt: 'pointerdown', func: handlePointerDown },
{ dom: container, evt: 'pointermove', func: handlePointerMove }, { dom: container, evt: 'pointermove', func: handlePointerMove },
{ dom: container, evt: 'pointerup', func: handlePointerUp }, { dom: container, evt: 'pointerup', func: handlePointerUp },
{ dom: container, evt: 'pointerup', func: handleTouchDblClick }, { dom: container, evt: 'pointerup', func: handleTouchDblClick },
{ dom: container, evt: 'click', func: handleClick }, { dom: container, evt: 'click', func: handleClick },
{ dom: container, evt: 'dblclick', func: handleDblClick }, { dom: container, evt: 'dblclick', func: handleDblClick },
{ dom: container, evt: 'contextmenu', func: handleContextMenu }, { dom: container, evt: 'contextmenu', func: handleContextMenu },
{ dom: container, evt: 'wheel', func: typeof mind.handleWheel === 'function' ? mind.handleWheel : handleWheel }, { dom: container, evt: 'wheel', func: typeof mind.handleWheel === 'function' ? mind.handleWheel : handleWheel },
]) ])
return off return off
} }

View File

@ -49,6 +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 menuUl = document.createElement('ul') const menuUl = document.createElement('ul')
menuUl.className = 'menu-list' menuUl.className = 'menu-list'
@ -63,6 +64,8 @@ export default function (mind: MindElixirInstance, option: true | ContextMenuOpt
menuUl.appendChild(up) menuUl.appendChild(up)
menuUl.appendChild(down) menuUl.appendChild(down)
menuUl.appendChild(summary) menuUl.appendChild(summary)
menuUl.appendChild(imagePreview)
menuUl.appendChild(imageEdit)
if (option.link) { if (option.link) {
menuUl.appendChild(link) menuUl.appendChild(link)
menuUl.appendChild(linkBidirectional) menuUl.appendChild(linkBidirectional)
@ -102,8 +105,11 @@ export default function (mind: MindElixirInstance, option: true | ContextMenuOpt
if (hasImage) { if (hasImage) {
imagePreview.style.display = 'block' imagePreview.style.display = 'block'
imagePreview.className = '' imagePreview.className = ''
imageEdit.style.display = 'block'
imageEdit.className = ''
} else { } else {
imagePreview.style.display = 'none' imagePreview.style.display = 'none'
imageEdit.style.display = 'none'
} }
if (isRoot) { if (isRoot) {
@ -246,6 +252,29 @@ export default function (mind: MindElixirInstance, option: true | ContextMenuOpt
} }
} }
} }
imageEdit.onclick = () => {
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)
}
}
}
}
return () => { return () => {
// maybe useful? // maybe useful?
add_child.onclick = null add_child.onclick = null

View File

@ -89,6 +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
} }
export function createBus<T extends Record<string, (...args: any[]) => void> = EventMap>() { export function createBus<T extends Record<string, (...args: any[]) => void> = EventMap>() {