Vue3+Ts 模仿一个 IDE 中的文件树
使用 Vue3
做一个能够通过选定的本地文件夹生成一个类似 IDE
左侧的文件树,同时点击文件可显示基本信息的功能。
本项目已发布到开源仓库 filetree,代码对应文件为 App.vue
与 TreeNode.vue
。也可以通过 这个链接快速访问本项目。
以下是对核心代码的讲解。
1. 文件处理
要实现文件树最大的核心在于 解析文件夹结构 与 构建文件树 。
1.1 定义文件树节点
文件树与数据结构中的树类似,将每个文件抽象为一个节点,节点包含 文件名、路径、子文件、折叠状态。
interface FileItem {
name: string;
path: string;
children?: FileItem[];
collapsed?: boolean;
}
1.2 构建文件树
将一个 FileList
对象转换成一个树形结构的 FileItem
数组。 每个 FileItem
代表一个文件或文件夹,并包含其子文件或子文件夹。
const buildFileTree = (files: FileList): FileItem[] => {
// 初始化根节点 其名称为 root,路径为空,子节点为空数组。
const root: FileItem = {name: 'Root', path: '', children: []};
for (let i = 0; i < files.length; i++) { //遍历选中的文件夹
const file = files[i]; // i = 0 为第一个子文件
// 使用 webkitRelativePath 获取文件的相对路径,并将其按 / 分割成数组 pathParts。
// 当i=0 ,pathParts为根节点第一个子文件夹的相对路径数组
const pathParts = file.webkitRelativePath.split('/');
// currentLevel 初始化为节点的子节点数组。
let currentLevel = root.children!;
for (let j = 0; j < pathParts.length; j++) {
const part = pathParts[j];
const existingPath = currentLevel.find((item) => item.name === part);
if (existingPath) {
// 如果当前路径部分已经存在,则将 currentLevel 更新为该节点的子节点数组。
currentLevel = existingPath.children || [];
} else {
// 如果当前路径部分不是最后一个部分(即不是文件名),则为新节点初始化一个空子节点数组。
// 将新节点添加到当前层级中,并将 currentLevel 更新为新节点的子节点数组。
const newItem: FileItem = {name: part, path: file.webkitRelativePath};
if (j < pathParts.length - 1) {
newItem.children = [];
}
currentLevel.push(newItem);
currentLevel = newItem.children || [];
}
}
}
return root.children || [];
};
1.3 构建文件路径和 File 对象的映射
构建 fileMap
,用于后续文件信息的展示。
const buildFileMap = (files: FileList) => {
fileMap.value = {};
for (let i = 0; i < files.length; i++) {
const file = files[i];
// 将文件路径作为键,文件对象作为值
fileMap.value[file.webkitRelativePath] = file;
}
};
1.4 处理文件夹选择
handleFolderSelect
方法绑定在 input[type=file]
中。
// fileTree 对象
const fileTree = ref<FileItem[]>([]);
// fileMap
const fileMap = ref<Record<string, File>>({}); // 存储文件路径和 File 对象的映射
// 处理文件夹选择
const handleFolderSelect = (event: Event) => {
const input = event.target as HTMLInputElement;
if (input.files !== null && input.files.length > 0) {
fileInfo.value = null
// 提取选中的文件夹的名称(根文件夹名)
selFolderName.value = input.files[0].webkitRelativePath.split('/')[0];
fileTree.value = buildFileTree(input.files);
// 构建文件树
buildFileMap(input.files);
}
};
2. 文件树组件
文件树组件 TreeNode
对传入的 fileTree
和 fileMap
进行递归渲染。
文件树 props :
const props = defineProps<{
files: FileItem[];
fileMap: Record<string, File>; // 用于存储文件路径和 File 对象的映射
}>();
TreeNode.vue :
<template>
<ul>
<li v-for="item in files" :key="item.path">
<div class="file-item"
@click="handleItemClick(item)"
>
<span class="file-item-name">{{ item.name }}</span>
<span v-if="item.children" class="file-item-label">
<span class="file-item-length">{{ item.children.length }}</span>
</span>
</div>
<TreeNode
v-if="item.children && !item.collapsed"
:files="item.children"
:fileMap="fileMap"
/>
</li>
</ul>
</template>
因为还需要根据 文件后缀匹配文件图标 ,同时文件夹需要能够 折叠/展开,因此还需进行 collapsed
判断。 图标使用 iconify
,通过 <Icon icon="iconname"/>
进行图标渲染。
// icon 对象
interface IconConfig {
type: string; // 文件后缀
icon: string; // 图标
}
// 获取icon
const getFileIcon = (filename: string): string | undefined => {
// 获取文件后缀
const fileExtension = filename.split('.').pop()?.toLowerCase();
// 在 icon.json 中查找对应的图标
const iconConfig = icons.find((icon: IconConfig) => icon.type === fileExtension);
// 返回图标,如果未找到则返回 默认图标
return iconConfig ? iconConfig.icon : 'flat-color-icons:document';
}
icons.find()
的 icons
来源于自定义的 icon.json
:
[
{
"type": "pdf",
"icon": "vscode-icons:file-type-pdf2"
},
{
"type": "md",
"icon": "skill-icons:markdown-dark"
},
{
"type": "docx",
"icon": "vscode-icons:file-type-word"
}
]