使用 graph.removeNodeData([node.id]) 删除节点报错
- Dominant language
- TypeScript
- Stars
- 12.3k
- Forks
- 1.6k
- PR merge metrics
- No merged PRs in 30d
Description
> 请提供最小复现代码块❤️
_Originally posted by @yvonneyx in [#7457](https://github.com/antvis/G6/issues/7457#issuecomment-3454263802)_
```
import { ref, onMounted, onUnmounted } from 'vue'
import { Rect, Text } from '@antv/g'
import {
Badge,
BaseBehavior,
BaseNode,
BaseTransform,
CommonEvent,
CubicHorizontal,
ExtensionCategory,
Graph,
idOf,
NodeEvent,
register,
treeToGraphData,
} from '@antv/g6'
// 图表容器引用
const graphContainer = ref<HTMLDivElement>(null)
// G6 实例
let graph: any = null
const RootNodeStyle = {
labelFontSize: 24,
labelFontWeight: 600,
labelOffsetY: 10,
labelPlacement: 'center',
ports: [{ placement: 'right' }, { placement: 'left' }],
radius: 8,
labelWordWrap: true,
labelMaxWidth: 400,
labelMaxLines: 10,
labelLineHeight: 26,
}
const NodeStyle = {
labelFontSize: 16,
labelOffsetY: 2,
labelCursor: 'pointer',
labelBackgroundCursor: 'pointer',
labelPlacement: 'center',
ports: [{ placement: 'left' }, { placement: 'right' }],
radius: 8,
labelWordWrap: true,
labelMaxWidth: 400,
labelMaxLines: 10,
labelLineHeight: 18,
}
const TreeEvent = {
DELETE_NODE: 'delete-node',
}
let textShape
const measureText = (text) => {
if (!textShape) textShape = new Text({ style: text })
textShape.attr(text)
return textShape.getBBox().width
}
const getNodeWidth = (nodeData, isRoot) => {
const padding = 50
const nodeStyle = isRoot ? RootNodeStyle : NodeStyle
return (
measureText({
text: nodeData?.data?.text || '',
fontSize: nodeStyle.labelFontSize,
fontFamily: 'Gill Sans',
}) + padding
)
}
const getNodeSize = (nodeData, isRoot) => {
let width = getNodeWidth(nodeData, isRoot)
const baseWidth = 400
const num = Math.ceil(width / baseWidth)
let height = isRoot ? 48 : 32
if (num > 1) {
height = height * num
}
width = width < 350 ? width : 430
return [width, height]
}
class MindmapNode extends BaseNode {
static defaultStyleProps = {
showIcon: false,
}
constructor(options) {
Object.assign(options.style, MindmapNode.defaultStyleProps)
super(options)
}
getDeleteStyle(attributes) {
const { showIcon, color = '#000000' } = attributes
if (!showIcon) return false
const [width, height] = this.getSize(attributes)
const offsetX = 14
return {
backgroundFill: '#fff',
backgroundHeight: 22,
backgroundLineWidth: 2,
backgroundStroke: color,
backgroundWidth: 22,
cursor: 'pointer',
fill: '#333333',
fontFamily: 'iconfont',
fontSize: 12,
text: '\ue6cc',
textAlign: 'center',
x: width / 2,
y: height + offsetX,
}
}
drawDeleteShape(attributes, container) {
const addStyle: any = this.getDeleteStyle(attributes)
const btn = this.upsert('delete', Badge, addStyle, container)
this.forwardEvent(btn, CommonEvent.CLICK, (event) => {
event.stopPropagation()
this.context.graph.emit(TreeEvent.DELETE_NODE, {
id: this.id,
direction: attributes.direction,
})
})
}
forwardEvent(target, type, listener) {
if (target && !Reflect.has(target, '__bind__')) {
Reflect.set(target, '__bind__', true)
target.addEventListener(type, listener)
}
}
getKeyStyle(attributes) {
const [width, height] = this.getSize(attributes)
const keyShape = super.getKeyStyle(attributes)
return { width, height, ...keyShape }
}
drawKeyShape(attributes, container) {
const keyStyle = this.getKeyStyle(attributes)
return this.upsert('key', Rect, keyStyle, container)
}
render(attributes = this.parsedAttributes, container = this) {
super.render(attributes, container)
this.drawDeleteShape(attributes, container)
}
}
class MindmapEdge extends CubicHorizontal {
get rootId() {
return idOf(this.context.model.getRootsData()[0])
}
getKeyPath(attributes) {
const path = super.getKeyPath(attributes)
const isRoot = this.targetNode.id === this.rootId
const labelWidth = getNodeWidth(this.targetNode, isRoot)
const [, tp] = this.getEndpoints(attributes)
const sign =
this.sourceNode.getCenter()[0] < this.targetNode.getCenter()[0] ? 1 : -1
return [...path, ['L', tp[0] + labelWidth * sign, tp[1]]]
}
}
class CollapseExpandTree extends BaseBehavior {
constructor(context, options) {
super(context, options)
this.bindEvents()
}
update(options) {
this.unbindEvents()
super.update(options)
this.bindEvents()
}
bindEvents() {
const { graph } = this.context
graph.on(NodeEvent.POINTER_ENTER, this.showIcon)
graph.on(NodeEvent.POINTER_LEAVE, this.hideIcon)
graph.on(TreeEvent.DELETE_NODE, this.deleteNode)
}
unbindEvents() {
const { graph } = this.context
graph.off(NodeEvent.POINTER_ENTER, this.showIcon)
graph.off(NodeEvent.POINTER_LEAVE, this.hideIcon)
graph.off(TreeEvent.DELETE_NODE, this.deleteNode)
}
status = 'idle'
showIcon = (event) => {
this.setIcon(event, true)
}
hideIcon = (event) => {
this.setIcon(event, false)
}
setIcon = (event, show) => {
if (this.status !== 'idle') return
const { target } = event
const id = target.id
const { graph, element } = this.context
graph.updateNodeData([{ id, style: { showIcon: show } }])
element.draw({ animation: false, silence: true })
}
// 删除节点
deleteNode = async (event) => {
const { graph } = this.context
// 1. 判断节点是否存在
const node = graph.getNodeData(event.id)
console.log(node, 'node---')
if (!node) {
console.warn(`节点不存在: ${event.id}`)
return
}
// 2. 直接用 event.id 删除节点
await graph.removeNodeData([event.id])
// 3. 刷新视图
await graph.render()
}
}
class AssignColorByBranch extends BaseTransform {
static themeColor = '#8258F8'
static defaultOptions = {
colors: [
'#1783FF',
'#F08F56',
'#D580FF',
'#00C9C9',
'#7863FF',
'#DB9D0D',
'#60C42D',
'#FF80CA',
'#2491B3',
'#17C76F',
// ...Object.values(AssignColorByBranch.themeColor),
],
}
constructor(context, options) {
super(
context,
Object.assign({}, AssignColorByBranch.defaultOptions, options)
)
}
beforeDraw(input) {
const nodes = this.context.model.getNodeData()
if (nodes.length === 0) return input
let colorIndex = 0
const dfs = (nodeId, color) => {
const node = nodes.find((datum) => datum.id == nodeId)
if (!node) return
node.style ||= {}
node.style.color =
color || this.options.colors[colorIndex++ % this.options.colors.length]
node.children?.forEach((childId) => dfs(childId, node.style?.color))
}
nodes
.filter((node) => node.depth === 1)
.forEach((rootNode) => dfs(rootNode.id))
return input
}
}
register(ExtensionCategory.NODE, 'mindmap', MindmapNode)
register(ExtensionCategory.EDGE, 'mindmap', MindmapEdge)
register(ExtensionCategory.BEHAVIOR, 'collapse-expand-tree', CollapseExpandTree)
register(
ExtensionCategory.TRANSFORM,
'assign-color-by-branch',
AssignColorByBranch
)
// 初始化图表
const initGraph = async () => {
if (!graphContainer.value) return
console.log(treeListData.value, 'treeListData.value')
const rootId = treeListData.value.id
graph = new Graph({
container: graphContainer.value,
autoResize: true,
padding: 30,
autoFit: 'view',
zoomRange: [0.1, 10],
data: treeToGraphData(treeListData.value),
node: {
type: 'mindmap',
style: function (d) {
const isRoot = idOf(d) === rootId
const colors: any = d?.style?.color || '#000000'
return {
// direction,
labelText: d?.data?.text,
size: getNodeSize(d, isRoot),
labelFontFamily: 'Gill Sans',
// 通过设置节点标签背景来扩大交互区域 | Expand the interaction area by setting the node label background
labelBackground: true,
labelBackgroundFill: 'transparent',
labelPadding: [13, 60, 40, 20],
...(isRoot ? RootNodeStyle : NodeStyle),
fill: colors,
// fillOpacity: d?.depth > 1 ? 0.7 : 1,
labelFontWeight: d?.depth > 1 ? 400 : 600,
labelFill: d?.style?.color ? '#1a1a1a' : '#ffffff',
}
},
},
edge: {
type: 'mindmap',
style: {
lineWidth: 6,
stroke: function (data: any) {
return this.getNodeData(data.target).style.color || '#99ADD1'
},
curvePosition: 0,
curveOffset: 4,
},
},
layout: {
type: 'mindmap',
direction: 'LR',
getWidth: (node) => getNodeWidth(node, node.id === rootId),
getHGap: (node) => (node.collapsed ? 20 : 40),
getVGap: (node) => (node.collapsed ? 5 : 10),
getHeight: (node) => {
const baseH = getNodeSize(node, node.id === rootId)[1]
return baseH + 20
},
},
behaviors: ['drag-canvas', 'zoom-canvas', 'collapse-expand-tree'],
transforms: ['assign-color-by-branch'],
animation: false,
})
graph.render()
// 节点点击事件
graph.on('node:click', async (ev) => {
const { target } = ev
// console.log('点击边:', target, ev)
const nodeData = graph.getNodeData(target.id)
console.log('节点数据:', nodeData)
})
}
const treeListData: any = ref([])
onMounted(async () => {
treeListData.value = {
'id': 'KQ-lw8v6uj',
'data': {
'text': 'Web3重塑数字经济版图',
'level': 1,
'elementId': '60o3CG1t5A',
'slideId': 'KQ-lw8v6uj'
},
'children': [
{
'id': '54cE1x4Aqm',
'data': {
'level': 2,
'text': 'sss',
'elementId': '54cE1x4Aqm',
'isEmpty': false,
'slideId': 'HeL55xB96X',
'childrenSlideId': []
},
'index': 0,
'children': []
},
{
'id': '5CZf_miD6L',
'data': {
'level': 2,
'text': '添加章节标题',
'isEmpty': true,
'slideId': '-gVk5hF0fX',
'childrenSlideId': [
'-gVk5hF0fX'
]
},
'index': 2,
'children': []
},
]
}
await initGraph()
})
onUnmounted(() => {
// 销毁图表实例,释放资源
if (graph) {
graph.destroy()
graph = null
}
})
.graph-content {
padding: 30rem 23rem 70rem;
position: relative;
width: 100%;
height: 100%;
background: #fff;
}
```
我发现我删除节点的时候因为 这两个方法导致报错 Node not found for id
```
graph.on(NodeEvent.POINTER_ENTER, this.showIcon)
graph.on(NodeEvent.POINTER_LEAVE, this.hideIcon)
```
Contributor guide
No contributing guide indexed for this repository
Research direction
Start at CollapseExpandTree.bindEvents, the showIcon and hideIcon handlers, and deleteNode in the provided reproduction. Reproduce node removal while pointer-enter and pointer-leave listeners are registered, then trace which handler accesses the removed node. Done means removing a node no longer raises “Node not found for id” and the graph still renders correctly.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100