|
|
@@ -360,6 +360,11 @@ let currentRoot = null
|
|
|
// 处理节点点击
|
|
|
function handleNodeClick(event, d) {
|
|
|
event.stopPropagation()
|
|
|
+ // 锁定状态下点击节点无效果,但提醒用户
|
|
|
+ if (store.lockedHoverNodeId) {
|
|
|
+ shakeLockButton()
|
|
|
+ return
|
|
|
+ }
|
|
|
store.selectNode(d)
|
|
|
}
|
|
|
|
|
|
@@ -905,6 +910,11 @@ function renderWalkedLayer() {
|
|
|
.style('cursor', 'pointer')
|
|
|
.on('click', (event, d) => {
|
|
|
event.stopPropagation()
|
|
|
+ // 锁定状态下点击节点无效果,但提醒用户
|
|
|
+ if (store.lockedHoverNodeId) {
|
|
|
+ shakeLockButton()
|
|
|
+ return
|
|
|
+ }
|
|
|
store.selectNode(d)
|
|
|
})
|
|
|
|
|
|
@@ -938,9 +948,6 @@ function renderWalkedLayer() {
|
|
|
setupHoverHandlers()
|
|
|
}
|
|
|
|
|
|
-// 锁定按钮相关
|
|
|
-let showButtonTimer = null // 延迟显示按钮的定时器
|
|
|
-
|
|
|
// 在节点文字后面添加锁定按钮(作为独立的 tspan)
|
|
|
function showLockButton(nodeEl, immediate = false) {
|
|
|
if (!nodeEl) return
|
|
|
@@ -949,13 +956,19 @@ function showLockButton(nodeEl, immediate = false) {
|
|
|
const textEl = node.select('text')
|
|
|
if (textEl.empty()) return
|
|
|
|
|
|
+ // 获取当前节点 ID
|
|
|
+ const nodeData = node.datum()
|
|
|
+ const currentNodeId = nodeData?.data?.id || nodeData?.id
|
|
|
+
|
|
|
+ // 判断当前节点是否是已锁定的节点
|
|
|
+ const isThisNodeLocked = store.lockedHoverNodeId && store.lockedHoverNodeId === currentNodeId
|
|
|
+
|
|
|
// 如果已有按钮,只更新状态
|
|
|
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)
|
|
|
+ btn.text(isThisNodeLocked ? ' 🔓解锁' : ' 🔒锁定')
|
|
|
+ .attr('fill', isThisNodeLocked ? '#f6ad55' : '#63b3ed')
|
|
|
+ if (!isThisNodeLocked) startBreathingAnimation(btn)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
@@ -966,42 +979,25 @@ function showLockButton(nodeEl, immediate = false) {
|
|
|
d3.select(svgRef.value).selectAll('.lock-btn').remove()
|
|
|
}
|
|
|
|
|
|
- const isLocked = !!store.lockedHoverNodeId
|
|
|
-
|
|
|
// 添加按钮 tspan(紧跟在文字后面)
|
|
|
const btn = textEl.append('tspan')
|
|
|
.attr('class', 'lock-btn')
|
|
|
- .attr('fill', isLocked ? '#f6ad55' : '#63b3ed')
|
|
|
+ .attr('fill', isThisNodeLocked ? '#f6ad55' : '#63b3ed')
|
|
|
.attr('font-weight', 'bold')
|
|
|
.style('cursor', 'pointer')
|
|
|
- .text(isLocked ? ' 🔓解锁' : ' 🔒锁定')
|
|
|
+ .text(isThisNodeLocked ? ' 🔓解锁' : ' 🔒锁定')
|
|
|
.on('click', (e) => {
|
|
|
e.stopPropagation()
|
|
|
handleLockClick()
|
|
|
})
|
|
|
|
|
|
// 呼吸灯动画(未锁定时)
|
|
|
- if (!isLocked) {
|
|
|
+ if (!isThisNodeLocked) {
|
|
|
startBreathingAnimation(btn)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 如果是立即显示,不需要延迟
|
|
|
- if (immediate) {
|
|
|
- createBtn()
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // 取消之前的显示定时器
|
|
|
- if (showButtonTimer) {
|
|
|
- clearTimeout(showButtonTimer)
|
|
|
- }
|
|
|
-
|
|
|
- // 延迟 400ms 后显示
|
|
|
- showButtonTimer = setTimeout(() => {
|
|
|
- createBtn()
|
|
|
- showButtonTimer = null
|
|
|
- }, 400)
|
|
|
+ createBtn()
|
|
|
}
|
|
|
|
|
|
// 呼吸灯动画(只有按钮部分,蓝色呼吸)
|
|
|
@@ -1023,26 +1019,47 @@ function startBreathingAnimation(btn) {
|
|
|
|
|
|
// 隐藏锁定按钮
|
|
|
function hideLockButton() {
|
|
|
- if (showButtonTimer) {
|
|
|
- clearTimeout(showButtonTimer)
|
|
|
- showButtonTimer = null
|
|
|
- }
|
|
|
- // 只清除当前 SVG 内的按钮
|
|
|
if (svgRef.value) {
|
|
|
d3.select(svgRef.value).selectAll('.lock-btn').interrupt().remove()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// 抖动锁定按钮(提醒用户需要先解锁)
|
|
|
+function shakeLockButton() {
|
|
|
+ d3.selectAll('.lock-btn')
|
|
|
+ .interrupt()
|
|
|
+ .attr('fill', '#fc8181') // 红色警告
|
|
|
+ .transition().duration(50).attr('dx', 3)
|
|
|
+ .transition().duration(50).attr('dx', -3)
|
|
|
+ .transition().duration(50).attr('dx', 3)
|
|
|
+ .transition().duration(50).attr('dx', -3)
|
|
|
+ .transition().duration(50).attr('dx', 0)
|
|
|
+ .transition().duration(200).attr('fill', '#f6ad55') // 恢复橙色
|
|
|
+}
|
|
|
+
|
|
|
// 处理锁定按钮点击
|
|
|
function handleLockClick() {
|
|
|
const startNodeId = store.selectedNodeId
|
|
|
- if (store.lockedHoverNodeId) {
|
|
|
+ const currentHoverNodeId = store.hoverNodeId
|
|
|
+
|
|
|
+ // 判断是解锁还是新锁定
|
|
|
+ if (store.lockedHoverNodeId && store.lockedHoverNodeId === currentHoverNodeId) {
|
|
|
+ // 点击的是当前锁定节点的按钮 → 解锁(弹出栈)
|
|
|
store.clearLockedHover()
|
|
|
- // 清除所有锁定按钮(两边都清除)
|
|
|
- d3.selectAll('.lock-btn').interrupt().remove()
|
|
|
- } else if (startNodeId) {
|
|
|
+ // 如果还有上一层锁定,更新按钮状态
|
|
|
+ if (store.lockedHoverNodeId) {
|
|
|
+ d3.selectAll('.lock-btn')
|
|
|
+ .interrupt()
|
|
|
+ .text(' 🔓解锁')
|
|
|
+ .attr('fill', '#f6ad55')
|
|
|
+ } else {
|
|
|
+ // 完全解锁,清除按钮
|
|
|
+ d3.selectAll('.lock-btn').interrupt().remove()
|
|
|
+ }
|
|
|
+ } else if (currentHoverNodeId) {
|
|
|
+ // 点击的是新 hover 节点的按钮 → 锁定新路径(压入栈)
|
|
|
store.lockCurrentHover(startNodeId)
|
|
|
- // 更新所有锁定按钮状态(两边同步)
|
|
|
+ // 更新按钮状态
|
|
|
d3.selectAll('.lock-btn')
|
|
|
.interrupt()
|
|
|
.text(' 🔓解锁')
|
|
|
@@ -1090,16 +1107,36 @@ function setupHoverHandlers() {
|
|
|
}
|
|
|
})
|
|
|
.on('mouseleave', () => {
|
|
|
- // 如果已锁定,不清除
|
|
|
- if (store.lockedHoverNodeId) return
|
|
|
+ // 调用 clearHover 恢复状态(如果已锁定会恢复到锁定路径)
|
|
|
store.clearHover()
|
|
|
- hideLockButton()
|
|
|
+
|
|
|
+ if (store.lockedHoverNodeId) {
|
|
|
+ // 已锁定:恢复锁定路径高亮,并在锁定节点上显示按钮
|
|
|
+ const svg = d3.select(svgRef.value)
|
|
|
+ const allNodes = svg.selectAll('.tree-node, .match-node, .walked-node')
|
|
|
+ const allLinks = svg.selectAll('.tree-link, .match-link, .walked-link')
|
|
|
+ const allLabels = svg.selectAll('.match-score, .walked-score')
|
|
|
+ // 恢复到纯锁定路径高亮(不传 lockedPath,因为这就是唯一的路径)
|
|
|
+ applyHoverHighlight(allNodes, allLinks, allLabels, store.hoverPathNodes, null)
|
|
|
+ // 在锁定节点上显示解锁按钮
|
|
|
+ const lockedNodeInfo = nodeElements[store.lockedHoverNodeId]
|
|
|
+ if (lockedNodeInfo?.element) {
|
|
|
+ showLockButton(lockedNodeInfo.element, true)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ hideLockButton()
|
|
|
+ }
|
|
|
})
|
|
|
}
|
|
|
|
|
|
// 匹配节点点击处理
|
|
|
function handleMatchNodeClick(event, d) {
|
|
|
event.stopPropagation()
|
|
|
+ // 锁定状态下点击节点无效果,但提醒用户
|
|
|
+ if (store.lockedHoverNodeId) {
|
|
|
+ shakeLockButton()
|
|
|
+ return
|
|
|
+ }
|
|
|
store.selectNode(d)
|
|
|
}
|
|
|
|
|
|
@@ -1250,11 +1287,15 @@ function updateHighlight() {
|
|
|
applyHighlight(svgRef.value, store.highlightedNodeIds, edgeSet, store.selectedNodeId)
|
|
|
}
|
|
|
|
|
|
-// 点击空白取消
|
|
|
+// 点击空白取消(锁定状态下无效果)
|
|
|
function handleSvgClick(event) {
|
|
|
+ // 锁定状态下,点击空白无效果
|
|
|
+ if (store.lockedHoverNodeId) return
|
|
|
+
|
|
|
const target = event.target
|
|
|
if (!target.closest('.tree-node') && !target.closest('.match-node') && !target.closest('.walked-node')) {
|
|
|
store.clearSelection()
|
|
|
+ hideLockButton()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1291,8 +1332,9 @@ watch([() => store.hoverPathNodes.size, () => store.hoverNodeId], () => {
|
|
|
const allLabels = svg.selectAll('.match-score, .walked-score')
|
|
|
|
|
|
if (store.hoverPathNodes.size > 0) {
|
|
|
- // 应用 hover 高亮
|
|
|
- applyHoverHighlight(allNodes, allLinks, allLabels, store.hoverPathNodes)
|
|
|
+ // 应用 hover 高亮(支持嵌套:传入锁定路径)
|
|
|
+ const lockedPath = store.lockedHoverPathNodes.size > 0 ? store.lockedHoverPathNodes : null
|
|
|
+ applyHoverHighlight(allNodes, allLinks, allLabels, store.hoverPathNodes, lockedPath)
|
|
|
|
|
|
// 如果是从 GraphView 触发的,缩放到显示完整路径
|
|
|
if (store.hoverSource === 'graph') {
|
|
|
@@ -1383,9 +1425,10 @@ function restoreLockedHover() {
|
|
|
const allLinks = svg.selectAll('.tree-link, .match-link, .walked-link')
|
|
|
const allLabels = svg.selectAll('.match-score, .walked-score')
|
|
|
|
|
|
- // 恢复高亮效果
|
|
|
+ // 恢复高亮效果(传入锁定路径)
|
|
|
if (store.hoverPathNodes.size > 0) {
|
|
|
- applyHoverHighlight(allNodes, allLinks, allLabels, store.hoverPathNodes)
|
|
|
+ const lockedPath = store.lockedHoverPathNodes.size > 0 ? store.lockedHoverPathNodes : null
|
|
|
+ applyHoverHighlight(allNodes, allLinks, allLabels, store.hoverPathNodes, lockedPath)
|
|
|
}
|
|
|
|
|
|
// 恢复锁定按钮
|