|
|
@@ -932,117 +932,111 @@ function renderWalkedLayer() {
|
|
|
}
|
|
|
|
|
|
// 锁定按钮相关
|
|
|
-let lockButtonG = null
|
|
|
-let hideButtonTimer = null
|
|
|
-
|
|
|
-// 创建锁定按钮(在主组中创建一次)
|
|
|
-function createLockButton() {
|
|
|
- if (!mainG) return
|
|
|
+let showButtonTimer = null // 延迟显示按钮的定时器
|
|
|
+
|
|
|
+// 在节点文字后面添加锁定按钮(作为独立的 tspan)
|
|
|
+function showLockButton(nodeEl, immediate = false) {
|
|
|
+ if (!nodeEl) return
|
|
|
+
|
|
|
+ const node = d3.select(nodeEl)
|
|
|
+ const textEl = node.select('text')
|
|
|
+ if (textEl.empty()) return
|
|
|
+
|
|
|
+ // 如果已有按钮,只更新状态
|
|
|
+ let btn = textEl.select('.lock-btn')
|
|
|
+ if (!btn.empty()) {
|
|
|
+ const isLocked = !!store.lockedHoverNodeId
|
|
|
+ btn.text(isLocked ? ' 🔓解锁' : ' 🔒锁定')
|
|
|
+ .attr('fill', isLocked ? '#f6ad55' : '#63b3ed')
|
|
|
+ if (!isLocked) startBreathingAnimation(btn)
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- // 移除旧按钮
|
|
|
- mainG.selectAll('.lock-button').remove()
|
|
|
+ // 创建按钮的函数
|
|
|
+ const createBtn = () => {
|
|
|
+ // 先清除其他节点的按钮
|
|
|
+ d3.selectAll('.lock-btn').remove()
|
|
|
+
|
|
|
+ const isLocked = !!store.lockedHoverNodeId
|
|
|
+
|
|
|
+ // 添加按钮 tspan(紧跟在文字后面)
|
|
|
+ const btn = textEl.append('tspan')
|
|
|
+ .attr('class', 'lock-btn')
|
|
|
+ .attr('fill', isLocked ? '#f6ad55' : '#63b3ed')
|
|
|
+ .attr('font-weight', 'bold')
|
|
|
+ .style('cursor', 'pointer')
|
|
|
+ .text(isLocked ? ' 🔓解锁' : ' 🔒锁定')
|
|
|
+ .on('click', (e) => {
|
|
|
+ e.stopPropagation()
|
|
|
+ handleLockClick()
|
|
|
+ })
|
|
|
|
|
|
- // 创建按钮组
|
|
|
- lockButtonG = mainG.append('g')
|
|
|
- .attr('class', 'lock-button')
|
|
|
- .style('display', 'none')
|
|
|
- .style('cursor', 'pointer')
|
|
|
- .on('click', (e) => {
|
|
|
- e.stopPropagation()
|
|
|
- const startNodeId = store.selectedNodeId
|
|
|
- if (store.lockedHoverNodeId) {
|
|
|
- // 已锁定,点击解锁
|
|
|
- store.clearLockedHover()
|
|
|
- hideLockButton()
|
|
|
- } else if (startNodeId) {
|
|
|
- // 未锁定,点击锁定
|
|
|
- store.lockCurrentHover(startNodeId)
|
|
|
- updateLockButtonIcon(true)
|
|
|
- }
|
|
|
- })
|
|
|
- .on('mouseenter', () => {
|
|
|
- // 鼠标进入按钮,取消隐藏定时器
|
|
|
- if (hideButtonTimer) {
|
|
|
- clearTimeout(hideButtonTimer)
|
|
|
- hideButtonTimer = null
|
|
|
- }
|
|
|
- })
|
|
|
- .on('mouseleave', () => {
|
|
|
- // 鼠标离开按钮,如果未锁定则隐藏
|
|
|
- if (!store.lockedHoverNodeId) {
|
|
|
- hideLockButton()
|
|
|
- }
|
|
|
- })
|
|
|
+ // 呼吸灯动画(未锁定时)
|
|
|
+ if (!isLocked) {
|
|
|
+ startBreathingAnimation(btn)
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 按钮背景(较大的点击区域)
|
|
|
- lockButtonG.append('rect')
|
|
|
- .attr('x', -20)
|
|
|
- .attr('y', -10)
|
|
|
- .attr('width', 40)
|
|
|
- .attr('height', 20)
|
|
|
- .attr('rx', 4)
|
|
|
- .attr('fill', '#2d3748')
|
|
|
- .attr('stroke', '#4a5568')
|
|
|
- .attr('stroke-width', 1)
|
|
|
+ // 如果是立即显示,不需要延迟
|
|
|
+ if (immediate) {
|
|
|
+ createBtn()
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- // 锁定状态文字
|
|
|
- lockButtonG.append('text')
|
|
|
- .attr('class', 'lock-icon')
|
|
|
- .attr('text-anchor', 'middle')
|
|
|
- .attr('dy', '0.35em')
|
|
|
- .attr('font-size', '10px')
|
|
|
- .attr('fill', '#fff')
|
|
|
- .text('锁定')
|
|
|
-}
|
|
|
+ // 取消之前的显示定时器
|
|
|
+ if (showButtonTimer) {
|
|
|
+ clearTimeout(showButtonTimer)
|
|
|
+ }
|
|
|
|
|
|
-// 更新锁定按钮图标
|
|
|
-function updateLockButtonIcon(isLocked) {
|
|
|
- if (!lockButtonG) return
|
|
|
- const text = lockButtonG.select('.lock-icon')
|
|
|
- text.text(isLocked ? '解锁' : '锁定')
|
|
|
- // 锁定状态用不同颜色
|
|
|
- lockButtonG.select('rect').attr('fill', isLocked ? '#744210' : '#2d3748')
|
|
|
+ // 延迟 400ms 后显示
|
|
|
+ showButtonTimer = setTimeout(() => {
|
|
|
+ createBtn()
|
|
|
+ showButtonTimer = null
|
|
|
+ }, 400)
|
|
|
}
|
|
|
|
|
|
-// 显示锁定按钮(紧贴节点右上方)
|
|
|
-function showLockButton(x, y) {
|
|
|
- if (!lockButtonG) return
|
|
|
-
|
|
|
- // 取消隐藏定时器
|
|
|
- if (hideButtonTimer) {
|
|
|
- clearTimeout(hideButtonTimer)
|
|
|
- hideButtonTimer = null
|
|
|
+// 呼吸灯动画(只有按钮部分,蓝色呼吸)
|
|
|
+function startBreathingAnimation(btn) {
|
|
|
+ function breathe() {
|
|
|
+ if (btn.empty() || !btn.node()) return
|
|
|
+ if (store.lockedHoverNodeId) return
|
|
|
+ btn
|
|
|
+ .transition()
|
|
|
+ .duration(800)
|
|
|
+ .attr('fill', '#90cdf4')
|
|
|
+ .transition()
|
|
|
+ .duration(800)
|
|
|
+ .attr('fill', '#63b3ed')
|
|
|
+ .on('end', breathe)
|
|
|
}
|
|
|
-
|
|
|
- // 更新图标状态
|
|
|
- updateLockButtonIcon(!!store.lockedHoverNodeId)
|
|
|
-
|
|
|
- // 按钮紧贴节点右侧(只偏移8px,确保鼠标能顺利移到按钮)
|
|
|
- lockButtonG
|
|
|
- .attr('transform', `translate(${x + 8}, ${y})`)
|
|
|
- .style('display', 'block')
|
|
|
+ breathe()
|
|
|
}
|
|
|
|
|
|
// 隐藏锁定按钮
|
|
|
function hideLockButton() {
|
|
|
- if (!lockButtonG) return
|
|
|
- lockButtonG.style('display', 'none')
|
|
|
+ if (showButtonTimer) {
|
|
|
+ clearTimeout(showButtonTimer)
|
|
|
+ showButtonTimer = null
|
|
|
+ }
|
|
|
+ d3.selectAll('.lock-btn').interrupt().remove()
|
|
|
}
|
|
|
|
|
|
-// 延迟隐藏锁定按钮(给用户移动到按钮的时间)
|
|
|
-function delayHideLockButton() {
|
|
|
- // 如果已锁定,不隐藏
|
|
|
- if (store.lockedHoverNodeId) return
|
|
|
-
|
|
|
- if (hideButtonTimer) {
|
|
|
- clearTimeout(hideButtonTimer)
|
|
|
- }
|
|
|
- hideButtonTimer = setTimeout(() => {
|
|
|
- if (!store.lockedHoverNodeId) {
|
|
|
- hideLockButton()
|
|
|
+// 处理锁定按钮点击
|
|
|
+function handleLockClick() {
|
|
|
+ const startNodeId = store.selectedNodeId
|
|
|
+ if (store.lockedHoverNodeId) {
|
|
|
+ store.clearLockedHover()
|
|
|
+ hideLockButton()
|
|
|
+ } else if (startNodeId) {
|
|
|
+ store.lockCurrentHover(startNodeId)
|
|
|
+ // 更新按钮显示
|
|
|
+ const btn = d3.select('.lock-btn')
|
|
|
+ if (!btn.empty()) {
|
|
|
+ btn.interrupt()
|
|
|
+ .text(' 🔓解锁')
|
|
|
+ .attr('fill', '#f6ad55')
|
|
|
}
|
|
|
- hideButtonTimer = null
|
|
|
- }, 200)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// 设置 hover 处理器(在所有元素创建后调用)
|
|
|
@@ -1053,43 +1047,42 @@ function setupHoverHandlers() {
|
|
|
const allNodes = svg.selectAll('.tree-node, .match-node, .walked-node')
|
|
|
const startNodeId = store.selectedNodeId
|
|
|
|
|
|
- // 创建锁定按钮
|
|
|
- createLockButton()
|
|
|
-
|
|
|
// 如果已有锁定状态,显示解锁按钮
|
|
|
if (store.lockedHoverNodeId) {
|
|
|
const lockedNodeInfo = nodeElements[store.lockedHoverNodeId]
|
|
|
- if (lockedNodeInfo) {
|
|
|
- showLockButton(lockedNodeInfo.x - 50, lockedNodeInfo.y - 25)
|
|
|
+ if (lockedNodeInfo?.element) {
|
|
|
+ showLockButton(lockedNodeInfo.element, true)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 添加 hover 处理器(路径计算由 store 统一处理)
|
|
|
allNodes
|
|
|
- .on('mouseenter', (event, d) => {
|
|
|
+ .on('mouseenter', function(event, d) {
|
|
|
const nodeId = d.data?.id || d.id
|
|
|
// 排除起始节点
|
|
|
if (nodeId === startNodeId) return
|
|
|
|
|
|
store.computeHoverPath(startNodeId, nodeId, 'post-tree')
|
|
|
|
|
|
- // 显示锁定按钮
|
|
|
- // 如果已锁定,按钮保持在锁定节点位置;否则跟随hover节点
|
|
|
- if (store.lockedHoverNodeId) {
|
|
|
- const lockedNodeInfo = nodeElements[store.lockedHoverNodeId]
|
|
|
- if (lockedNodeInfo) {
|
|
|
- showLockButton(lockedNodeInfo.x - 50, lockedNodeInfo.y - 25)
|
|
|
- }
|
|
|
- } else {
|
|
|
- const nodeInfo = nodeElements[nodeId]
|
|
|
- if (nodeInfo && store.hoverPathNodes.size > 0) {
|
|
|
- showLockButton(nodeInfo.x - 50, nodeInfo.y - 25)
|
|
|
+ // 显示锁定按钮(在当前hover节点上)
|
|
|
+ if (store.hoverPathNodes.size > 0) {
|
|
|
+ // 如果已锁定,按钮显示在锁定节点上
|
|
|
+ if (store.lockedHoverNodeId) {
|
|
|
+ const lockedNodeInfo = nodeElements[store.lockedHoverNodeId]
|
|
|
+ if (lockedNodeInfo?.element) {
|
|
|
+ showLockButton(lockedNodeInfo.element, true)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 未锁定,按钮显示在当前hover节点上
|
|
|
+ showLockButton(this)
|
|
|
}
|
|
|
}
|
|
|
})
|
|
|
.on('mouseleave', () => {
|
|
|
+ // 如果已锁定,不清除
|
|
|
+ if (store.lockedHoverNodeId) return
|
|
|
store.clearHover()
|
|
|
- delayHideLockButton()
|
|
|
+ hideLockButton()
|
|
|
})
|
|
|
}
|
|
|
|