X6 中边悬浮置顶,规避 `mouseleave` 事件丢失问题

最近在这个 X6 ER 图示例里,我修了一个看起来不大、但实际很容易踩坑的交互问题:

  1. 鼠标悬浮到一条边上时,希望这条边显示到更上层,避免被其他元素压住。

这篇文章记录一下这个问题的成因,以及我最后采用的修复方案。

现象

在 ER 图里,边默认是普通灰色;鼠标移动到边上时,会切换为高亮色。

但如果只做最直接的悬浮效果,会遇到两个问题:

  1. 当前悬浮的边可能仍然被别的节点或边压着,视觉上不够明显。
  2. 如果在 mouseenter 里把边提到更高层,DOM / View 层级发生变化后,原本那条边的 mouseleave 事件有概率丢失。

丢失之后的表现是:

  1. 边已经没有被鼠标悬浮,但仍然保持高亮。
  2. 再移动到其他边时,会出现多个边状态不一致的情况。

修复方案

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. 当前悬浮边的层级永远设置成"现有最大层级 + 1"。
  2. 非悬浮边恢复默认样式,并回到自己的初始层级。

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 的职责是:

  1. 记录新的悬浮边
  2. 找到上一条悬浮边
  3. 如果上一条边不该再悬浮了,就主动把它恢复默认状态

这就把"等待 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)
}

这是一个小细节,但很重要。否则组件反复挂载/卸载后,事件可能重复绑定。

代码位置

github.com/adai212/X6D...

相关推荐
AlienZHOU2 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc5 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师6 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen7 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒7 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow7 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林8187 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang800787 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明7 小时前
第 6 章 组件进阶
前端·vue.js
江华森8 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端