feat(文件树): 实现后端搜索并优化前端搜索性能
- 将文件搜索逻辑移至后端 services.js,支持文件夹名称匹配 - 重构前端搜索,使用防抖调用后端搜索 API - 搜索结果自动构建树形结构并展开所有匹配目录 - 移除前端递归过滤逻辑,提升大目录搜索性能
This commit is contained in:
parent
738f320e3d
commit
00a7e919f7
|
|
@ -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())) {
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue