diff --git a/frontend/src/components/SessionTree.vue b/frontend/src/components/SessionTree.vue index 600b8a7..33e35d6 100644 --- a/frontend/src/components/SessionTree.vue +++ b/frontend/src/components/SessionTree.vue @@ -4,10 +4,11 @@ import { useSessionTreeStore } from '../stores/sessionTree' import { useWorkspaceStore } from '../stores/workspace' import { useTreeDragDrop } from '../composables/useTreeDragDrop' import { useKeyboardShortcuts } from '../composables/useKeyboardShortcuts' +import { useTreeSearch } from '../composables/useTreeSearch' import SessionTreeNode from './SessionTreeNode.vue' import ContextMenu from './ContextMenu.vue' import type { ContextMenuItem } from './ContextMenu.vue' -import { FolderPlus, Edit2, Trash2 } from 'lucide-vue-next' +import { FolderPlus, Edit2, Trash2, Search, X } from 'lucide-vue-next' const treeStore = useSessionTreeStore() const workspaceStore = useWorkspaceStore() @@ -18,13 +19,22 @@ const showRenameDialog = ref(false) const renameNodeId = ref(null) const renameValue = ref('') +// Search functionality +const nodesRef = computed(() => treeStore.nodes) +const { searchQuery, filteredNodes, clearSearch, isSearchMatch } = useTreeSearch(nodesRef) + // Context menu const showContextMenu = ref(false) const contextMenuX = ref(0) const contextMenuY = ref(0) const contextMenuNodeId = ref(null) -const rootNodes = computed(() => treeStore.rootNodes) +const rootNodes = computed(() => { + const nodes = searchQuery.value ? filteredNodes.value : treeStore.nodes + return nodes + .filter(n => n.parentId === null) + .sort((a, b) => a.order - b.order) +}) const contextMenuItems = computed(() => { if (!contextMenuNodeId.value) return [] @@ -64,6 +74,9 @@ const contextMenuItems = computed(() => { const dragHandlers = useTreeDragDrop() provide('dragHandlers', dragHandlers) +// Provide search match checker +provide('isSearchMatch', isSearchMatch) + // Keyboard shortcuts useKeyboardShortcuts([ { @@ -89,6 +102,14 @@ useKeyboardShortcuts([ showNewFolderDialog.value = true }, }, + { + key: 'f', + ctrl: true, + handler: () => { + const searchInput = document.querySelector('.tree-search-input') as HTMLInputElement + searchInput?.focus() + }, + }, ]) function handleNodeClick(nodeId: string) { @@ -162,14 +183,37 @@ function deleteNode(nodeId: string) { + +
+
+ + + +
+
+
+
+ 未找到匹配的会话 +
{ // Drag-drop functionality const dragHandlers = inject('dragHandlers', null) +const isSearchMatch = inject<(nodeId: string) => boolean>('isSearchMatch', () => false) const isDragging = computed(() => dragHandlers?.draggedNode.value?.id === props.node.id @@ -138,8 +139,15 @@ function handleDragEnd() { {{ node.name }} + + +
diff --git a/frontend/src/composables/useTreeSearch.ts b/frontend/src/composables/useTreeSearch.ts new file mode 100644 index 0000000..d9b3d1f --- /dev/null +++ b/frontend/src/composables/useTreeSearch.ts @@ -0,0 +1,55 @@ +import { ref, computed, type Ref } from 'vue' +import type { SessionTreeNode } from '../types/sessionTree' + +export function useTreeSearch(nodesRef: Ref) { + const searchQuery = ref('') + const searchResults = ref>(new Set()) + + const filteredNodes = computed(() => { + const nodes = nodesRef.value + + if (!searchQuery.value.trim()) { + return nodes + } + + const query = searchQuery.value.toLowerCase() + const results = new Set() + const matchedNodes = new Set() + + // Find all matching nodes + nodes.forEach(node => { + if (node.name.toLowerCase().includes(query)) { + matchedNodes.add(node.id) + results.add(node.id) + + // Add all ancestors to results + let current = node + while (current.parentId) { + results.add(current.parentId) + current = nodes.find(n => n.id === current.parentId)! + } + } + }) + + searchResults.value = matchedNodes + + // Filter nodes to only show matched paths + return nodes.filter(node => results.has(node.id)) + }) + + function clearSearch() { + searchQuery.value = '' + searchResults.value.clear() + } + + function isSearchMatch(nodeId: string): boolean { + return searchResults.value.has(nodeId) + } + + return { + searchQuery, + filteredNodes, + clearSearch, + isSearchMatch, + } +}