加载中...

LogicFlow 学习笔记——9. LogicFlow 进阶 节点

LogicFlow 学习笔记——9. LogicFlow 进阶 节点

LogicFlow 进阶 节点(Node)

连线规则

在某些时候,我们可能需要控制边的连接方式,比如开始节点不能被其他节点连接、结束节点不能连接其他节点、用户节点后面必须是判断节点等,想要达到这种效果,我们需要为节点设置以下两个属性。

  • sourceRules - 当节点作为边的起始节点(source)时的校验规则
  • targetRules - 当节点作为边的目标节点(target)时的校验规则

以正方形(square)为例,在边时我们希望它的下一节点只能是圆形节点(circle),那么我们应该给square添加作为source节点的校验规则。

import { RectNode, RectNodeModel } from ''@logicflow/core'';
class SquareModel extends RectNodeModel {
  initNodeData(data) {
    super.initNodeData(data);

    const circleOnlyAsTarget = {
      message: "正方形节点下一个节点只能是圆形节点",
      validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => {
        return targetNode.type === "circle";
      },
    };
    this.sourceRules.push(circleOnlyAsTarget);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在上例中,我们为modelsourceRules属性添加了一条校验规则,校验规则是一个对象,我们需要为其提供messagevalidate属性。
message属性是当不满足校验规则时所抛出的错误信息,validate则是传入规则的校验的回调函数validate方法有两个参数,分别为边的起始节点(source)和目标节点(target),我们可以根据参数信息来决定是否通过校验,其返回值是一个布尔值

提示
当我们在面板上进行边操作的时候,LogicFlow会校验每一条规则,只有全部通过后才能连接。
在边时,当鼠标松开后如果没有通过自定义规则(validate方法返回值为false),LogicFlow会对外抛出事件connection:not-allowed

lf.on(''connection:not-allowed'', (msg) => {
	console.log(msg)
})
  • 1
  • 2
  • 3

下面举个例子,通过设置不同状态下节点的样式来展示连接状态
在节点model中,有个state属性,当节点连接规则校验不通过时,state属性值为5。我们可以通过这个属性来实现连线是节点的提示效果。

新建src/views/Example/LogicFlowAdvance/NodeExample/Component/HexagonNode/index.ts代码如下:

import { ConnectRule, PointTuple, PolygonNode, PolygonNodeModel } from ''@logicflow/core''

class CustomHexagonModel extends PolygonNodeModel {
  setAttributes(): void {
    const width = 100
    const height = 100
    const x = 50
    const y = 50
    // 计算六边形,中心点为 [50, 50],宽高均为 100
    const pointsList: PointTuple[] = [
      [x - 0.25 * width, y - 0.5 * height],
      [x + 0.25 * width, y - 0.5 * height],
      [x + 0.5 * width, y],
      [x + 0.25 * width, y + 0.5 * height],
      [x - 0.25 * width, y + 0.5 * height],
      [x - 0.5 * width, y]
    ]
    this.points = pointsList
  }

  getConnectedSourceRules(): ConnectRule[] {
    const rules = super.getConnectedSourceRules()
    const geteWayOnlyAsTarget = {
      message: ''下一个节点只能是 circle'',
      validate: (source: any, target: any, sourceAnchor: any, targetAnchor: any) => {
        console.log(
          ''sourceAnchor, targetAnchor, source, target'',
          sourceAnchor,
          targetAnchor,
          source,
          target
        )
        return target.type === ''circle''
      }
    }
    rules.push(geteWayOnlyAsTarget)
    return rules
  }

  getNodeStyle(): {
    [x: string]: any
    fill?: string | undefined
    stroke?: string | undefined
    strokeWidth?: number | undefined
  } {
    const style = super.getNodeStyle()
    if (this.properties.isSelected) {
      style.fill = ''red''
    }
    if (this.isHovered) {
      style.stroke = ''red''
    }
    // 如果此节点不允许被连接,节点变红
    if (this.state === 5) {
      style.fill = ''red''
    }
    if (this.state === 4) {
      style.fill = ''green''
    }
    return style
  }
}

export default {
  type: ''HexagonNode'',
  view: PolygonNode,
  model: CustomHexagonModel
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

之后新建src/views/Example/LogicFlowAdvance/NodeExample/Example01.vue代码如下:

<script setup lang="ts">
import LogicFlow, { Definition } from ''@logicflow/core''
import { onMounted } from ''vue''
import HexagonNode from ''./Component/HexagonNode''
import ''@logicflow/core/dist/style/index.css''

const data = {
  nodes: [
    {
      id: ''1'',
      type: ''rect'',
      x: 300,
      y: 100
    },
    {
      id: ''2'',
      type: ''circle'',
      x: 300,
      y: 250
    },
    {
      id: ''3'',
      type: ''HexagonNode'',
      x: 100,
      y: 100,
      text: ''只能连接到圆''
    }
  ],
  edges: []
}

const SilentConfig = {
  stopScrollGraph: true,
  stopMoveGraph: true,
  stopZoomGraph: true
}

const styleConfig: Partial<Definition> = {
  style: {
    rect: {
      rx: 5,
      ry: 5,
      strokeWidth: 2
    },
    circle: {
      fill: ''#f5f5f5'',
      stroke: ''#666''
    },
    ellipse: {
      fill: ''#dae8fc'',
      stroke: ''#6c8ebf''
    },
    polygon: {
      fill: ''#d5e8d4'',
      stroke: ''#82b366''
    },
    diamond: {
      fill: ''#ffe6cc'',
      stroke: ''#d79b00''
    },
    text: {
      color: ''#b85450'',
      fontSize: 12
    }
  }
}

onMounted(() => {
  const lf = new LogicFlow({
    container: document.getElementById(''container'')!,
    grid: true,
    ...SilentConfig,
    ...styleConfig
  })
  lf.register(HexagonNode)
  lf.setTheme({
    nodeText: {
      color: ''#000000'',
      overflowMode: ''ellipsis'',
      lineHeight: 1.2,
      fontSize: 12
    }
  })
  lf.render(data)
  lf.translateCenter()
  lf.on(''connection:not-allowed'', (error) => {
    alert(error.msg)
  })
})
</script>
<template>
  <h3>Example Node (Advance) - 01</h3>
  <div id="container"></div>
</template>
<style>
#container {
  /* 定义容器的宽度和高度 */
  width: 100%;
  height: 500px;
}
</style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101

运行后效果如下:
New Image

移动

有些时候,我们需要更加细粒度的控制节点什么时候可以移动,什么时候不可以移动,比如在实现分组插件时,需要控制分组节点子节点不允许移动出分组。和连线规则类似,我们可以给节点的moveRules添加规则函数。

class MovableNodeModel extends RectNodeModel {
  initNodeData(data) {
    super.initNodeData(data);
    this.moveRules.push((model, deltaX, deltaY) => {
      // 需要处理的内容
    });
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

graphModel中支持添加全局移动规则,例如在移动A节点的时候,期望把B节点也一起移动了。

lf.graphModel.addNodeMoveRules((model, deltaX, deltaY) => {
  // 如果移动的是分组,那么分组的子节点也跟着移动。
  if (model.isGroup && model.children) {
    lf.graphModel.moveNodes(model.children, deltaX, deltaY, true);
  }
  return true;
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

新建src/views/Example/LogicFlowAdvance/NodeExample/Component/CustomNode/index.ts代码如下:

import { RectNode, RectNodeModel } from ''@logicflow/core''
class CustomNode extends RectNode {
  // 禁止节点点击后被显示到所有元素前面
  toFront() {
    return false
  }
}

class CustomNodeModel extends RectNodeModel {
  initNodeData(data: any) {
    if (!data.text || typeof data.text === ''string'') {
      data.text = {
        value: data.text || '''',
        x: data.x - 230,
        y: data.y
      }
    }
    super.initNodeData(data)
    this.width = 500
    this.height = 200
    this.isGroup = true
    this.zIndex = -1
    this.children = data.children
  }
  getTextStyle() {
    const style = super.getTextStyle()
    style.overflowMode = ''autoWrap''
    style.width = 15
    return style
  }
}

export default {
  type: ''custom-node'',
  view: CustomNode,
  model: CustomNodeModel
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

新建src/views/Example/LogicFlowAdvance/NodeExample/Component/MovableNode/index.ts,代码如下:

import { RectNode, RectNodeModel } from ''@logicflow/core''
class MovableNode extends RectNode {}

class MovableNodeModel extends RectNodeModel {
  initNodeData(data: any) {
    super.initNodeData(data)
    this.moveRules.push((model, deltaX, deltaY) => {
      // 不允许移动到坐标为负值的地方
      if (model.x + deltaX - this.width / 2 < 0 || model.y + deltaY - this.height / 2 < 0) {
        return false
      }
      return true
    })
    console.log(data)
    this.children = data.children
    if (this.children) {
      this.isGroup = true
    }
  }
}

export default {
  type: ''movable-node'',
  view: MovableNode,
  model: MovableNodeModel
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

新建src/views/Example/LogicFlowAdvance/NodeExample/Example02.vue代码如下:

<script setup lang="ts">
import LogicFlow from ''@logicflow/core''
import { onMounted } from ''vue''
import ''@logicflow/core/dist/style/index.css''
import CustomNode from ''./Component/CustomNode''
import MovableNode from ''./Component/MovableNode''

const data = {
  nodes: [
    {
      id: ''node-1'',
      type: ''custom-node'',
      x: 300,
      y: 250,
      text: ''你好'',
      children: [''circle-1'']
    },
    {
      type: ''movable-node'',
      x: 100,
      y: 70,
      text: ''你好'',
      children: [''node-1'']
    },
    {
      id: ''circle-1'',
      type: ''circle'',
      x: 300,
      y: 250,
      text: ''hello world''
    }
  ],
  edges: []
}

const SilentConfig = {
  stopScrollGraph: true,
  stopMoveGraph: true,
  stopZoomGraph: true
}

onMounted(() => {
  const lf = new LogicFlow({
    container: document.getElementById(''container'')!,
    grid: true,
    ...SilentConfig
  })
  lf.register(CustomNode)
  lf.register(MovableNode)
  lf.graphModel.addNodeMoveRules((model, deltaX, deltaY) => {
    console.log(model)
    if (model.isGroup && model.children) {
      // 如果移动的是分组,那么分组的子节点也跟着移动。
      lf.graphModel.moveNodes(model.children, deltaX, deltaY, true)
    }
    return true
  })
  lf.render(data)
  lf.translateCenter()
})
</script>
<template>
  <h3>Example Node (Advance) - 02</h3>
  <div id="container"></div>
</template>
<style>
#container {
  /* 定义容器的宽度和高度 */
  width: 100%;
  height: 500px;
}
</style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72

运行后效果如下:
New Image

锚点

对于各种基础类型节点,LogicFlow都内置了默认锚点。LogicFlow支持通过重写获取锚点的方法来实现自定义节点的锚点。
新建src/views/Example/LogicFlowAdvance/NodeExample/Component/SqlEdge/index.ts代码如下:

import { PolylineEdge, PolylineEdgeModel } from ''@logicflow/core''

// 自定义边模型类,继承自 BezierEdgeModel
class CustomEdgeModel2 extends PolylineEdgeModel {
  /**
   * 重写 getEdgeStyle 方法,定义边的样式
   */
  getEdgeStyle() {
    const style = super.getEdgeStyle() // 调用父类方法获取默认的边样式
    style.strokeWidth = 1 // 设置边的线条宽度为1
    style.stroke = ''#ababac'' // 设置边的颜色为淡灰色
    return style // 返回自定义的边样式
  }

  /**
   * 重写 getData 方法,增加锚点数据的保存
   */
  getData() {
    const data: any = super.getData() // 调用父类方法获取默认的边数据
    // 添加锚点ID到数据中,以便保存和后续使用
    data.sourceAnchorId = this.sourceAnchorId // 保存源锚点ID
    data.targetAnchorId = this.targetAnchorId // 保存目标锚点ID
    return data // 返回包含锚点信息的边数据
  }

  /**
   * 自定义方法,基于锚点的位置更新边的路径
   */
  updatePathByAnchor() {
    // 获取源节点模型
    const sourceNodeModel = this.graphModel.getNodeModelById(this.sourceNodeId)
    // 从源节点的默认锚点中查找指定的锚点
    const sourceAnchor = sourceNodeModel
      .getDefaultAnchor()
      .find((anchor) => anchor.id === this.sourceAnchorId)

    // 获取目标节点模型
    const targetNodeModel = this.graphModel.getNodeModelById(this.targetNodeId)
    // 从目标节点的默认锚点中查找指定的锚点
    const targetAnchor = targetNodeModel
      .getDefaultAnchor()
      .find((anchor) => anchor.id === this.targetAnchorId)

    // 如果找到源锚点,则更新边的起始点
    if (sourceAnchor) {
      const startPoint = {
        x: sourceAnchor.x,
        y: sourceAnchor.y
      }
      this.updateStartPoint(startPoint)
    }

    // 如果找到目标锚点,则更新边的终点
    if (targetAnchor) {
      const endPoint = {
        x: targetAnchor.x,
        y: targetAnchor.y
      }
      this.updateEndPoint(endPoint)
    }

    // 清空当前边的控制点列表,以便贝塞尔曲线重新计算控制点
    this.pointsList = []
    this.initPoints()
  }
}

// 导出自定义边配置
export default {
  type: ''sql-edge'', // 自定义边的类型标识
  view: PolylineEdge, // 使用贝塞尔曲线边的视图
  model: CustomEdgeModel2 // 使用自定义的边模型
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

新建src/views/Example/LogicFlowAdvance/NodeExample/Component/SqlNode/index.ts代码如下:

import { h, HtmlNode, HtmlNodeModel } from ''@logicflow/core''

class SqlNode extends HtmlNode {
  /**
   * 1.1.7 版本后支持在 view 中重写锚点形状
   */
  getAnchorShape(anchorData: any) {
    const { x, y, type } = anchorData
    return h(''rect'', {
      x: x - 5,
      y: y - 5,
      width: 10,
      height: 10,
      className: `custom-anchor ${type === ''left'' ? ''incomming-anchor'' : ''outgoing-anchor''}`
    })
  }

  setHtml(rootEl: HTMLElement): void {
    rootEl.innerHTML = ''''
    const {
      properties: { fields, tableName }
    } = this.props.model
    rootEl.setAttribute(''class'', ''table-container'')
    const container = document.createElement(''div'')
    container.className = `table-node table-color-${Math.ceil(Math.random() * 4)}`
    const tableNameElement = document.createElement(''div'')
    tableNameElement.innerHTML = tableName
    tableNameElement.className = ''table-name''
    container.appendChild(tableNameElement)
    const fragment = document.createDocumentFragment()
    for (let i = 0; i < fields.length; i++) {
      const item = fields[i]
      const fieldElement = document.createElement(''div'')
      fieldElement.className = ''table-feild''
      const itemKey = document.createElement(''span'')
      itemKey.innerText = item.key
      const itemType = document.createElement(''span'')
      itemType.innerText = item.type
      itemType.className = ''feild-type''
      fieldElement.appendChild(itemKey)
      fieldElement.appendChild(itemType)
      fragment.appendChild(fieldElement)
    }
    container.appendChild(fragment)
    rootEl.appendChild(container)
  }
}

class SqlNodeModel extends HtmlNodeModel {
  /**
   * 给 model 自定义添加字段方法
   */
  addField(item: any) {
    this.properties.fields.unshift(item)
    this.setAttributes()
    // 为了保持节点顶部位置不变,在节点变化后,对节点进行一个位移,位移距离为添加高度的一半
    this.move(0, 24 / 2)
    // 更新节点连接边的 path
    this.incoming.edges.forEach((egde) => {
      // 调用自定义的更新方案
      egde.updatePathByAnchor()
    })
    this.outgoing.edges.forEach((edge) => {
      // 调用自定义的更新方案
      edge.updatePathByAnchor()
    })
  }

  getOutlineStyle() {
    const style = super.getOutlineStyle()
    style.stroke = ''none''
    if (style.hover) {
      style.hover.stroke = ''none''
    }
    return style
  }

  // 如果不用修改锚的形状,可以重写颜色相关样式
  getAnchorStyle(anchorInfo: any) {
    const style = super.getAnchorStyle(anchorInfo)
    if (anchorInfo.type === ''left'') {
      style.fill = ''red''
      style.hover.fill = ''transparent''
      style.hover.stroke = ''transpanrent''
      style.className = ''lf-hide-default''
    } else {
      style.fill = ''green''
    }
    return style
  }

  setAttributes() {
    this.width = 200
    const {
      properties: { fields }
    } = this
    this.height = 60 + fields.length * 24
    const circleOnlyAsTarget = {
      message: ''只允许从右边的锚点连出'',
      validate: (_sourceNode: any, _targetNode: any, sourceAnchor: any) => {
        return sourceAnchor.type === ''right''
      }
    }
    this.sourceRules.push(circleOnlyAsTarget)
    this.targetRules.push({
      message: ''只允许连接左边的锚点'',
      validate: (_sourceNode, _targetNode, _sourceAnchor, targetAnchor: any) => {
        return targetAnchor.type === ''left''
      }
    })
  }

  getDefaultAnchor() {
    const {
      id,
      x,
      y,
      width,
      height,
      isHovered,
      isSelected,
      properties: { fields, isConnection }
    } = this
    const anchors: any[] = []
    fields.forEach((feild: any, index: any) => {
      // 如果是连出,就不显示左边的锚点
      if (isConnection || !(isHovered || isSelected)) {
        anchors.push({
          x: x - width / 2 + 10,
          y: y - height / 2 + 60 + index * 24,
          id: `${id}_${feild.key}_left`,
          edgeAddable: false,
          type: ''left''
        })
      }
      if (!isConnection) {
        anchors.push({
          x: x + width / 2 - 10,
          y: y - height / 2 + 60 + index * 24,
          id: `${id}_${feild.key}_right`,
          type: ''right''
        })
      }
    })
    return anchors
  }
}

export default {
  type: ''sql-node'',
  model: SqlNodeModel,
  view: SqlNode
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153

新建 src/views/Example/LogicFlowAdvance/NodeExample/Example03.vue 代码如下:

<script setup lang="ts">
import LogicFlow from ''@logicflow/core''
import { onMounted, ref } from ''vue''
import ''@logicflow/core/dist/style/index.css''
import SqlEdge from ''./Component/SqlEdge''
import SqlNode from ''./Component/SqlNode''
import { ElButton } from ''element-plus''

const data = {
  nodes: [
    {
      id: ''node_id_1'',
      type: ''sql-node'',
      x: 100,
      y: 100,
      properties: {
        tableName: ''Users'',
        fields: [
          {
            key: ''id'',
            type: ''string''
          },
          {
            key: ''name'',
            type: ''string''
          },
          {
            key: ''age'',
            type: ''integer''
          }
        ]
      }
    },
    {
      id: ''node_id_2'',
      type: ''sql-node'',
      x: 400,
      y: 200,
      properties: {
        tableName: ''Settings'',
        fields: [
          {
            key: ''id'',
            type: ''string''
          },
          {
            key: ''key'',
            type: ''integer''
          },
          {
            key: ''value'',
            type: ''string''
          }
        ]
      }
    }
  ],
  edges: []
}

const SilentConfig = {
  stopScrollGraph: true,
  stopMoveGraph: true,
  stopZoomGraph: true
}

const lfRef = ref<LogicFlow>()

onMounted(() => {
  const lf = new LogicFlow({
    container: document.getElementById(''container'')!,
    grid: true,
    ...SilentConfig
  })
  lf.register(SqlEdge)
  lf.register(SqlNode)
  lf.setDefaultEdgeType(''sql-edge'')
  lf.setTheme({
    bezier: {
      stroke: ''#afafaf'',
      strokeWidth: 1
    }
  })
  lf.render(data)
  lf.translateCenter()

  // 1.1.28新增,可以自定义锚点显示时机了
  lf.on(''anchor:dragstart'', ({ data, nodeModel }) => {
    console.log(''dragstart'', data)
    if (nodeModel.type === ''sql-node'') {
      lf.graphModel.nodes.forEach((node) => {
        if (node.type === ''sql-node'' && nodeModel.id !== node.id) {
          node.isShowAnchor = true
          node.setProperties({
            isConnection: true
          })
        }
      })
    }
  })

  lf.on(''anchor:dragend'', ({ data, nodeModel }) => {
    console.log(''dragend'', data)
    if (nodeModel.type === ''sql-node'') {
      lf.graphModel.nodes.forEach((node) => {
        if (node.type === ''sql-node'' && nodeModel.id !== node.id) {
          node.isShowAnchor = false
          lf.deleteProperty(node.id, ''isConnection'')
        }
      })
    }
  })

  lfRef.value = lf
})

const addField = () => {
  lfRef.value?.getNodeModelById(''node_id_1'').addField({
    key: Math.random().toString(36).substring(2, 7),
    type: [''integer'', ''long'', ''string'', ''boolean''][Math.floor(Math.random() * 4)]
  })
}
</script>
<template>
  <h3>Example Node (Advance) - 02</h3>
  <ElButton @click="addField()" style="margin-bottom: 10px">Add Field</ElButton>
  <div id="container" class="sql"></div>
</template>
<style>
#container {
  /* 定义容器的宽度和高度 */
  width: 100%;
  height: 500px;
}
.sql {
  .table-container {
    box-sizing: border-box;
    padding: 10px;
  }

  .table-node {
    width: 100%;
    height: 100%;
    overflow: hidden;
    background: #fff;
    border-radius: 4px;
    box-shadow: 0 1px 3px rgb(0 0 0 / 30%);
  }

  .table-node::before {
    display: block;
    width: 100%;
    height: 8px;
    background: #d79b00;
    content: '''';
  }

  .table-node.table-color-1::before {
    background: #9673a6;
  }

  .table-node.table-color-2::before {
    background: #dae8fc;
  }

  .table-node.table-color-3::before {
    background: #82b366;
  }

  .table-node.table-color-4::before {
    background: #f8cecc;
  }

  .table-name {
    height: 28px;
    font-size: 14px;
    line-height: 28px;
    text-align: center;
    background: #f5f5f5;
  }

  .table-feild {
    display: flex;
    justify-content: space-between;
    height: 24px;
    padding: 0 10px;
    font-size: 12px;
    line-height: 24px;
  }

  .feild-type {
    color: #9f9c9f;
  }
  /* 自定义锚点样式 */
  .custom-anchor {
    cursor: crosshair;
    fill: #d9d9d9;
    stroke: #999;
    stroke-width: 1;
    /* rx: 3; */
    /* ry: 3; */
  }

  .custom-anchor:hover {
    fill: #ff7f0e;
    stroke: #ff7f0e;
  }

  .lf-node-not-allow .custom-anchor:hover {
    cursor: not-allowed;
    fill: #d9d9d9;
    stroke: #999;
  }

  .incomming-anchor {
    stroke: #d79b00;
  }

  .outgoing-anchor {
    stroke: #82b366;
  }
}
</style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223

启动后效果如下:
New Image
上面的示例中,我们自定义锚点的时候,不仅可以定义锚点的数量和位置,还可以给锚点加上任意属性。有了这些属性,我们可以再做很多额外的事情。例如,我们增加一个校验规则,只允许节点从右边连出,从左边连入;或者加个id,在获取数据的时候保存当前连线从哪个锚点连接到哪个锚点。

注意
一定要确保锚点id唯一,否则可能会出现在连线规则校验不准确的问题。在实际开发中,存在隐藏锚点的需求,可以参考 github issue 如何隐藏锚点?

更新

HTML 节点目前通过修改 properties 触发节点更新

 /**
  * @overridable 支持重写
  * 和react的shouldComponentUpdate类似,都是为了避免出发不必要的render.
  * 但是这里不一样的地方在于,setHtml方法,我们只在properties发生变化了后再触发。
  * 而x,y等这些坐标相关的方法发生了变化,不会再重新触发setHtml.
  */
 shouldUpdate() {
   if (this.preProperties && this.preProperties === this.currentProperties) return;
   this.preProperties = this.currentProperties;
   return true;
 }
 componentDidMount() {
   if (this.shouldUpdate()) {
     this.setHtml(this.rootEl);
   }
 }
 componentDidUpdate() {
   if (this.shouldUpdate()) {
     this.setHtml(this.rootEl);
   }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

如果期望其他内容的修改可以触发节点更新,可以重写shouldUpdate(相关issue: #1208

shouldUpdate() {
  if (this.preProperties &&
   this.preProperties === this.currentProperties &&
   this.preText === this.props.model.text.value
 ) return;
  this.preProperties = this.currentProperties;
  this.preText = this.props.model.text.value
  return true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9