最近在这个 X6 ER 图示例里,我修了一个看起来不大、但实际很容易踩坑的交互问题:
- 鼠标悬浮到一条边上时,希望这条边显示到更上层,避免被其他元素压住。

这篇文章记录一下这个问题的成因,以及我最后采用的修复方案。
现象
在 ER 图里,边默认是普通灰色;鼠标移动到边上时,会切换为高亮色。
但如果只做最直接的悬浮效果,会遇到两个问题:
- 当前悬浮的边可能仍然被别的节点或边压着,视觉上不够明显。
- 如果在
mouseenter里把边提到更高层,DOM / View 层级发生变化后,原本那条边的mouseleave事件有概率丢失。
丢失之后的表现是:
- 边已经没有被鼠标悬浮,但仍然保持高亮。
- 再移动到其他边时,会出现多个边状态不一致的情况。
修复方案
1. 记录每条边的初始层级
为了让边在悬浮结束后能回到原始位置,先缓存每条边最初的 z-index:
typescript
const edgeInitialZIndexMap = new Map<string, number>()
const cacheEdgeInitialZIndex = (edge: Edge) => {
if (!edge || edgeInitialZIndexMap.has(edge.id)) return
edgeInitialZIndexMap.set(edge.id, edge.getZIndex())
}
这样后面无论边被提到多高,都可以恢复回来,而不是简单写死成某个固定层级。
2. 用一个全局状态记录"当前真正处于悬浮中的边"
这里增加了一个单一状态源:
csharp
let pointerHoverEdgeId: string | null = null
这样悬浮态不再分散在每条边自己的事件回调里,而是统一由这一个状态驱动。
3. 悬浮时提升边层级,并同步样式
我把"高亮颜色 + 提升层级"收敛到一个统一的状态同步函数里:
scss
const syncEdgeState = (edge: Edge) => {
cacheEdgeInitialZIndex(edge)
const baseZ = getEdgeLayerBaseZIndex()
if (pointerHoverEdgeId === edge.id) {
applyEdgeHoverStyle(edge)
edge.setZIndex(baseZ + 1)
return
}
applyEdgeDefaultStyle(edge)
edge.setZIndex(getEdgeInitialZIndex(edge))
}
这里的关键点是:
- 当前悬浮边的层级永远设置成"现有最大层级 + 1"。
- 非悬浮边恢复默认样式,并回到自己的初始层级。

4. 保留边级事件处理正常路径
正常情况下,仍然优先使用 X6 自己的边事件:
javascript
graph.on('edge:mouseenter', ({ edge }) => {
if (edge.shape === 'er-relationship') {
syncHoverFallback(edge.id)
syncEdgeState(edge)
}
})
graph.on('edge:mouseleave', ({ edge }) => {
if (pointerHoverEdgeId === edge.id) {
pointerHoverEdgeId = null
}
syncEdgeState(edge)
})
这部分负责处理绝大多数正常交互路径。
5. 用容器级 mousemove 做兜底
真正解决问题的关键,在于增加了容器级监听。
当边的层级变化后,如果某次 edge:mouseleave 没有触发,就由容器上的 mousemove 去判断"鼠标现在到底悬浮在哪个元素上",然后主动修正状态:
scss
const handleContainerMouseMove = (event: MouseEvent) => {
const nextEdgeId = resolveHoverEdgeId(event.target)
syncHoverFallback(nextEdgeId)
}
其中 resolveHoverEdgeId 会通过 graph.findViewByElem(target) 反查当前命中的元素是否属于某条边。
而 syncHoverFallback 的职责是:
- 记录新的悬浮边
- 找到上一条悬浮边
- 如果上一条边不该再悬浮了,就主动把它恢复默认状态
这就把"等待 mouseleave 通知我结束"改成了"我每次都根据当前真实命中结果来纠正状态",稳定性会高很多。
6. 鼠标离开整个容器时,统一清空悬浮状态
除了 mousemove,还补了两个离开容器的兜底:
csharp
container.addEventListener('mouseleave', handleContainerMouseLeave)
graph.on('graph:mouseleave', handleContainerMouseLeave)
这样当鼠标直接离开画布区域时,也能确保悬浮态被清掉,不会把高亮残留在最后一条边上。
7. 在 dispose 时清理容器事件
因为这次新增了原生 DOM 事件监听,所以销毁时也要把它们收掉:
ini
const originalDispose = graph.dispose.bind(graph)
let hoverFallbackDisposed = false
graph.dispose = (...args: any[]) => {
if (!hoverFallbackDisposed) {
container.removeEventListener('mousemove', handleContainerMouseMove)
container.removeEventListener('mouseleave', handleContainerMouseLeave)
graph.off('graph:mouseleave', handleContainerMouseLeave)
hoverFallbackDisposed = true
}
originalDispose(...args)
}
这是一个小细节,但很重要。否则组件反复挂载/卸载后,事件可能重复绑定。