LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

使用 HTML + JavaScript 实现文件树(附完整代码)

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

效果演示

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

页面结构

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

文件树区域

展示文件和文件夹的树形结构。
<div class="file-tree" id="fileTree">  <div class="loading">正在加载文件树...</div></div>

统计信息区域

<div class="stats">    <span id="itemCount">共 0 个项目</span></div>

核心功能实现

数据结构设计

文件树的数据结构使用嵌套对象表示,每个节点包含名称、类型、大小和子节点等信息。
const mockFileData = [  { name: "个人文档"type"folder", size: "856MB", children: [      { name: "工作报告.docx"type"file", size: "2.3MB" },      { name: "会议记录.pdf"type"file", size: "1.8MB" },    ]  },];

节点渲染机制

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="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>`  }
  html += '</div>';  return html;}

交互事件处理

通过 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}`);  }}

节点操作功能

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();}

扩展建议

  • 添加拖拽功能实现文件移动

  • 增加搜索和过滤功能

  • 添加多选操作支持

  • 支持键盘快捷键操作

完整代码

git地址:https://gitee.com/ironpro/hjdemo/blob/master/file-tree/index.html
<!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>    * { margin0padding0box-sizing: border-box; user-select: none; }    htmlbody { height100%; }    body { background#f6f7f9min-height100vhcolor#333display: flex; flex-direction: column; }    .container { max-width100%flex1display: flex; flex-direction: column; }    header { height52pxbackground#fffborder-bottom1px solid #e5e5e5display: flex; align-items: center; padding0 24pxposition: sticky; top0z-index9flex-shrink0; }    header h1 { font-size18pxfont-weight500margin-right: auto; display: flex; align-items: center; gap8px; }    .file-tree { padding10px 20pxflex1overflow-y: auto; }    .tree-item { margin2px 0user-select: none; }    .tree-node { display: flex; align-items: center; padding8px 14pxborder-radius6pxcursor: pointer; transition: all 0.3s ease; border1px solid #e5e5e5background#fffmargin-bottom4px; }    .tree-node:hover { background#f0f7ffborder-color#06a7ff; }    .tree-node.selected { backgroundrgba(61672550.1); }    .node-icon { width20pxheight20pxmargin-right8pxdisplay: flex; align-items: center; justify-content: center; font-size16pxtransition: transform 0.3s ease; }    .node-name { flex1font-size14pxcolor#333; }    .node-size { font-size14pxcolor#666margin-left8px; }    .node-actions { display: flex; gap5pxopacity0transition: opacity 0.3s ease; }    .tree-node:hover .node-actions { opacity1; }    .action-btn { width24pxheight24pxborder: none; background: transparent; cursor: pointer; border-radius4pxdisplay: flex; align-items: center; justify-content: center; font-size14pxcolor#666transition: all 0.3s ease; }    .action-btn:hover { background#dee2e6color#333; }    .tree-children { margin-left24pxborder-left2px solid #e9ecefpadding-left14pxmax-height0overflow: hidden; transition: max-height 0.3s ease; }    .tree-children.expanded { max-height2000px; }    .folder-empty { color#999font-style: italic; padding8px 14pxmargin-left24px; }    .stats { display: flex; justify-content: space-between; align-items: center; padding14px 24pxbackground#fffborder-top1px solid #e5e5e5font-size14pxcolor#666; }    .loading { text-align: center; padding20pxcolor#666; }    .loading::after { content''display: inline-block; width20pxheight20pxborder2px solid #f3f3f3border-top2px solid #06a7ffborder-radius50%animation: spin 1s linear infinite; margin-left10px; }    @keyframes spin { 0% { transformrotate(0deg); } 100% { transformrotate(360deg); } }    .rename-input { width100%padding5pxborder-radius3px;border1px solid #06a7fffont-size14pxuser-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" width="20" height="20"><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" width="20" height="20"><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.childrenthis.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-item" data-name="${this.escapeHtml(node.name)}" data-type="${node.type}" data-id="${node.id}">          <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 class="action-btn" title="重命名" onclick="fileTreeManager.renameNode('${node.id}'); event.stopPropagation();">                <svg viewBox="0 0 24 24" width="16" height="16"><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 class="action-btn" title="删除" onclick="fileTreeManager.deleteNode('${node.id}'); event.stopPropagation();">                <svg viewBox="0 0 24 24" width="16" height="16"><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>`      }
      html += '</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.childrenthis.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.childrenthis.getAllFolderIds(node.children, ids);        }      });      return ids;    }  }  const fileTreeManager = new FileTreeManager();</script></body></html>


阅读原文:https://mp.weixin.qq.com/s/abgfPvLtOAFeJRyMAY75cg


该文章在 2026/2/9 11:21:38 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved