|
@@ -43,7 +43,7 @@
|
|
|
|
|
|
|
|
<!-- SVG 容器 -->
|
|
<!-- SVG 容器 -->
|
|
|
<div ref="containerRef" class="flex-1 overflow-hidden bg-base-100">
|
|
<div ref="containerRef" class="flex-1 overflow-hidden bg-base-100">
|
|
|
- <svg ref="svgRef" class="w-full h-full" @click="handleSvgClick"></svg>
|
|
|
|
|
|
|
+ <svg ref="svgRef" class="w-full h-full" @click="handleSvgClick" @dblclick="handleSvgDblClick"></svg>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -350,12 +350,21 @@ let nodeElements = {}
|
|
|
let baseNodeElements = {} // 基础节点(帖子树+匹配层),不含游走节点
|
|
let baseNodeElements = {} // 基础节点(帖子树+匹配层),不含游走节点
|
|
|
let currentRoot = null
|
|
let currentRoot = null
|
|
|
|
|
|
|
|
-// 处理节点点击
|
|
|
|
|
|
|
+// 处理节点单击(触发游走)
|
|
|
function handleNodeClick(event, d) {
|
|
function handleNodeClick(event, d) {
|
|
|
event.stopPropagation()
|
|
event.stopPropagation()
|
|
|
store.selectNode(d)
|
|
store.selectNode(d)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 处理节点双击(锁定hover)
|
|
|
|
|
+function handleNodeDblClick(event, d) {
|
|
|
|
|
+ event.stopPropagation()
|
|
|
|
|
+ const startNodeId = store.selectedNodeId
|
|
|
|
|
+ if (startNodeId) {
|
|
|
|
|
+ store.lockCurrentHover(startNodeId)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 渲染树
|
|
// 渲染树
|
|
|
function renderTree() {
|
|
function renderTree() {
|
|
|
const svg = d3.select(svgRef.value)
|
|
const svg = d3.select(svgRef.value)
|
|
@@ -455,6 +464,7 @@ function renderTree() {
|
|
|
.attr('transform', d => `translate(${d.x},${d.y})`)
|
|
.attr('transform', d => `translate(${d.x},${d.y})`)
|
|
|
.style('cursor', 'pointer')
|
|
.style('cursor', 'pointer')
|
|
|
.on('click', handleNodeClick)
|
|
.on('click', handleNodeClick)
|
|
|
|
|
+ .on('dblclick', handleNodeDblClick)
|
|
|
|
|
|
|
|
// 节点形状(使用统一配置)
|
|
// 节点形状(使用统一配置)
|
|
|
nodes.each(function(d) {
|
|
nodes.each(function(d) {
|
|
@@ -632,6 +642,7 @@ function renderMatchLayer(contentG, root, baseTreeHeight) {
|
|
|
.attr('transform', d => `translate(${d.x},${d.y})`)
|
|
.attr('transform', d => `translate(${d.x},${d.y})`)
|
|
|
.style('cursor', 'pointer')
|
|
.style('cursor', 'pointer')
|
|
|
.on('click', handleMatchNodeClick)
|
|
.on('click', handleMatchNodeClick)
|
|
|
|
|
+ .on('dblclick', handleMatchNodeDblClick)
|
|
|
|
|
|
|
|
// 匹配节点形状(使用统一配置)
|
|
// 匹配节点形状(使用统一配置)
|
|
|
matchNodes.each(function(d) {
|
|
matchNodes.each(function(d) {
|
|
@@ -900,6 +911,13 @@ function renderWalkedLayer() {
|
|
|
event.stopPropagation()
|
|
event.stopPropagation()
|
|
|
store.selectNode(d)
|
|
store.selectNode(d)
|
|
|
})
|
|
})
|
|
|
|
|
+ .on('dblclick', (event, d) => {
|
|
|
|
|
+ event.stopPropagation()
|
|
|
|
|
+ const startNodeId = store.selectedNodeId
|
|
|
|
|
+ if (startNodeId) {
|
|
|
|
|
+ store.lockCurrentHover(startNodeId)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
// 节点形状
|
|
// 节点形状
|
|
|
walkedNodeGroups.each(function(d) {
|
|
walkedNodeGroups.each(function(d) {
|
|
@@ -950,12 +968,21 @@ function setupHoverHandlers() {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 匹配节点点击处理
|
|
|
|
|
|
|
+// 匹配节点单击处理(触发游走)
|
|
|
function handleMatchNodeClick(event, d) {
|
|
function handleMatchNodeClick(event, d) {
|
|
|
event.stopPropagation()
|
|
event.stopPropagation()
|
|
|
store.selectNode(d)
|
|
store.selectNode(d)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 匹配节点双击处理(锁定hover)
|
|
|
|
|
+function handleMatchNodeDblClick(event, d) {
|
|
|
|
|
+ event.stopPropagation()
|
|
|
|
|
+ const startNodeId = store.selectedNodeId
|
|
|
|
|
+ if (startNodeId) {
|
|
|
|
|
+ store.lockCurrentHover(startNodeId)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ========== 详情显示格式化函数 ==========
|
|
// ========== 详情显示格式化函数 ==========
|
|
|
|
|
|
|
|
// 格式化字段名(camelCase/snake_case -> 中文/可读)
|
|
// 格式化字段名(camelCase/snake_case -> 中文/可读)
|
|
@@ -1103,7 +1130,7 @@ function updateHighlight() {
|
|
|
applyHighlight(svgRef.value, store.highlightedNodeIds, edgeSet, store.selectedNodeId)
|
|
applyHighlight(svgRef.value, store.highlightedNodeIds, edgeSet, store.selectedNodeId)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 点击空白取消激活
|
|
|
|
|
|
|
+// 单击空白取消游走
|
|
|
function handleSvgClick(event) {
|
|
function handleSvgClick(event) {
|
|
|
const target = event.target
|
|
const target = event.target
|
|
|
if (!target.closest('.tree-node') && !target.closest('.match-node') && !target.closest('.walked-node')) {
|
|
if (!target.closest('.tree-node') && !target.closest('.match-node') && !target.closest('.walked-node')) {
|
|
@@ -1111,6 +1138,14 @@ function handleSvgClick(event) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 双击空白取消hover锁定
|
|
|
|
|
+function handleSvgDblClick(event) {
|
|
|
|
|
+ const target = event.target
|
|
|
|
|
+ if (!target.closest('.tree-node') && !target.closest('.match-node') && !target.closest('.walked-node')) {
|
|
|
|
|
+ store.clearLockedHover()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 监听选中/高亮变化,统一更新
|
|
// 监听选中/高亮变化,统一更新
|
|
|
watch(() => store.selectedNodeId, (nodeId, oldNodeId) => {
|
|
watch(() => store.selectedNodeId, (nodeId, oldNodeId) => {
|
|
|
updateHighlight()
|
|
updateHighlight()
|