From 428ac15499f504412584e339d686a8bb5219473b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A1=97=E8=A7=92=E5=B0=8F=E6=9E=97?= <1013335014@qq.com> Date: Thu, 3 Apr 2025 11:53:56 +0800 Subject: [PATCH] =?UTF-8?q?Feat=EF=BC=9A1.=E6=96=B0=E5=A2=9E=E5=B8=A6?= =?UTF-8?q?=E9=B1=BC=E5=A4=B4=E9=B1=BC=E5=B0=BE=E7=9A=84=E9=B1=BC=E9=AA=A8?= =?UTF-8?q?=E5=9B=BE=E7=BB=93=E6=9E=84=EF=BC=9B2.=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E8=BF=9E=E7=BA=BF=E9=80=BB=E8=BE=91=E5=8E=BB?= =?UTF-8?q?=E9=99=A4=E5=AF=B9=E9=B1=BC=E9=AA=A8=E5=9B=BE=E7=9A=84=E7=A1=AC?= =?UTF-8?q?=E7=BC=96=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple-mind-map/src/constants/constant.js | 6 + simple-mind-map/src/core/render/Render.js | 7 +- .../src/core/render/node/MindMapNode.js | 11 +- simple-mind-map/src/core/render/node/Style.js | 27 +-- simple-mind-map/src/layouts/Fishbone.js | 157 +++++++++++++++++- simple-mind-map/src/plugins/Drag.js | 14 +- 6 files changed, 196 insertions(+), 26 deletions(-) diff --git a/simple-mind-map/src/constants/constant.js b/simple-mind-map/src/constants/constant.js index eea5764f..8332dba3 100644 --- a/simple-mind-map/src/constants/constant.js +++ b/simple-mind-map/src/constants/constant.js @@ -15,6 +15,7 @@ export const CONSTANTS = { TIMELINE: 'timeline', TIMELINE2: 'timeline2', FISHBONE: 'fishbone', + FISHBONE2: 'fishbone2', RIGHT_FISHBONE: 'rightFishbone', VERTICAL_TIMELINE: 'verticalTimeline' }, @@ -129,6 +130,10 @@ export const layoutList = [ name: '鱼骨图', value: CONSTANTS.LAYOUT.FISHBONE }, + { + name: '鱼骨图2', + value: CONSTANTS.LAYOUT.FISHBONE2 + }, { name: '向右鱼骨图', value: CONSTANTS.LAYOUT.RIGHT_FISHBONE @@ -144,6 +149,7 @@ export const layoutValueList = [ CONSTANTS.LAYOUT.TIMELINE2, CONSTANTS.LAYOUT.VERTICAL_TIMELINE, CONSTANTS.LAYOUT.FISHBONE, + CONSTANTS.LAYOUT.FISHBONE2, CONSTANTS.LAYOUT.RIGHT_FISHBONE ] diff --git a/simple-mind-map/src/core/render/Render.js b/simple-mind-map/src/core/render/Render.js index ea63291c..d87628f3 100644 --- a/simple-mind-map/src/core/render/Render.js +++ b/simple-mind-map/src/core/render/Render.js @@ -61,7 +61,9 @@ const layouts = { // 竖向时间轴 [CONSTANTS.LAYOUT.VERTICAL_TIMELINE]: VerticalTimeline, // 鱼骨图 - [CONSTANTS.LAYOUT.FISHBONE]: Fishbone + [CONSTANTS.LAYOUT.FISHBONE]: Fishbone, + // 鱼骨图2 + [CONSTANTS.LAYOUT.FISHBONE2]: Fishbone } // 渲染 @@ -116,6 +118,9 @@ class Render { // 设置布局结构 setLayout() { + if (this.layout && this.layout.beforeChange) { + this.layout.beforeChange() + } const { layout } = this.mindMap.opt let L = layouts[layout] || this.mindMap[layout] if (!L) { diff --git a/simple-mind-map/src/core/render/node/MindMapNode.js b/simple-mind-map/src/core/render/node/MindMapNode.js index c0a0af12..9c085fc1 100644 --- a/simple-mind-map/src/core/render/node/MindMapNode.js +++ b/simple-mind-map/src/core/render/node/MindMapNode.js @@ -810,13 +810,10 @@ class MindMapNode { } let childrenLen = this.getChildrenLength() // 切换为鱼骨结构时,清空根节点和二级节点的连线 - if ( - [CONSTANTS.LAYOUT.FISHBONE, CONSTANTS.LAYOUT.RIGHT_FISHBONE].includes( - this.mindMap.opt.layout - ) && - (this.isRoot || this.layerIndex === 1) - ) { - childrenLen = 0 + if (this.mindMap.renderer.layout.nodeIsRemoveAllLines) { + if (this.mindMap.renderer.layout.nodeIsRemoveAllLines(this)) { + childrenLen = 0 + } } if (childrenLen > this._lines.length) { // 创建缺少的线 diff --git a/simple-mind-map/src/core/render/node/Style.js b/simple-mind-map/src/core/render/node/Style.js index d3fa39ad..69752f8f 100644 --- a/simple-mind-map/src/core/render/node/Style.js +++ b/simple-mind-map/src/core/render/node/Style.js @@ -8,6 +8,18 @@ const backgroundStyleProps = [ 'backgroundSize' ] +export const shapeStyleProps = [ + 'gradientStyle', + 'startColor', + 'endColor', + 'startDir', + 'endDir', + 'fillColor', + 'borderColor', + 'borderWidth', + 'borderDasharray' +] + // 样式类 class Style { // 设置背景样式 @@ -128,17 +140,10 @@ class Style { // 形状 shape(node) { - const styles = { - gradientStyle: this.merge('gradientStyle'), - startColor: this.merge('startColor'), - endColor: this.merge('endColor'), - startDir: this.merge('startDir'), - endDir: this.merge('endDir'), - fillColor: this.merge('fillColor'), - borderColor: this.merge('borderColor'), - borderWidth: this.merge('borderWidth'), - borderDasharray: this.merge('borderDasharray') - } + const styles = {} + shapeStyleProps.forEach(key => { + styles[key] = this.merge(key) + }) if (styles.gradientStyle) { if (!this._gradient) { this._gradient = this.ctx.nodeDraw.gradient('linear') diff --git a/simple-mind-map/src/layouts/Fishbone.js b/simple-mind-map/src/layouts/Fishbone.js index 1b986db0..8240e684 100644 --- a/simple-mind-map/src/layouts/Fishbone.js +++ b/simple-mind-map/src/layouts/Fishbone.js @@ -2,14 +2,75 @@ import Base from './Base' import { walk, asyncRun, degToRad, getNodeIndexInNodeList } from '../utils' import { CONSTANTS } from '../constants/constant' import utils from './fishboneUtils' +import { SVG } from '@svgdotjs/svg.js' +import { shapeStyleProps } from '../core/render/node/Style' // 鱼骨图 class Fishbone extends Base { // 构造函数 - constructor(opt = {}) { + constructor(opt = {}, layout) { super(opt) + this.layout = layout this.indent = 0.3 this.childIndent = 0.5 + this.fishTail = null + this.maxx = 0 + this.headRatio = 1 + this.tailRatio = 0.6 + this.bindEvent() + this.extendShape() + this.beforeChange = this.beforeChange.bind(this) + } + + // 重新渲染时,节点连线是否全部删除 + // 鱼尾鱼骨图会多渲染一些连线,按需删除无法删除掉,只能全部删除重新创建 + nodeIsRemoveAllLines(node) { + return node.isRoot || node.layerIndex === 1 + } + + // 是否是带鱼头鱼尾的鱼骨图 + isFishbone2() { + return this.layout === CONSTANTS.LAYOUT.FISHBONE2 + } + + bindEvent() { + if (!this.isFishbone2()) return + this.onCheckUpdateFishTail = this.onCheckUpdateFishTail.bind(this) + this.mindMap.on('afterExecCommand', this.onCheckUpdateFishTail) + } + + unBindEvent() { + this.mindMap.off('afterExecCommand', this.onCheckUpdateFishTail) + } + + // 扩展节点形状 + extendShape() { + if (!this.isFishbone2()) return + // 扩展鱼头形状 + this.mindMap.addShape({ + name: 'fishHead', + createShape: node => { + const rect = SVG( + `` + ) + const { width, height } = node.shapeInstance.getNodeSize() + rect.size(width, height) + return rect + }, + getPadding: ({ width, height, paddingX, paddingY }) => { + width += paddingX * 2 + height += paddingY * 2 + let shapePaddingX = 0.25 * width + let shapePaddingY = 0 + width += shapePaddingX * 2 + const newHeight = width / this.headRatio + shapePaddingY = (newHeight - height) / 2 + return { + paddingX: shapePaddingX, + paddingY: shapePaddingY + } + } + }) } // 布局 @@ -17,12 +78,14 @@ class Fishbone extends Base { let task = [ () => { this.computedBaseValue() + this.addFishTail() }, () => { this.computedLeftTopValue() }, () => { this.adjustLeftTopValue() + this.updateFishTailPosition() }, () => { callback(this.root) @@ -31,14 +94,77 @@ class Fishbone extends Base { asyncRun(task) } + // 创建鱼尾 + addFishTail() { + if (!this.isFishbone2()) return + const exist = this.mindMap.lineDraw.findOne('.smm-layout-fishbone-tail') + if (!exist) { + this.fishTail = SVG( + '' + ) + this.fishTail.addClass('smm-layout-fishbone-tail') + } else { + this.fishTail = exist + } + const tailHeight = this.root.height + const tailWidth = tailHeight * this.tailRatio + this.fishTail.size(tailWidth, tailHeight) + this.styleFishTail() + this.mindMap.lineDraw.add(this.fishTail) + } + + // 如果根节点更新了形状样式,那么鱼尾也要更新 + onCheckUpdateFishTail(name, node, data) { + if (name === 'SET_NODE_DATA') { + let hasShapeProp = false + Object.keys(data).forEach(key => { + if (shapeStyleProps.includes(key)) { + hasShapeProp = true + } + }) + if (hasShapeProp) { + this.styleFishTail() + } + } + } + + styleFishTail() { + this.root.style.shape(this.fishTail) + } + + // 删除鱼尾 + removeFishTail() { + const exist = this.mindMap.lineDraw.findOne('.smm-layout-fishbone-tail') + if (exist) { + exist.remove() + } + } + + // 更新鱼尾形状位置 + updateFishTailPosition() { + if (!this.isFishbone2()) return + this.fishTail.x(this.maxx).cy(this.root.top + this.root.height / 2) + } + // 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值 computedBaseValue() { walk( this.renderer.renderTree, null, (node, parent, isRoot, layerIndex, index, ancestors) => { + if (isRoot && this.isFishbone2()) { + // 将根节点形状强制修改为鱼头 + node.data.shape = 'fishHead' + } // 创建节点 - let newNode = this.createNode(node, parent, isRoot, layerIndex, index, ancestors) + let newNode = this.createNode( + node, + parent, + isRoot, + layerIndex, + index, + ancestors + ) // 根节点定位在画布中心位置 if (isRoot) { this.setNodeCenter(newNode) @@ -133,19 +259,27 @@ class Fishbone extends Base { if (node.isRoot) { let topTotalLeft = 0 let bottomTotalLeft = 0 + let maxx = -Infinity node.children.forEach(item => { if (this.checkIsTop(item)) { item.left += topTotalLeft this.updateChildren(item.children, 'left', topTotalLeft) let { left, right } = this.getNodeBoundaries(item, 'h') + if (right > maxx) { + maxx = right + } topTotalLeft += right - left } else { item.left += bottomTotalLeft this.updateChildren(item.children, 'left', bottomTotalLeft) let { left, right } = this.getNodeBoundaries(item, 'h') + if (right > maxx) { + maxx = right + } bottomTotalLeft += right - left } }) + this.maxx = maxx } }, true @@ -277,11 +411,14 @@ class Fishbone extends Base { let nodeHalfTop = node.top + node.height / 2 let offset = node.height / 2 + this.getMarginY(node.layerIndex + 1) let line = this.lineDraw.path() + const lineEndX = this.isFishbone2() + ? this.maxx + : maxx - offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) line.plot( this.transformPath( - `M ${node.left + node.width},${nodeHalfTop} L ${ - maxx - offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) - },${nodeHalfTop}` + `M ${ + node.left + node.width + },${nodeHalfTop} L ${lineEndX},${nodeHalfTop}` ) ) node.style.line(line) @@ -406,6 +543,16 @@ class Fishbone extends Base { rect.size(width, expandBtnSize).x(0).y(height) } } + + // 切换切换为其他结构时的处理 + beforeChange() { + // 删除鱼尾 + if (!this.isFishbone2()) return + this.root.nodeData.data.shape = CONSTANTS.SHAPE.RECTANGLE + this.removeFishTail() + this.unBindEvent() + this.mindMap.removeShape('fishHead') + } } export default Fishbone diff --git a/simple-mind-map/src/plugins/Drag.js b/simple-mind-map/src/plugins/Drag.js index d1652f09..371c7d49 100644 --- a/simple-mind-map/src/plugins/Drag.js +++ b/simple-mind-map/src/plugins/Drag.js @@ -408,6 +408,7 @@ class Drag extends Base { TIMELINE2, VERTICAL_TIMELINE, FISHBONE, + FISHBONE2, RIGHT_FISHBONE } = CONSTANTS.LAYOUT this.overlapNode = null @@ -447,6 +448,7 @@ class Drag extends Base { this.handleLogicalStructure(node) break case FISHBONE: + case FISHBONE2: case RIGHT_FISHBONE: this.handleFishbone(node) break @@ -472,6 +474,7 @@ class Drag extends Base { TIMELINE2, VERTICAL_TIMELINE, FISHBONE, + FISHBONE2, RIGHT_FISHBONE } = CONSTANTS.LAYOUT const { LEFT, TOP, RIGHT, BOTTOM } = CONSTANTS.LAYOUT_GROW_DIR @@ -583,6 +586,7 @@ class Drag extends Base { } break case FISHBONE: + case FISHBONE2: case RIGHT_FISHBONE: if (layerIndex <= 1) { notRenderPlaceholder = true @@ -672,6 +676,7 @@ class Drag extends Base { halfPlaceholderHeight break case FISHBONE: + case FISHBONE2: case RIGHT_FISHBONE: if (layerIndex <= 1) { notRenderPlaceholder = true @@ -709,6 +714,7 @@ class Drag extends Base { TIMELINE2, VERTICAL_TIMELINE, FISHBONE, + FISHBONE2, RIGHT_FISHBONE } = CONSTANTS.LAYOUT switch (this.mindMap.opt.layout) { @@ -720,6 +726,7 @@ class Drag extends Base { case TIMELINE2: case VERTICAL_TIMELINE: case FISHBONE: + case FISHBONE2: case RIGHT_FISHBONE: return node.dir default: @@ -732,7 +739,7 @@ class Drag extends Base { handleVerticalCheck(node, checkList, isReverse = false) { const { layout } = this.mindMap.opt const { LAYOUT, LAYOUT_GROW_DIR } = CONSTANTS - const { VERTICAL_TIMELINE, FISHBONE, RIGHT_FISHBONE } = LAYOUT + const { VERTICAL_TIMELINE, FISHBONE, FISHBONE2, RIGHT_FISHBONE } = LAYOUT const { LEFT } = LAYOUT_GROW_DIR const mouseMoveX = this.mouseMoveX const mouseMoveY = this.mouseMoveY @@ -799,6 +806,7 @@ class Drag extends Base { this.placeholderHeight / 2 switch (layout) { case FISHBONE: + case FISHBONE2: case RIGHT_FISHBONE: if (layerIndex === 2) { notRenderLine = true @@ -829,6 +837,7 @@ class Drag extends Base { this.placeholderHeight / 2 switch (layout) { case FISHBONE: + case FISHBONE2: case RIGHT_FISHBONE: if (layerIndex === 2) { notRenderLine = true @@ -866,7 +875,7 @@ class Drag extends Base { handleHorizontalCheck(node, checkList) { const { layout } = this.mindMap.opt const { LAYOUT } = CONSTANTS - const { FISHBONE, RIGHT_FISHBONE, TIMELINE, TIMELINE2 } = LAYOUT + const { FISHBONE, FISHBONE2, RIGHT_FISHBONE, TIMELINE, TIMELINE2 } = LAYOUT let mouseMoveX = this.mouseMoveX let mouseMoveY = this.mouseMoveY let nodeRect = this.getNodeRect(node) @@ -906,6 +915,7 @@ class Drag extends Base { this.placeholderWidth / 2 break case FISHBONE: + case FISHBONE2: case RIGHT_FISHBONE: if (layerIndex === 1) { notRenderLine = true