跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
HTML / CSS大前端

使用 HTML 和 JavaScript 实现文件树

综述由AI生成如何使用 HTML、CSS 和 JavaScript 实现一个功能完整的文件树组件。内容涵盖页面结构设计、核心数据结构定义、节点渲染机制以及交互事件处理,包括选中、展开收起、重命名和删除等功能。文章提供了完整的代码示例,并给出了扩展建议如拖拽移动、搜索过滤等,适合前端开发者参考学习。

邪神洛基发布于 2026/3/26更新于 2026/5/819 浏览
使用 HTML 和 JavaScript 实现文件树

一、文件树

文件树是现代文件管理系统中的核心组件,通过树形结构展示文件和文件夹的层级关系,让用户能够直观地浏览和管理文件。这种界面设计提供了清晰的层次结构,支持文件的展开收起、选中、重命名、删除等操作,极大提升了用户体验和操作效率。本文将介绍如何使用 HTML、CSS 和 JavaScript 实现文件树。

二、效果演示

文件树具有丰富的交互功能。用户可以通过单击选中某个文件或文件夹,被选中的节点会高亮显示。双击文件夹可以展开或收起其子内容,双击文件会触发打开操作。每个节点右侧都有操作按钮,点击重命名按钮可以修改文件名,删除按钮可以移除节点。鼠标悬停在节点上时,会显示操作按钮并改变背景颜色。界面还提供了统计信息,显示当前文件树中项目的总数。

三、系统分析

1、页面结构

页面主要包括以下几个区域:

1.1 文件树区域

展示文件和文件夹的树形结构。

<div class="file-tree" id="fileTree">
  <div class="loading">正在加载文件树...</div>
</div>
1.2 统计信息区域
<div class="stats">
  <span id="itemCount">共 0 个项目</span>
</div>

2、核心功能实现

2.1 数据结构设计

文件树的数据结构使用嵌套对象表示,每个节点包含名称、类型、大小和子节点等信息。

const mockFileData = [
  {
    name: "个人文档",
    type: "folder",
    size: "856MB",
    children: [
      { name: "工作报告.docx", type: "file", size: "2.3MB" },
      { name: "会议记录.pdf", type: "file", size: "1.8MB" }
    ]
  }
];
2.2 节点渲染机制

renderNode 方法负责将数据渲染为 DOM 元素,递归处理子节点。根据节点类型显示不同的图标,对文件夹处理展开收起状态。

renderNode(node, level = 0) {
  if (!node) return '';
  const isExpanded = this.expandedNodes.has(node.id);
  const isSelected = this.selectedNodes.has(node.id);
  const hasChildren = node.children && node.children.length > 0;
  let html = '';
  // 生成 HTML 代码
  if (hasChildren) {
    html += `<div class="${isExpanded ? 'expanded' : ''}">`;
    node.children.forEach(child => {
      html += this.renderNode(child, level + 1);
    });
    html += '</div>';
  } else if (node.type === 'folder') {
    html += `<div class="${isExpanded ? 'expanded' : ''}"><div>空文件夹</div></div>`;
  }
  html += '</div>';
  return html;
}
2.3 交互事件处理

通过 handleNodeClick 和 handleNodeDblClick 方法处理用户的点击和双击事件,实现节点选中和展开收起功能。

handleNodeClick(event, nodeId) {
  event.stopPropagation();
  this.setSelection(nodeId);
}

handleNodeDblClick(event, nodeId) {
  event.stopPropagation();
  const node = this.nodeIdMap.get(nodeId);
  if (node && node.type === 'folder') {
    this.toggleNode(nodeId);
  } else {
    alert(`正在打开文件:${node.name}`);
  }
}
2.4 节点操作功能

renameNode 和 deleteNode 方法分别实现重命名和删除功能。重命名时将文本替换为输入框,支持 Enter 确认和 Escape 取消操作。

renameNode(nodeId) {
  const node = this.nodeIdMap.get(nodeId);
  if (!node) return;
  const treeItem = document.querySelector(`[data-id="${nodeId}"]`);
  if (!treeItem) return;
  const originalName = node.name;
  const nameDiv = treeItem.querySelector('.node-name');
  const input = document.createElement('input');
  input.type = 'text';
  input.className = 'rename-input';
  input.value = originalName;
  nameDiv.innerHTML = '';
  nameDiv.appendChild(input);
  input.focus();
  input.select();
  input.addEventListener('mousedown', (e) => e.stopPropagation());
  input.addEventListener('click', (e) => e.stopPropagation());
  input.addEventListener('dblclick', (e) => e.stopPropagation());
  const finishRename = (newName) => {
    if (newName && newName !== originalName) {
      const parent = this.findParentNode(nodeId);
      if (parent && parent.children.some(child => child.name === newName && child.id !== nodeId)) {
        alert('同名文件或文件夹已存在!');
        input.value = originalName;
        nameDiv.textContent = originalName;
        return;
      }
      node.name = newName;
      this.nodeIdMap.delete(nodeId);
      this.generateNodeIds([node], parent ? parent.id : null);
    } else {
      nameDiv.textContent = originalName;
    }
    this.render();
  };
  input.addEventListener('blur', () => finishRename(input.value));
  input.addEventListener('keypress', (e) => {
    if (e.key === 'Enter') finishRename(input.value);
    else if (e.key === 'Escape') {
      nameDiv.textContent = originalName;
      this.render();
    }
  });
}

