feat: add session tree search functionality
- Add useTreeSearch composable with filtering logic - Implement search bar with Ctrl+F shortcut - Show search results with ancestor paths - Add search match indicator (cyan dot) - Display "no results" message when search is empty - Support clear search with X button Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<string | null>(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<string | null>(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<ContextMenuItem[]>(() => {
|
||||
if (!contextMenuNodeId.value) return []
|
||||
@@ -64,6 +74,9 @@ const contextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
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) {
|
||||
<button
|
||||
@click="showNewFolderDialog = true"
|
||||
class="flex items-center gap-1.5 px-2.5 py-1.5 rounded text-xs text-slate-300 hover:bg-slate-800 transition-colors"
|
||||
title="新建文件夹"
|
||||
title="新建文件夹 (Ctrl+N)"
|
||||
>
|
||||
<FolderPlus class="w-4 h-4" />
|
||||
文件夹
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="px-2 py-2 border-b border-slate-700">
|
||||
<div class="relative">
|
||||
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="搜索会话... (Ctrl+F)"
|
||||
class="tree-search-input w-full pl-8 pr-8 py-1.5 rounded bg-slate-800 border border-slate-700 text-slate-200 text-sm placeholder-slate-500 focus:border-cyan-500 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
v-if="searchQuery"
|
||||
@click="clearSearch"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 text-slate-500 hover:text-slate-300 transition-colors"
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-2">
|
||||
<div v-if="searchQuery && rootNodes.length === 0" class="text-center py-8 text-slate-500 text-sm">
|
||||
未找到匹配的会话
|
||||
</div>
|
||||
<SessionTreeNode
|
||||
v-for="node in rootNodes"
|
||||
:key="node.id"
|
||||
|
||||
@@ -32,6 +32,7 @@ const connection = computed(() => {
|
||||
|
||||
// Drag-drop functionality
|
||||
const dragHandlers = inject<any>('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() {
|
||||
|
||||
<span class="text-sm truncate flex-1">{{ node.name }}</span>
|
||||
|
||||
<!-- Search match indicator -->
|
||||
<span
|
||||
v-if="connection"
|
||||
v-if="isSearchMatch(node.id)"
|
||||
class="w-1.5 h-1.5 rounded-full bg-cyan-400 flex-shrink-0"
|
||||
title="搜索匹配"
|
||||
/>
|
||||
|
||||
<span
|
||||
v-else-if="connection"
|
||||
class="w-2 h-2 rounded-full bg-slate-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user