Feat:1.新增带鱼头鱼尾的鱼骨图结构;2.渲染节点连线逻辑去除对鱼骨图的硬编码

This commit is contained in:
街角小林
2025-04-03 11:53:56 +08:00
parent 0f9f057d65
commit 428ac15499
6 changed files with 196 additions and 26 deletions

View File

@@ -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
]

View File

@@ -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) {

View File

@@ -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) {
// 创建缺少的线

View File

@@ -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')

View File

@@ -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(
`<path d="M4,181 C4,181, 0,177, 4,173 Q 96.09523809523809,0, 288.2857142857143,0 L 288.2857142857143,354 Q 48.047619047619044,354, 8,218.18367346938777 C8,218.18367346938777, 6,214.18367346938777, 8,214.18367346938777 L 41.183673469387756,214.18367346938777 Z"></path>`
)
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(
'<path d="M 606.9342905223708 0 Q 713.1342905223709 -177 819.3342905223708 -177 L 766.2342905223709 0 L 819.3342905223708 177 Q 713.1342905223709 177 606.9342905223708 0 z"></path>'
)
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

View File

@@ -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