Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions ui/src/workflow/common/NodeSearch.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<template>
<div>
<!-- 搜索遮罩层 -->
<Teleport to="body">
<div v-if="showSearch" class="search-mask" @click.self="closeSearch">
<div class="search-container">
<el-input
ref="searchInputRef"
v-model="searchText"
placeholder="搜索..."
:prefix-icon="Search"
clearable
@keyup.enter="handleSearch"
@keyup.esc="closeSearch"
>
<template #append>
<el-button @click="closeSearch">取消</el-button>
</template>
</el-input>
</div>
</div>
</Teleport>
</div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { Search } from '@element-plus/icons-vue'

// Props定义
interface Props {
onSearch?: (keyword: string) => void // 搜索回调
}

const props = withDefaults(defineProps<Props>(), {
useElementPlus: false,
onSearch: undefined,
})

// 状态
const showSearch = ref(false)
const searchText = ref('')
const searchInputRef = ref<any>(null)
const nativeInputRef = ref<HTMLInputElement | null>(null)

// 快捷键处理
const handleKeyDown = (e: KeyboardEvent) => {
// Ctrl+F 或 Cmd+F (Mac)
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
e.preventDefault() // 阻止浏览器默认搜索
openSearch()
}

// 按ESC关闭
if (e.key === 'Escape' && showSearch.value) {
closeSearch()
}
}

// 打开搜索
const openSearch = () => {
showSearch.value = true
searchText.value = ''

nextTick(() => {
searchInputRef.value?.focus()
})
}

// 关闭搜索
const closeSearch = () => {
showSearch.value = false
searchText.value = ''
}

// 执行搜索
const handleSearch = () => {
if (searchText.value.trim()) {
props.onSearch?.(searchText.value)
}
}

// 生命周期
onMounted(() => {
window.addEventListener('keydown', handleKeyDown)
})

onUnmounted(() => {
window.removeEventListener('keydown', handleKeyDown)
})
</script>

<style scoped>
.search-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
z-index: 9999;
padding-top: 20vh;
}

.search-container {
width: 500px;
max-width: 90%;
animation: slideDown 0.2s ease;
}