deleteNode(nodeId) {
  const node = this.nodeIdMap.get(nodeId);
  if (!node) return;
  if (!confirm(`确定要删除"${node.name}"吗?`)) return;
  const parent = this.findParentNode(nodeId);
  if (parent) {
    parent.children = parent.children.filter(child => child.id !== nodeId);
  } else {
    this.data = this.data.filter(item => item.id !== nodeId);
  }
  this.removeNodeData(nodeId);
  this.render();
}

四、扩展建议

  • 添加拖拽功能实现文件移动
  • 增加搜索和过滤功能
  • 添加多选操作支持
  • 支持键盘快捷键操作

五、完整代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>文件树</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; user-select: none; }
    html, body { height: 100%; }
    body { background: #f6f7f9; min-height: 100vh; color: #333; display: flex; flex-direction: column; }
    .container { max-width: 100%; flex: 1; display: flex; flex-direction: column; }
    header { height: 52px; background: #fff; border-bottom: 1px solid #e5e5e5; display: flex; align-items: center; padding: 0 24px; position: sticky; top: 0; z-index: 9; flex-shrink: 0; }
    header h1 { font-size: 18px; font-weight: 500; margin-right: auto; display: flex; align-items: center; gap: 8px; }
    .file-tree { padding: 10px 20px; flex: 1; overflow-y: auto; }
    .tree-item { margin: 2px 0; user-select: none; }
    .tree-node { display: flex; align-items: center; padding: 8px 12px; border-radius: 6px; cursor: pointer; transition: all 0.3s ease; border: 1px solid #e5e5e5; background: #fff; margin-bottom: 4px; }
    .tree-node:hover { background: #f0f7ff; border-color: #06a7ff; }
    .tree-node.selected { background: rgba(6, 167, 255, 0.1); }
    .node-icon { width: 20px; height: 20px; margin-right: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; transition: transform 0.3s ease; }
    .node-name { flex: 1; font-size: 14px; color: #333; }
    .node-size { font-size: 12px; color: #666; margin-left: 8px; }
    .node-actions { display: flex; gap: 5px; opacity: 0; transition: opacity 0.3s ease; }
    .tree-node:hover .node-actions { opacity: 1; }
    .action-btn { width: 24px; height: 24px; border: none; background: transparent; cursor: pointer; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 12px; color: #666; transition: all 0.3s ease; }
    .action-btn:hover { background: #dee2e6; color: #333; }
    .tree-children { margin-left: 24px; border-left: 2px solid #e9ecef; padding-left: 12px; max-height: 0; overflow: hidden; transition: max-height 0.3s ease; }
    .tree-children.expanded { max-height: 2000px; }
    .folder-empty { color: #999; font-style: italic; padding: 8px 12px; margin-left: 24px; }
    .stats { display: flex; justify-content: space-between; align-items: center; padding: 12px 24px; background: #fff; border-top: 1px solid #e5e5e5; font-size: 14px; color: #666; }
    .loading { text-align: center; padding: 20px; color: #666; }
    .loading::after { content: ''; display: inline-block; width: 20px; height: 20px; border: 2px solid #f3f3f3; border-top: 2px solid #06a7ff; border-radius: 50%; animation: spin 1s linear infinite; margin-left: 10px; }
    @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
    .rename-input { width: 100%; padding: 5px; border-radius: 3px; border: 1px solid #06a7ff; font-size: 14px; user-select: auto; }
    .rename-input:focus { outline: none; }
  </style>
</head>
<body>
  <div class="container">
    <header><h1>文件树</h1></header>
    <div class="file-tree" id="fileTree"><div class="loading">正在加载文件树...</div></div>
    <div class="stats"><span id="itemCount">共 0 个项目</span></div>
  </div>
  <script>
    const iconMap = {
      folder: '<svg viewBox="0 0 24 24"><path fill="#FFA000" d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.11.89 2 2 2h16c1.11 0 2-.89-2-2V8c0-1.11-.89-2-2-2h-8l-2-2z"/></svg>',
      file: '<svg viewBox="0 0 24 24"><path fill="#9E9E9E" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6z"/></svg>'
    };
    const mockFileData = [
      { name: "个人文档", type: "folder", size: "856MB", children: [{ name: "工作报告.docx", type: "file", size: "2.3MB" }, { name: "会议记录.pdf", type: "file", size: "1.8MB" }] }
    ];
    class FileTreeManager {
      constructor() {
        this.data = mockFileData;
        this.expandedNodes = new Set();
        this.selectedNodes = new Set();
        this.nodeIdMap = new Map();
        this.init();
      }
      init() {
        this.generateNodeIds(this.data);
        this.render();
        this.updateStats();
      }
      generateNodeIds(nodes, parentId = null) {
        nodes.forEach(node => {
          const id = parentId ? `${parentId}-${node.name}` : node.name;
          this.nodeIdMap.set(id, node);
          node.id = id;
          if (node.children) this.generateNodeIds(node.children, id);
        });
      }
      render() {
        const container = document.getElementById('fileTree');
        container.innerHTML = '';
        let html = '';
        this.data.forEach(rootNode => {
          html += this.renderNode(rootNode);
        });
        container.innerHTML = html;
        this.updateStats();
      }
      renderNode(node, level = 0) {
        if (!node) return '';
        const isExpanded = this.expandedNodes.has(node.id);
        const isSelected = this.selectedNodes.has(node.id);
        const hasChildren = node.children && node.children.length > 0;
        let html = `<div class="tree-node ${isSelected ? 'selected' : ''}" onclick="fileTreeManager.handleNodeClick(event, '${node.id}')" ondblclick="fileTreeManager.handleNodeDblClick(event, '${node.id}')">
          <div class="node-icon ${isExpanded ? 'expanded' : ''}">${this.getIcon(node.type, node.name)}</div>
          <div class="node-name">${node.name}</div>
          <div class="node-size">${node.size || ''}</div>
          <div class="node-actions">
            <button title="重命名" onclick="fileTreeManager.renameNode('${node.id}'); event.stopPropagation();">
              <svg viewBox="0 0 24 24"><path fill="#666" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
            </button>
            <button title="删除" onclick="fileTreeManager.deleteNode('${node.id}'); event.stopPropagation();">
              <svg viewBox="0 0 24 24"><path fill="#666" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
            </button>
          </div>
        </div>`;
        if (hasChildren) {
          html += `<div class="tree-children ${isExpanded ? 'expanded' : ''}">`;
          node.children.forEach(child => {
            html += this.renderNode(child, level + 1);
          });
          html += '</div>';
        } else if (node.type === 'folder') {
          html += `<div class="tree-children ${isExpanded ? 'expanded' : ''}"><div class="folder-empty">空文件夹</div></div>`;
        }
        return html;
      }
      escapeHtml(text) {
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
      }
      getIcon(type, filename) {
        if (type === 'folder') return iconMap.folder;
        return iconMap.file;
      }
      handleNodeClick(event, nodeId) {
        event.stopPropagation();
        this.setSelection(nodeId);
      }
      handleNodeDblClick(event, nodeId) {
        event.stopPropagation();
        const node = this.nodeIdMap.get(nodeId);
        if (node && node.type === 'folder') {
          this.toggleNode(nodeId);
        } else {
          alert(`正在打开文件:${node.name}`);
        }
      }
      toggleNode(nodeId) {
        if (this.expandedNodes.has(nodeId)) {
          this.expandedNodes.delete(nodeId);
        } else {
          this.expandedNodes.add(nodeId);
        }
        this.render();
      }
      setSelection(nodeId) {
        this.selectedNodes.clear();
        this.selectedNodes.add(nodeId);
        this.render();
      }
      renameNode(nodeId) {
        const node = this.nodeIdMap.get(nodeId);
        if (!node) return;
        const treeItem = document.querySelector(`[data-id="${nodeId}"]`);
        if (!treeItem) return;
        const originalName = node.name;
        const nameDiv = treeItem.querySelector('.node-name');
        const input = document.createElement('input');
        input.type = 'text';
        input.className = 'rename-input';
        input.value = originalName;
        nameDiv.innerHTML = '';
        nameDiv.appendChild(input);
        input.focus();
        input.select();
        input.addEventListener('mousedown', (e) => e.stopPropagation());
        input.addEventListener('click', (e) => e.stopPropagation());
        input.addEventListener('dblclick', (e) => e.stopPropagation());
        const finishRename = (newName) => {
          if (newName && newName !== originalName) {
            const parent = this.findParentNode(nodeId);
            if (parent && parent.children.some(child => child.name === newName && child.id !== nodeId)) {
              alert('同名文件或文件夹已存在!');
              input.value = originalName;
              nameDiv.textContent = originalName;
              return;
            }
            node.name = newName;
            this.nodeIdMap.delete(nodeId);
            this.generateNodeIds([node], parent ? parent.id : null);
          } else {
            nameDiv.textContent = originalName;
          }
          this.render();
        };
        input.addEventListener('blur', () => finishRename(input.value));
        input.addEventListener('keypress', (e) => {
          if (e.key === 'Enter') finishRename(input.value);
          else if (e.key === 'Escape') {
            nameDiv.textContent = originalName;
            this.render();
          }
        });
      }
      deleteNode(nodeId) {
        const node = this.nodeIdMap.get(nodeId);
        if (!node) return;
        if (!confirm(`确定要删除"${node.name}"吗?`)) return;
        const parent = this.findParentNode(nodeId);
        if (parent) {
          parent.children = parent.children.filter(child => child.id !== nodeId);
        } else {
          this.data = this.data.filter(item => item.id !== nodeId);
        }
        this.removeNodeData(nodeId);
        this.render();
      }
      removeNodeData(nodeId) {
        const node = this.nodeIdMap.get(nodeId);
        if (node) {
          this.nodeIdMap.delete(nodeId);
          if (node.children) {
            node.children.forEach(child => this.removeNodeData(child.id));
          }
        }
        this.expandedNodes.delete(nodeId);
        this.selectedNodes.delete(nodeId);
      }
      findParentNode(nodeId) {
        const findInTree = (nodes, id) => {
          for (const node of nodes) {
            if (node.children) {
              if (node.children.some(child => child.id === id)) return node;
              const found = findInTree(node.children, id);
              if (found) return found;
            }
          }
          return null;
        };
        return findInTree(this.data, nodeId);
      }
      getAllNodeIds(nodes, ids = []) {
        nodes.forEach(node => {
          ids.push(node.id);
          if (node.children) this.getAllNodeIds(node.children, ids);
        });
        return ids;
      }
      updateStats() {
        const allNodes = this.getAllNodeIds(this.data);
        const folderCount = this.getAllFolderIds(this.data).length;
        const fileCount = allNodes.length - folderCount;
        document.getElementById('itemCount').textContent = `共 ${allNodes.length} 个项目 (${folderCount} 个文件夹,${fileCount} 个文件)`;
      }
      getAllFolderIds(nodes, ids = []) {
        nodes.forEach(node => {
          if (node.type === 'folder') {
            ids.push(node.id);
            if (node.children) this.getAllFolderIds(node.children, ids);
          }
        });
        return ids;
      }
    }
    const fileTreeManager = new FileTreeManager();
  </script>
</body>
</html>

目录

  1. 一、文件树
  2. 二、效果演示
  3. 三、系统分析
  4. 1、页面结构
  5. 1.1 文件树区域
  6. 1.2 统计信息区域
  7. 2、核心功能实现
  8. 2.1 数据结构设计
  9. 2.2 节点渲染机制
  10. 2.3 交互事件处理
  11. 2.4 节点操作功能
  12. 四、扩展建议
  13. 五、完整代码
  • 💰 8折买阿里云服务器限时8折了解详情
  • GPT-5.5 超高智商模型1元抵1刀ChatGPT中转购买
  • 代充Chatgpt Plus/pro 帐号了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Gemini 全能 QQ 机器人部署指南
  • HarmonyOS 6.0 Camera Kit 微距状态监听详解
  • Python SQLAlchemy ORM 数据库操作指南
  • C 语言游戏开发:Pygame、SDL、OpenGL 深度解析
  • Git 分支管理实战指南:从基础概念到团队协作规范
  • AI 产品经理职业发展路径与核心技术能力解析
  • 被工具定义的编程时代:VS Code、Copilot 与 JetBrains 工具链解析
  • OpenClaw Agent Skills 核心技能推荐及安装指南
  • 动态规划专题:01 背包模型详解与空间优化
  • 学习大语言模型原理必看的 10 篇论文
  • 深入解析顶级 AI Agent 设计模式与实现策略
  • 【论文笔记】A Survey on Data Synthesis and Augmentation for Large Language Models
  • OpenClaw 本地部署教程:环境配置、插件开发与常见问题排查
  • Python 变量与数据类型核心指南
  • Ubuntu 前端开发环境搭建与 Vue 实战
  • 基于开源鸿蒙(OpenHarmony)的【智能家居综合应用】系统
  • Python 基于 FTDI FT2232H 实现 SPI 通信控制
  • 堆(Heap)的实现:基于完全二叉树的顺序存储与调整算法
  • 中小团队低成本搭建项目管理系统:Ubuntu 下 DooTask 私有化部署实战
  • 人工智能基础概念全解析:从历史浪潮到未来展望

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online