Skip to content

Vue3+Ts 模仿一个 IDE 中的文件树

约 992 字大约 3 分钟

VueTypeScript

2024-12-31

使用 Vue3 做一个能够通过选定的本地文件夹生成一个类似 IDE 左侧的文件树,同时点击文件可显示基本信息的功能。

24123101_01.png

本项目已发布到开源仓库 filetree,代码对应文件为 App.vueTreeNode.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 对传入的 fileTreefileMap 进行递归渲染。
文件树 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"
  }
]