feat(GraphCanvas): 添加关键词高亮功能 #260

新增highlightKeywords属性用于根据关键词高亮节点
添加高亮状态样式和动画效果
实现关键词匹配和高亮清除功能
This commit is contained in:
Wenjie Zhang 2025-09-11 02:44:35 +08:00
parent edfa34f60f
commit 95b084ec69
2 changed files with 95 additions and 3 deletions

View File

@ -32,7 +32,8 @@ const props = defineProps({
nodeStyleOptions: { type: Object, default: () => ({}) },
edgeStyleOptions: { type: Object, default: () => ({}) },
enableFocusNeighbor: { type: Boolean, default: true },
sizeByDegree: { type: Boolean, default: true }
sizeByDegree: { type: Boolean, default: true },
highlightKeywords: { type: Array, default: () => [] }
})
const emit = defineEmits(['ready', 'node-click', 'edge-click', 'canvas-click', 'data-rendered'])
@ -153,6 +154,17 @@ function initGraph() {
state: props.nodeStyleOptions.state || {
hidden: { opacity: 0.15, 'label-text-opacity': 0 },
focus: { opacity: 1, stroke: '#2563eb', lineWidth: 2.5, shadowColor: '#60a5fa', shadowBlur: 16 },
highlighted: {
opacity: 1,
stroke: '#ff0000',
lineWidth: 4,
shadowColor: '#ff0000',
shadowBlur: 30,
'label-text-fill': '#ff0000',
'label-text-font-weight': 'bold',
'label-text-size': 20,
fill: '#ffcccc',
},
},
},
edge: {
@ -253,11 +265,55 @@ function setGraphData() {
const data = formatData()
graphInstance.setData(data)
graphInstance.render()
//
setTimeout(() => {
applyHighlightKeywords()
emit('data-rendered')
}, 100)
}
//
function applyHighlightKeywords() {
if (!graphInstance || !props.highlightKeywords || props.highlightKeywords.length === 0) return
const { nodes } = graphInstance.getData()
const updates = {}
nodes.forEach(node => {
const nodeLabel = node.data.label || node.data[props.labelField] || String(node.id)
const shouldHighlight = props.highlightKeywords.some(keyword =>
keyword.trim() !== '' && nodeLabel.toLowerCase().includes(keyword.toLowerCase())
)
if (shouldHighlight) {
updates[node.id] = ['highlighted']
}
})
if (Object.keys(updates).length > 0) {
graphInstance.setElementState(updates)
graphInstance.draw()
}
}
//
function clearHighlights() {
if (!graphInstance) return
const { nodes } = graphInstance.getData()
const updates = {}
nodes.forEach(node => {
updates[node.id] = []
})
if (Object.keys(updates).length > 0) {
graphInstance.setElementState(updates)
graphInstance.draw()
}
}
function renderGraph() {
if (!graphInstance) initGraph()
setGraphData()
@ -316,6 +372,14 @@ watch(() => props.graphData, () => {
renderTimeout = setTimeout(() => setGraphData(), 50)
}, { deep: true })
//
watch(() => props.highlightKeywords, () => {
if (graphInstance) {
clearHighlights()
setTimeout(() => applyHighlightKeywords(), 50)
}
}, { deep: true })
onMounted(() => {
// ResizeObserver
if (window.ResizeObserver) {
@ -343,7 +407,17 @@ onUnmounted(() => {
})
//
defineExpose({ refreshGraph, fitView, fitCenter, getInstance, focusNode, clearFocus, setData: setGraphData })
defineExpose({
refreshGraph,
fitView,
fitCenter,
getInstance,
focusNode,
clearFocus,
setData: setGraphData,
applyHighlightKeywords,
clearHighlights
})
</script>
<style lang="less">
@ -389,4 +463,21 @@ defineExpose({ refreshGraph, fitView, fitCenter, getInstance, focusNode, clearFo
}
}
/* 高亮节点的脉冲动画效果 */
@keyframes highlightPulse {
0% {
filter: brightness(1);
}
50% {
filter: brightness(1.3) drop-shadow(0 0 8px rgba(255, 0, 0, 0.8));
}
100% {
filter: brightness(1);
}
}
.highlight-animation {
animation: highlightPulse 2s infinite ease-in-out;
}
</style>

View File

@ -31,6 +31,7 @@
<GraphCanvas
ref="graphRef"
:graph-data="graphData"
:highlight-keywords="[state.searchInput]"
>
<template #top>
<div class="actions">
@ -143,7 +144,7 @@ LIMIT $num</code></pre>
import { computed, onMounted, reactive, ref, h } from 'vue';
import { message } from 'ant-design-vue';
import { useConfigStore } from '@/stores/config';
import { UploadOutlined, SyncOutlined, GlobalOutlined, InfoCircleOutlined, SearchOutlined, ReloadOutlined, LoadingOutlined } from '@ant-design/icons-vue';
import { UploadOutlined, SyncOutlined, GlobalOutlined, InfoCircleOutlined, SearchOutlined, ReloadOutlined, LoadingOutlined, HighlightOutlined } from '@ant-design/icons-vue';
import HeaderComponent from '@/components/HeaderComponent.vue';
import { neo4jApi } from '@/apis/graph_api';
import { useUserStore } from '@/stores/user';