From 6dcb29f218f315fb844808d215201c567ac596b5 Mon Sep 17 00:00:00 2001 From: Achuan-2 Date: Wed, 26 Nov 2025 11:02:27 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=8B=96=E5=8A=A8=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E5=A4=A7=E5=B0=8F=E6=94=AF=E6=8C=81=E5=90=B8=E9=99=84=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple-mind-map/src/plugins/NodeImgAdjust.js | 51 ++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/simple-mind-map/src/plugins/NodeImgAdjust.js b/simple-mind-map/src/plugins/NodeImgAdjust.js index 9045ade1f..02b7a1cb2 100644 --- a/simple-mind-map/src/plugins/NodeImgAdjust.js +++ b/simple-mind-map/src/plugins/NodeImgAdjust.js @@ -7,6 +7,7 @@ class NodeImgAdjust { constructor({ mindMap }) { this.mindMap = mindMap this.handleEl = null // 自定义元素,用来渲染临时图片、调整按钮 + this.resizeBtnEl = null // 调整按钮元素 this.isShowHandleEl = false // 自定义元素是否在显示中 this.node = null // 当前节点实例 this.img = null // 当前节点的图片节点 @@ -21,6 +22,7 @@ class NodeImgAdjust { this.currentImgWidth = 0 // 当前拖拽实时图片的大小 this.currentImgHeight = 0 this.isAdjusted = false // 是否是拖拽结束后的渲染期间 + this.referenceWidths = [] // 参考宽度列表 this.bindEvent() } @@ -164,6 +166,7 @@ class NodeImgAdjust { cursor: nwse-resize; ` btnEl.className = 'node-image-resize' + this.resizeBtnEl = btnEl // 给按钮元素绑定事件 btnEl.addEventListener('mouseenter', () => { // 移入按钮,会触发节点图片的移出事件,所以需要再次显示按钮 @@ -240,6 +243,26 @@ class NodeImgAdjust { this.mousedownOffset.y = e.clientY - this.rect.y2 // 将节点图片渲染到自定义元素上 this.handleEl.style.backgroundImage = `url(${this.node.getData('image')})` + // 收集参考宽度 + this.referenceWidths = this.collectImageWidths() + } + + // 收集所有带图片节点的宽度 + collectImageWidths() { + const widths = new Set() + const walk = node => { + if (node.getData('image') && node !== this.node) { + const { width } = node.getData('imageSize') || {} + if (width) widths.add(width) + } + if (node.children) { + node.children.forEach(walk) + } + } + if (this.mindMap.renderer.root) { + walk(this.mindMap.renderer.root) + } + return Array.from(widths) } // 鼠标移动 @@ -281,6 +304,30 @@ class NodeImgAdjust { // 计算当前拖拽位置对应的图片的实时大小 let newWidth = Math.abs(e.clientX - this.rect.x - this.mousedownOffset.x) let newHeight = Math.abs(e.clientY - this.rect.y - this.mousedownOffset.y) + + // 吸附逻辑 + const SNAP_THRESHOLD = 5 + let snapped = false + for (const width of this.referenceWidths) { + const screenWidth = width * scaleX + if (Math.abs(newWidth - screenWidth) <= SNAP_THRESHOLD) { + newWidth = screenWidth + snapped = true + break + } + } + // 如果吸附了,根据宽高比调整高度 + if (snapped) { + newHeight = newWidth / oRatio + if (this.resizeBtnEl) { + this.resizeBtnEl.style.backgroundColor = '#409eff' + } + } else { + if (this.resizeBtnEl) { + this.resizeBtnEl.style.backgroundColor = 'rgba(0, 0, 0, 0.3)' + } + } + // 限制最小值 if (newWidth < minImgResizeWidth) newWidth = minImgResizeWidth if (newHeight < minImgResizeHeight) newHeight = minImgResizeHeight @@ -305,6 +352,10 @@ class NodeImgAdjust { this.showNodeImage() // 隐藏自定义元素 this.hideHandleEl() + // 重置按钮颜色 + if (this.resizeBtnEl) { + this.resizeBtnEl.style.backgroundColor = 'rgba(0, 0, 0, 0.3)' + } // 更新节点图片为新的大小 const { image, imageTitle } = this.node.getData() const { scaleX, scaleY } = this.mousedownDrawTransform From 0e005d40eba703c5ed1d588bb9426de5204b8377 Mon Sep 17 00:00:00 2001 From: Achuan-2 Date: Wed, 26 Nov 2025 11:19:49 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E6=8B=96=E5=8A=A8=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=AE=9E=E6=97=B6=E9=A2=84=E8=A7=88=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E5=8F=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple-mind-map/src/plugins/NodeImgAdjust.js | 88 ++++++++++++++------ 1 file changed, 61 insertions(+), 27 deletions(-) diff --git a/simple-mind-map/src/plugins/NodeImgAdjust.js b/simple-mind-map/src/plugins/NodeImgAdjust.js index 02b7a1cb2..8acb22143 100644 --- a/simple-mind-map/src/plugins/NodeImgAdjust.js +++ b/simple-mind-map/src/plugins/NodeImgAdjust.js @@ -22,7 +22,12 @@ class NodeImgAdjust { this.currentImgWidth = 0 // 当前拖拽实时图片的大小 this.currentImgHeight = 0 this.isAdjusted = false // 是否是拖拽结束后的渲染期间 + this.isAdjusted = false // 是否是拖拽结束后的渲染期间 this.referenceWidths = [] // 参考宽度列表 + this.startClientX = 0 + this.startClientY = 0 + this.startImgWidth = 0 + this.startImgHeight = 0 this.bindEvent() } @@ -238,11 +243,17 @@ class NodeImgAdjust { this.isMousedown = true this.mousedownDrawTransform = this.mindMap.draw.transform() // 隐藏节点实际图片 - this.hideNodeImage() + // this.hideNodeImage() this.mousedownOffset.x = e.clientX - this.rect.x2 this.mousedownOffset.y = e.clientY - this.rect.y2 + // 记录初始状态 + this.startClientX = e.clientX + this.startClientY = e.clientY + const { width, height } = this.node.getData('imageSize') + this.startImgWidth = width + this.startImgHeight = height // 将节点图片渲染到自定义元素上 - this.handleEl.style.backgroundImage = `url(${this.node.getData('image')})` + // this.handleEl.style.backgroundImage = `url(${this.node.getData('image')})` // 收集参考宽度 this.referenceWidths = this.collectImageWidths() } @@ -302,23 +313,50 @@ class NodeImgAdjust { imgMaxWidth = imgMaxWidth * scaleX imgMaxHeight = imgMaxHeight * scaleY // 计算当前拖拽位置对应的图片的实时大小 - let newWidth = Math.abs(e.clientX - this.rect.x - this.mousedownOffset.x) - let newHeight = Math.abs(e.clientY - this.rect.y - this.mousedownOffset.y) - + // 逻辑尺寸变化量 + const deltaX = (e.clientX - this.startClientX) / scaleX + // 逻辑尺寸 + let newWidth = this.startImgWidth + deltaX + + // 转换为屏幕像素用于吸附判断 + let screenWidth = newWidth * scaleX + // 吸附逻辑 const SNAP_THRESHOLD = 5 let snapped = false for (const width of this.referenceWidths) { - const screenWidth = width * scaleX - if (Math.abs(newWidth - screenWidth) <= SNAP_THRESHOLD) { - newWidth = screenWidth + const refScreenWidth = width * scaleX + if (Math.abs(screenWidth - refScreenWidth) <= SNAP_THRESHOLD) { + newWidth = width + screenWidth = refScreenWidth snapped = true break } } - // 如果吸附了,根据宽高比调整高度 + + let newHeight = newWidth / oRatio + + // 限制最小值 + if (newWidth < minImgResizeWidth) newWidth = minImgResizeWidth + if (newHeight < minImgResizeHeight) newHeight = minImgResizeHeight + // 限制最大值 + if (newWidth > imgMaxWidth) newWidth = imgMaxWidth + if (newHeight > imgMaxHeight) newHeight = imgMaxHeight + + this.currentImgWidth = newWidth + this.currentImgHeight = newHeight + + // 更新handleEl的位置和大小 + + this.node.getData().imageSize = { + width: this.currentImgWidth, + height: this.currentImgHeight, + custom: true + } + this.mindMap.render() + + // 更新按钮状态 if (snapped) { - newHeight = newWidth / oRatio if (this.resizeBtnEl) { this.resizeBtnEl.style.backgroundColor = '#409eff' } @@ -327,29 +365,13 @@ class NodeImgAdjust { this.resizeBtnEl.style.backgroundColor = 'rgba(0, 0, 0, 0.3)' } } - - // 限制最小值 - if (newWidth < minImgResizeWidth) newWidth = minImgResizeWidth - if (newHeight < minImgResizeHeight) newHeight = minImgResizeHeight - // 限制最大值 - if (newWidth > imgMaxWidth) newWidth = imgMaxWidth - if (newHeight > imgMaxHeight) newHeight = imgMaxHeight - const [actWidth, actHeight] = resizeImgSizeByOriginRatio( - imageOriginWidth, - imageOriginHeight, - newWidth, - newHeight - ) - this.currentImgWidth = actWidth - this.currentImgHeight = actHeight - this.updateHandleElSize() } // 鼠标松开 onMouseup() { if (!this.isMousedown) return // 显示节点实际图片 - this.showNodeImage() + // this.showNodeImage() // 隐藏自定义元素 this.hideHandleEl() // 重置按钮颜色 @@ -382,6 +404,18 @@ class NodeImgAdjust { // 渲染完成事件 onRenderEnd() { + if (this.isMousedown && this.node) { + // 拖拽过程中渲染完成,需要更新操作柄位置 + // 必须重新获取 img 元素,因为 render 后 DOM 可能重建 + const img = this.node.group.findOne('image') + if (img) { + this.img = img + this.rect = img.rbox() + // 强制更新位置,确保跟随 + this.setHandleElRect() + } + return + } if (!this.isAdjusted) { this.hideHandleEl() return From c15dd40efc088e9b9a8a61d287fa1062c95a6471 Mon Sep 17 00:00:00 2001 From: Achuan-2 Date: Wed, 26 Nov 2025 11:26:34 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E8=8A=82=E7=82=B9=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E5=AE=BD=E5=BA=A6=E6=94=AF=E6=8C=81=E5=90=B8=E9=99=84=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E6=A0=B9=E6=8D=AE=E5=90=8C=E7=BA=A7=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E7=A1=AE=E5=AE=9A=E5=90=B8=E9=99=84=E5=AE=BD=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/core/render/node/nodeModifyWidth.js | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/simple-mind-map/src/core/render/node/nodeModifyWidth.js b/simple-mind-map/src/core/render/node/nodeModifyWidth.js index 734fea559..e66819b8f 100644 --- a/simple-mind-map/src/core/render/node/nodeModifyWidth.js +++ b/simple-mind-map/src/core/render/node/nodeModifyWidth.js @@ -21,6 +21,10 @@ function initDragHandle() { this.dragHandleMousedownBodyCursor = '' // 鼠标按下时记录当前节点的left值 this.dragHandleMousedownLeft = 0 + // 参考节点宽度列表 + this.referenceNodeWidths = [] + // 节点宽度与文本宽度的差值 + this.widthDiff = 0 this.onDragMousemoveHandle = this.onDragMousemoveHandle.bind(this) window.addEventListener('mousemove', this.onDragMousemoveHandle) @@ -29,6 +33,18 @@ function initDragHandle() { this.mindMap.on('node_mouseup', this.onDragMouseupHandle) } +// 收集同级节点宽度 +function collectSiblingNodeWidths() { + this.referenceNodeWidths = [] + if (this.parent && this.parent.children) { + this.parent.children.forEach(node => { + if (node !== this) { + this.referenceNodeWidths.push(node.width) + } + }) + } +} + // 鼠标移动事件 function onDragMousemoveHandle(e) { if (!this.isDragHandleMousedown) return @@ -51,6 +67,25 @@ function onDragMousemoveHandle(e) { let newWidth = this.dragHandleMousedownCustomTextWidth + (this.dragHandleIndex === 0 ? -ox : ox) / scaleX + + // 吸附逻辑 + const currentTotalWidth = newWidth + this.widthDiff + const SNAP_THRESHOLD = 5 + let snapped = false + for (const width of this.referenceNodeWidths) { + if (Math.abs(currentTotalWidth - width) <= SNAP_THRESHOLD) { + newWidth = width - this.widthDiff + snapped = true + break + } + } + + // 视觉反馈 + if (this._dragHandleNodes && this._dragHandleNodes[this.dragHandleIndex]) { + this._dragHandleNodes[this.dragHandleIndex].fill({ + color: snapped ? '#409eff' : 'transparent' + }) + } newWidth = Math.max(newWidth, minNodeTextModifyWidth) if (maxNodeTextModifyWidth !== -1) { newWidth = Math.min(newWidth, maxNodeTextModifyWidth) @@ -87,6 +122,10 @@ function onDragMouseupHandle() { this.dragHandleMousedownX = 0 this.dragHandleIndex = 0 this.dragHandleMousedownCustomTextWidth = 0 + // 重置手柄颜色 + if (this._dragHandleNodes) { + this._dragHandleNodes.forEach(node => node.fill({ color: 'transparent' })) + } this.setData({ customTextWidth: this.customTextWidth }) @@ -119,6 +158,9 @@ function createDragHandleNode() { : this.customTextWidth this.dragHandleMousedownBodyCursor = document.body.style.cursor this.dragHandleMousedownLeft = this.left + // 计算差值和收集参考宽度 + this.widthDiff = this.width - this.dragHandleMousedownCustomTextWidth + this.collectSiblingNodeWidths() this.isDragHandleMousedown = true }) }) @@ -146,6 +188,7 @@ function updateDragHandle() { export default { initDragHandle, + collectSiblingNodeWidths, onDragMousemoveHandle, onDragMouseupHandle, createDragHandleNode,