From 8be5e478e0719199bbb3314d78afb135b60d953a Mon Sep 17 00:00:00 2001 From: Feynman Date: Mon, 27 Jul 2026 15:05:08 +0800 Subject: [PATCH] fix: implement node highlighting feature in Canvas component - Added a timer to highlight located nodes for a specified duration. - Introduced CSS styles for visual feedback on locating nodes. - Updated the dataflow store to manage the located node state. - Enhanced the locateNode function to reset the highlight after the duration. - Adjusted related components to reflect the new highlighting state. --- packages/dag/src/Canvas.vue | 22 ++++- packages/dag/src/MonitorView.vue | 2 +- packages/dag/src/components/BaseNode.vue | 86 +++++++++++++++++++ .../src/components/elements/CanvasNode.vue | 16 +++- packages/dag/src/components/monitor/Node.vue | 10 ++- packages/dag/src/stores/dataflow.store.ts | 3 + 6 files changed, 131 insertions(+), 8 deletions(-) diff --git a/packages/dag/src/Canvas.vue b/packages/dag/src/Canvas.vue index 32b626300..e775a8be5 100644 --- a/packages/dag/src/Canvas.vue +++ b/packages/dag/src/Canvas.vue @@ -272,6 +272,10 @@ onMounted(() => { onUnmounted(() => { cleanupKeyboardShortcuts() dataflowStore.unregisterVueFlowUpdateCallback() + if (locatedNodeHighlightTimer) { + clearTimeout(locatedNodeHighlightTimer) + locatedNodeHighlightTimer = null + } leftPanelResizeObserver?.disconnect() leftPanelMutationObserver?.disconnect() rightPanelResizeObserver?.disconnect() @@ -315,6 +319,8 @@ const ZOOM_STEP = 0.1 const MIN_ZOOM = 0.1 const MAX_ZOOM = 10 const ZOOM_PRESETS = [0.25, 0.5, 0.75, 1, 2] +const LOCATED_NODE_HIGHLIGHT_DURATION = 1800 +let locatedNodeHighlightTimer: ReturnType | null = null const zoomPercentage = computed(() => Math.round(viewport.value.zoom * 100)) const previousZoom = ref(1) @@ -806,13 +812,25 @@ provide('clearDragHelperLines', () => { helperLineVertical.value = undefined }) -function locateNode(nodeId: string) { +async function locateNode(nodeId: string) { const node = vueFlow.findNode(nodeId) if (!node) return + dataflowStore.locatedNodeId = '' + await nextTick() + dataflowStore.locatedNodeId = nodeId + + if (locatedNodeHighlightTimer) clearTimeout(locatedNodeHighlightTimer) + locatedNodeHighlightTimer = setTimeout(() => { + if (dataflowStore.locatedNodeId === nodeId) { + dataflowStore.locatedNodeId = '' + } + locatedNodeHighlightTimer = null + }, LOCATED_NODE_HIGHLIGHT_DURATION) + fitViewWithOffset({ nodes: [nodeId], duration: 300, - maxZoom: viewport.value.zoom, + maxZoom: 1, }) } diff --git a/packages/dag/src/MonitorView.vue b/packages/dag/src/MonitorView.vue index 65b9ce3d2..6b4533324 100644 --- a/packages/dag/src/MonitorView.vue +++ b/packages/dag/src/MonitorView.vue @@ -44,7 +44,6 @@ const taskRecord = ref({ total: 0, items: [] as any[] }) const timeFormat = ref('HH:mm:ss') const nodeDetailDialog = ref(false) const nodeDetailDialogId = ref('') -const canvasRef = ref(null) const { t } = useI18n() const { @@ -56,6 +55,7 @@ const { formScope, isSaving, syncTypeLabel, + canvasRef, taskOperationsRef, consoleRef, skipErrorRef, diff --git a/packages/dag/src/components/BaseNode.vue b/packages/dag/src/components/BaseNode.vue index f48947447..63c79978e 100644 --- a/packages/dag/src/components/BaseNode.vue +++ b/packages/dag/src/components/BaseNode.vue @@ -212,6 +212,92 @@ $iconSize: 24px; pointer-events: none; }*/ } + +.df-node-wrap.is-locating { + --canvas-node-locate-stroke: rgba(44, 101, 255, 0.64); + --canvas-node-locate-ring: rgba(44, 101, 255, 0.2); + --canvas-node-locate-glow: rgba(44, 101, 255, 0.16); + + position: relative; + z-index: 9; + + &::before, + &::after { + content: ''; + position: absolute; + inset: -4px; + border-radius: 18px; + pointer-events: none; + } + + &::before { + border: 1px solid var(--canvas-node-locate-stroke); + animation: canvas-node-locate-trace 1.8s cubic-bezier(0.16, 1, 0.3, 1); + } + + &::after { + box-shadow: + 0 0 0 0 var(--canvas-node-locate-ring), + 0 10px 24px var(--canvas-node-locate-glow); + animation: canvas-node-locate-halo 1.8s cubic-bezier(0.16, 1, 0.3, 1); + } +} + +html.dark .df-node-wrap.is-locating { + --canvas-node-locate-stroke: rgba(92, 140, 255, 0.72); + --canvas-node-locate-ring: rgba(92, 140, 255, 0.28); + --canvas-node-locate-glow: rgba(92, 140, 255, 0.24); +} + +@keyframes canvas-node-locate-trace { + 0% { + opacity: 0; + transform: scale(0.985); + } + 14% { + opacity: 1; + transform: scale(1); + } + 100% { + opacity: 0; + transform: scale(1.035); + } +} + +@keyframes canvas-node-locate-halo { + 0% { + opacity: 0; + transform: scale(0.98); + } + 16% { + opacity: 1; + } + 68% { + opacity: 0.42; + } + 100% { + opacity: 0; + transform: scale(1.055); + box-shadow: + 0 0 0 14px rgba(44, 101, 255, 0), + 0 10px 24px rgba(44, 101, 255, 0); + } +} + +@media (prefers-reduced-motion: reduce) { + .df-node-wrap.is-locating { + &::before { + animation: none; + opacity: 1; + } + + &::after { + animation: none; + box-shadow: 0 0 0 4px var(--canvas-node-locate-ring); + } + } +} + .df-menu-list { margin: -6px; .df-menu-item { diff --git a/packages/dag/src/components/elements/CanvasNode.vue b/packages/dag/src/components/elements/CanvasNode.vue index e93b554e4..31e10dc9c 100644 --- a/packages/dag/src/components/elements/CanvasNode.vue +++ b/packages/dag/src/components/elements/CanvasNode.vue @@ -28,9 +28,14 @@ const hasNodeError = computed(() => store.getters['dataflow/hasNodeError']) const ins = computed(() => props.data?.__Ctor || {}) const wrapClass = computed(() => { - if (dataflowStore.selectedNode?.id === props.data.id || props.selected) - return 'border-primary' - return '' + const list: string[] = [] + if (dataflowStore.selectedNode?.id === props.data.id || props.selected) { + list.push('border-primary') + } + if (dataflowStore.locatedNodeId === props.data.id) { + list.push('is-locating') + } + return list }) const nodeClass = computed(() => { @@ -216,6 +221,11 @@ const canBeTarget = computed(() => { .df-node-wrap { z-index: 5; outline: none; + + &.is-locating { + z-index: 9; + } + &.can-be-connected { .node-anchor.input { display: flex; diff --git a/packages/dag/src/components/monitor/Node.vue b/packages/dag/src/components/monitor/Node.vue index b18c777c7..309809900 100644 --- a/packages/dag/src/components/monitor/Node.vue +++ b/packages/dag/src/components/monitor/Node.vue @@ -420,8 +420,14 @@ const alarmCls = computed(() => ) const wrapClass = computed(() => { - if (dataflowStore.selectedNode?.id === props.data.id) return 'border-primary' - return '' + const list: string[] = [] + if (dataflowStore.selectedNode?.id === props.data.id) { + list.push('border-primary') + } + if (dataflowStore.locatedNodeId === props.data.id) { + list.push('is-locating') + } + return list }) const nodeClass = computed(() => { diff --git a/packages/dag/src/stores/dataflow.store.ts b/packages/dag/src/stores/dataflow.store.ts index aca6851e9..2c8e07af8 100644 --- a/packages/dag/src/stores/dataflow.store.ts +++ b/packages/dag/src/stores/dataflow.store.ts @@ -106,6 +106,7 @@ export const useDataflowStore = defineStore('dataflow', () => { const pageVersion = ref(Date.now().toString()) const selectedNode = ref(null) const selectedNodeId = ref(null) + const locatedNodeId = ref('') const lastClickPosition = ref<[number, number]>([0, 0]) const stateIsReadonly = ref(false) const showSettings = ref(false) @@ -886,6 +887,7 @@ export const useDataflowStore = defineStore('dataflow', () => { // UI / 交互状态归零 selectedNode.value = null selectedNodeId.value = null + locatedNodeId.value = '' stateIsReadonly.value = false showSettings.value = false showConsole.value = false @@ -928,6 +930,7 @@ export const useDataflowStore = defineStore('dataflow', () => { patchDataflowDebounce, processorNodeTypes, selectedNode, + locatedNodeId, lastClickPosition, stateIsReadonly, taskSaving,