/* 原生输入框样式 */
.native-search {
display: flex;
gap: 8px;
background: white;
padding: 16px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.native-search input {
flex: 1;
padding: 10px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
outline: none;
}

.native-search input:focus {
border-color: #409eff;
}

.native-search button {
padding: 0 16px;
background: white;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}

.native-search button:hover {
border-color: #409eff;
color: #409eff;
}

.content {
padding: 20px;
}

.item {
padding: 8px;
border-bottom: 1px solid #eee;
}

@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code looks generally clean and well-structured. Here are some minor suggestions for optimization and considerations:

  1. TypeScript Typing: The props interface is defined using TypeScript type annotations for clarity.

  2. Arrow Functions: Arrow functions (()=>{}) can make the code slightly cleaner and more concise, especially when used in callback functions like .addEventListener() and .nextTick(). However, standard function declarations (function(){}) without an arrow might be suitable depending on personal preference or context.

  3. Comments: Comments can sometimes clutter the code and become outdated. Consider updating comments, removing unnecessary ones, or grouping similar comments together.

  4. Avoid Global Event Listeners: Although necessary for this specific functionality, consider encapsulating event handlers within component state management if possible to avoid adding global listeners that could potentially interfere with other components or lead to memory leaks under certain conditions.

  5. Optimize State Management: Ensure that the state variables are properly managed and updated only when necessary. For example, you might want to debounce updates to improve performance.

Overall, the code is straightforward and should work correctly as intended. If performance becomes a concern or additional features are needed, further optimizations can be made based on those requirements.

41 changes: 41 additions & 0 deletions ui/src/workflow/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<!-- 辅助工具栏 -->
<Control class="workflow-control" v-if="lf" :lf="lf"></Control>
<TeleportContainer :flow-id="flowId" />
<NodeSearch :on-search="onSearch"></NodeSearch>
</template>
<script setup lang="ts">
import LogicFlow from '@logicflow/core'
Expand All @@ -17,6 +18,8 @@ import { initDefaultShortcut } from '@/workflow/common/shortcut'
import Dagre from '@/workflow/plugins/dagre'
import { disconnectAll, getTeleport } from '@/workflow/common/teleport'
import { WorkflowMode } from '@/enums/application'
import { MsgSuccess, MsgWarning } from '@/utils/message'
import NodeSearch from '@/workflow/common/NodeSearch.vue'
const nodes: any = import.meta.glob('./nodes/**/index.ts', { eager: true })
const workflow_mode = inject('workflowMode') || WorkflowMode.Application
const loop_workflow_mode = inject('loopWorkflowMode') || WorkflowMode.ApplicationLoop
Expand Down Expand Up @@ -49,6 +52,44 @@ onUnmounted(() => {
const render = (data: any) => {
lf.value.render(data)
}
const searchQueue: Array<string> = []
const selectNode = (node: any) => {
lf.value.graphModel.selectNodeById(node.id)
lf.value.graphModel.transformModel.focusOn(
node.x,
node.y,
lf.value.container.clientWidth,
lf.value.container.clientHeight,
)
searchQueue.push(node.id)
}
const onSearch = (kw: string) => {
const graph_data = lf.value.getGraphData()
for (let index = 0; index < graph_data.nodes.length; index++) {
const node = graph_data.nodes[index]
let firstNode = null
if (node.properties.stepName.includes(kw)) {
if (!firstNode) {
firstNode = node
}

if (!searchQueue.includes(node.id)) {
selectNode(node)
break
}
}
if (index === graph_data.nodes.length - 1) {
searchQueue.length = 0
if (firstNode) {
selectNode(firstNode)
} else {
lf.value.graphModel.clearSelectElements()
MsgWarning('不存在的节点')
}
}
}
}

const renderGraphData = (data?: any) => {
const container: any = document.querySelector('#container')
if (container) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provided code looks mostly correct but there are a few areas where improvements can be made:

  1. Type Annotations: Ensure that all variables have appropriate type annotations to improve readability and maintainability.

  2. Comments and Documentation: Add comments explaining complex logic and consider adding documentation for functions that might not be immediately obvious.

  3. Vue Composition API: Use TypeScript with Vue's composition API correctly to ensure proper typing and function dependencies.

  4. Error Handling: Implement error handling for edge cases, such as when no matching nodes are found during a search.

Here’s an improved version of your code with some suggestions:

<!-- 辅助工具栏 -->
<Control class="workflow-control" v-if="lf" :lf="lf"></Control>
<TeleportContainer :flow-id="flowId" />
<NodeSearch :on-search="onSearch"></NodeSearch>
</template>

<script setup lang="ts">
import LogicFlow from '@logicflow/core'
import { initDefaultShortcut } from '@/workflow/common/shortcut'
import Dagre from '@/workflow/plugins/dagre'
import { disconnectAll, getTeleport } from '@/workflow/common/teleport'
import { WorkflowMode } from '@/enums/application'
import { MsgSuccess, MsgWarning } from '@/utils/message'

// Type definitions
interface Node {
  id: string;
  properties: Record<string, any>;
}

type GraphData = {
  nodes: Node[];
};

// Setup data...
const lf = ref(null);
const flowId = 'your-flow-id';
const workflow_mode = inject('workflowMode') || WorkflowMode.Application;
const loop_workflow_mode = inject('loopWorkflowMode') || WorkflowMode.Application;

// Search-related variables
const searchQueue: Set<string> = new Set();

const selectNode = (node: Node) => {
  lf.value.graphModel.selectNodeById(node.id);
  lf.value.graphModel.transformModel.focusOn(
    node.x,
    node.y,
    lf.value.container?.clientWidth ?? 0,
    lf.value.container?.clientHeight ?? 0,
  );
  searchQueue.add(node.id);
};

const onSearch = async (kw: string) => {
  try {
    const graph_data = await lf.value.getGraphData(); // Assuming this returns Promise<object>
    for (const node of graph_data.nodes) {
      if (
        node.properties &&
        node.properties.stepName.toLowerCase().includes(kw.toLowerCase())
      ) {
        await selectNode(node); // Await here since we change state inside selectNode
        return; // Only show the first match
      }
    }
    searchQueue.clear();
    lf.value.graphModel.clearSelectElements();
    MsgWarning('不存在的节点');
  } catch (error) {
    console.error("Failed to fetch graph data:", error);
    MsgWarning('获取图形数据失败,请重试。');
  }
};

const renderGraphData = (data?: GraphData) => {
  const container: HTMLElement | null = document.getElementById('container');
  if (container) {
    // Your rendering logic here
  }
};
</script>

<style scoped>
/* Your styles here */
</style>

Key Changes:

  • Type Annotations: Added Nodes and GraphData interfaces for better type safety.
  • Set for searchQueue: Used Set to automatically remove duplicates and simplify iteration.
  • Async/Await: Used async/await in selectNode after clearing searchQueue.
  • Error Handling: Caught promise rejections thrown by getGraphData() method call.

Expand Down