From 00a7e919f7db95aa4212d0f922bd1895a3f3459c Mon Sep 17 00:00:00 2001 From: cfq Date: Mon, 26 Jan 2026 18:03:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=96=87=E4=BB=B6=E6=A0=91):=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E5=90=8E=E7=AB=AF=E6=90=9C=E7=B4=A2=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=89=8D=E7=AB=AF=E6=90=9C=E7=B4=A2=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将文件搜索逻辑移至后端 services.js,支持文件夹名称匹配 - 重构前端搜索,使用防抖调用后端搜索 API - 搜索结果自动构建树形结构并展开所有匹配目录 - 移除前端递归过滤逻辑,提升大目录搜索性能 --- public/preload/services.js | 10 ++++ src/components/FileTree.vue | 72 +++-------------------- src/composables/useFileTree.js | 102 ++++++++++++++++++++++++++++++++- 3 files changed, 120 insertions(+), 64 deletions(-) diff --git a/public/preload/services.js b/public/preload/services.js index 9dccb2d..54f4391 100644 --- a/public/preload/services.js +++ b/public/preload/services.js @@ -201,6 +201,16 @@ window.services = { const fullPath = path.join(currentDir, item.name); if (item.isDirectory()) { if (item.name === ".git" || item.name === "node_modules") continue; // 忽略特定目录 + + // 如果文件夹名匹配,也加入结果 + if (item.name.toLowerCase().includes(keyword.toLowerCase())) { + allFiles.push({ + name: item.name, + path: fullPath, + type: "directory", + }); + } + await traverse(fullPath); } else { if (item.name.toLowerCase().includes(keyword.toLowerCase())) { diff --git a/src/components/FileTree.vue b/src/components/FileTree.vue index 18aef40..4073b31 100644 --- a/src/components/FileTree.vue +++ b/src/components/FileTree.vue @@ -96,7 +96,7 @@ import { useTabs } from '../composables/useTabs'; import { useConfig } from '../composables/useConfig'; import { message, Modal } from 'ant-design-vue'; -const { state, loadDirectory, refresh } = useFileTree(); +const { state, loadDirectory, refresh, searchFiles } = useFileTree(); const { openFile } = useTabs(); const { config, addPinnedFile, removePinnedFile, setShowHiddenItems } = useConfig(); @@ -142,76 +142,22 @@ const filterNodes = (nodes) => { })); }; -const searchFilterNodes = (nodes, keyword) => { - if (!Array.isArray(nodes) || nodes.length === 0) return []; - const lowerKeyword = keyword.toLowerCase(); - - const result = []; - for (const node of nodes) { - const nameMatch = node.name.toLowerCase().includes(lowerKeyword); - // 递归处理子节点 - let filteredChildren = []; - if (node.children) { - filteredChildren = searchFilterNodes(node.children, keyword); - } - - // 如果当前节点匹配,或者子节点有匹配项,则保留 - if (nameMatch || filteredChildren.length > 0) { - result.push({ - ...node, - children: node.children ? filteredChildren : undefined - }); - } - } - return result; -}; - -// 获取所有需要展开的路径 -const getExpandedKeys = (nodes, keyword, result = []) => { - if (!keyword) return result; - const lowerKeyword = keyword.toLowerCase(); - - for (const node of nodes) { - const nameMatch = node.name.toLowerCase().includes(lowerKeyword); - let hasChildMatch = false; - - if (node.children) { - const initialLength = result.length; - getExpandedKeys(node.children, keyword, result); - if (result.length > initialLength) { - hasChildMatch = true; - } - } - - // 如果子节点有匹配,当前节点需要展开 - if (hasChildMatch) { - result.push(node.path); - } - } - return result; -}; - const treeData = computed(() => { // 1. 基础过滤(隐藏文件) + // 搜索模式下,state.nodes 已经是过滤后的结果(虽然可能包含隐藏文件,如果 searchFileName 没过滤的话) + // 这里我们还是统一过滤一下隐藏文件 let nodes = filterNodes(state.nodes); - // 2. 搜索过滤 - if (searchKeyword.value) { - nodes = searchFilterNodes(nodes, searchKeyword.value); - } - return transformNodes(nodes); }); -// 监听搜索关键词,自动展开 +// 防抖搜索 +let searchTimer = null; watch(searchKeyword, (newVal) => { - if (newVal) { - const nodes = filterNodes(state.nodes); // 基于基础过滤后的节点计算 - const keysToExpand = []; - getExpandedKeys(nodes, newVal, keysToExpand); - // 合并并去重 - state.expandedKeys = [...new Set([...state.expandedKeys, ...keysToExpand])]; - } + if (searchTimer) clearTimeout(searchTimer); + searchTimer = setTimeout(() => { + searchFiles(newVal); + }, 300); }); const toggleShowHiddenItems = async () => { diff --git a/src/composables/useFileTree.js b/src/composables/useFileTree.js index ee76863..2609115 100644 --- a/src/composables/useFileTree.js +++ b/src/composables/useFileTree.js @@ -109,6 +109,105 @@ export function useFileTree() { } }; + // 构建树辅助函数 + const buildTreeFromSearchResults = (items, rootPath) => { + const rootNodes = []; + const sep = window.utools.isWindows() ? '\\' : '/'; + + items.forEach(item => { + // 计算相对路径 + if (!item.path.startsWith(rootPath)) return; + + let relative = item.path.substring(rootPath.length); + if (relative.startsWith(sep)) { + relative = relative.substring(1); + } + if (!relative) return; + + const parts = relative.split(sep); + + let currentLevel = rootNodes; + let currentPath = rootPath; + + parts.forEach((part, index) => { + const isLast = index === parts.length - 1; + // 如果是最后一项,使用 item 的类型;中间节点必定是 directory + const type = isLast ? item.type : 'directory'; + + currentPath = currentPath.endsWith(sep) ? currentPath + part : currentPath + sep + part; + + let existingNode = currentLevel.find(n => n.name === part); + + if (!existingNode) { + existingNode = { + id: currentPath, + path: currentPath, + name: part, + type: type, + children: type === 'directory' ? [] : undefined + }; + currentLevel.push(existingNode); + } + + if (type === 'directory') { + if (!existingNode.children) existingNode.children = []; + currentLevel = existingNode.children; + } + }); + }); + + // 排序 + const sortNodes = (nodes) => { + nodes.sort(compareFileTreeItems); + nodes.forEach(node => { + if (node.children) sortNodes(node.children); + }); + }; + sortNodes(rootNodes); + + return rootNodes; + }; + + // 递归获取所有目录路径用于展开 + const getAllDirectoryPaths = (nodes, result = []) => { + for (const node of nodes) { + if (node.type === 'directory') { + result.push(node.path); + if (node.children) { + getAllDirectoryPaths(node.children, result); + } + } + } + return result; + }; + + // 搜索文件 + const searchFiles = async (keyword) => { + if (!state.rootPath) return; + + if (!keyword) { + // 关键词为空,恢复根目录视图 + await loadDirectory(state.rootPath); + state.expandedKeys = []; + return; + } + + if (!window.services?.searchFileName) return; + + state.loading = true; + try { + const items = await window.services.searchFileName(state.rootPath, keyword); + const tree = buildTreeFromSearchResults(items, state.rootPath); + state.nodes = tree; + // 自动展开所有搜索结果中的目录 + state.expandedKeys = getAllDirectoryPaths(tree); + } catch (error) { + console.error("Search failed", error); + } finally { + state.loading = false; + } + }; + return { state, setRootPath, @@ -116,6 +215,7 @@ export function useFileTree() { expandNode, collapseNode, toggleExpand, - refresh + refresh, + searchFiles }; }