feat(文件树): 实现后端搜索并优化前端搜索性能

- 将文件搜索逻辑移至后端 services.js,支持文件夹名称匹配
- 重构前端搜索,使用防抖调用后端搜索 API
- 搜索结果自动构建树形结构并展开所有匹配目录
- 移除前端递归过滤逻辑,提升大目录搜索性能
This commit is contained in:
cfq 2026-01-26 18:03:16 +08:00
parent 738f320e3d
commit 00a7e919f7
3 changed files with 120 additions and 64 deletions

View File

@ -201,6 +201,16 @@ window.services = {
const fullPath = path.join(currentDir, item.name); const fullPath = path.join(currentDir, item.name);
if (item.isDirectory()) { if (item.isDirectory()) {
if (item.name === ".git" || item.name === "node_modules") continue; // 忽略特定目录 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); await traverse(fullPath);
} else { } else {
if (item.name.toLowerCase().includes(keyword.toLowerCase())) { if (item.name.toLowerCase().includes(keyword.toLowerCase())) {

View File

@ -96,7 +96,7 @@ import { useTabs } from '../composables/useTabs';
import { useConfig } from '../composables/useConfig'; import { useConfig } from '../composables/useConfig';
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
const { state, loadDirectory, refresh } = useFileTree(); const { state, loadDirectory, refresh, searchFiles } = useFileTree();
const { openFile } = useTabs(); const { openFile } = useTabs();
const { config, addPinnedFile, removePinnedFile, setShowHiddenItems } = useConfig(); 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(() => { const treeData = computed(() => {
// 1. // 1.
// state.nodes searchFileName
//
let nodes = filterNodes(state.nodes); let nodes = filterNodes(state.nodes);
// 2.
if (searchKeyword.value) {
nodes = searchFilterNodes(nodes, searchKeyword.value);
}
return transformNodes(nodes); return transformNodes(nodes);
}); });
// //
let searchTimer = null;
watch(searchKeyword, (newVal) => { watch(searchKeyword, (newVal) => {
if (newVal) { if (searchTimer) clearTimeout(searchTimer);
const nodes = filterNodes(state.nodes); // searchTimer = setTimeout(() => {
const keysToExpand = []; searchFiles(newVal);
getExpandedKeys(nodes, newVal, keysToExpand); }, 300);
//
state.expandedKeys = [...new Set([...state.expandedKeys, ...keysToExpand])];
}
}); });
const toggleShowHiddenItems = async () => { const toggleShowHiddenItems = async () => {

View File

@ -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 { return {
state, state,
setRootPath, setRootPath,
@ -116,6 +215,7 @@ export function useFileTree() {
expandNode, expandNode,
collapseNode, collapseNode,
toggleExpand, toggleExpand,
refresh refresh,
searchFiles
}; };
} }