feat(GraphCanvas): 添加关键词高亮功能 #260
新增highlightKeywords属性用于根据关键词高亮节点 添加高亮状态样式和动画效果 实现关键词匹配和高亮清除功能
This commit is contained in:
parent
edfa34f60f
commit
95b084ec69
@ -32,7 +32,8 @@ const props = defineProps({
|
|||||||
nodeStyleOptions: { type: Object, default: () => ({}) },
|
nodeStyleOptions: { type: Object, default: () => ({}) },
|
||||||
edgeStyleOptions: { type: Object, default: () => ({}) },
|
edgeStyleOptions: { type: Object, default: () => ({}) },
|
||||||
enableFocusNeighbor: { type: Boolean, default: true },
|
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'])
|
const emit = defineEmits(['ready', 'node-click', 'edge-click', 'canvas-click', 'data-rendered'])
|
||||||
@ -153,6 +154,17 @@ function initGraph() {
|
|||||||
state: props.nodeStyleOptions.state || {
|
state: props.nodeStyleOptions.state || {
|
||||||
hidden: { opacity: 0.15, 'label-text-opacity': 0 },
|
hidden: { opacity: 0.15, 'label-text-opacity': 0 },
|
||||||
focus: { opacity: 1, stroke: '#2563eb', lineWidth: 2.5, shadowColor: '#60a5fa', shadowBlur: 16 },
|
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: {
|
edge: {
|
||||||
@ -253,11 +265,55 @@ function setGraphData() {
|
|||||||
const data = formatData()
|
const data = formatData()
|
||||||
graphInstance.setData(data)
|
graphInstance.setData(data)
|
||||||
graphInstance.render()
|
graphInstance.render()
|
||||||
|
|
||||||
|
// 应用关键词高亮
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
applyHighlightKeywords()
|
||||||
emit('data-rendered')
|
emit('data-rendered')
|
||||||
}, 100)
|
}, 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() {
|
function renderGraph() {
|
||||||
if (!graphInstance) initGraph()
|
if (!graphInstance) initGraph()
|
||||||
setGraphData()
|
setGraphData()
|
||||||
@ -316,6 +372,14 @@ watch(() => props.graphData, () => {
|
|||||||
renderTimeout = setTimeout(() => setGraphData(), 50)
|
renderTimeout = setTimeout(() => setGraphData(), 50)
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
|
// 监听关键词变化
|
||||||
|
watch(() => props.highlightKeywords, () => {
|
||||||
|
if (graphInstance) {
|
||||||
|
clearHighlights()
|
||||||
|
setTimeout(() => applyHighlightKeywords(), 50)
|
||||||
|
}
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// ResizeObserver 监听容器尺寸,自动重渲染
|
// ResizeObserver 监听容器尺寸,自动重渲染
|
||||||
if (window.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>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<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>
|
</style>
|
||||||
@ -31,6 +31,7 @@
|
|||||||
<GraphCanvas
|
<GraphCanvas
|
||||||
ref="graphRef"
|
ref="graphRef"
|
||||||
:graph-data="graphData"
|
:graph-data="graphData"
|
||||||
|
:highlight-keywords="[state.searchInput]"
|
||||||
>
|
>
|
||||||
<template #top>
|
<template #top>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
@ -143,7 +144,7 @@ LIMIT $num</code></pre>
|
|||||||
import { computed, onMounted, reactive, ref, h } from 'vue';
|
import { computed, onMounted, reactive, ref, h } from 'vue';
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import { useConfigStore } from '@/stores/config';
|
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 HeaderComponent from '@/components/HeaderComponent.vue';
|
||||||
import { neo4jApi } from '@/apis/graph_api';
|
import { neo4jApi } from '@/apis/graph_api';
|
||||||
import { useUserStore } from '@/stores/user';
|
import { useUserStore } from '@/stores/user';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user