使用HTML+JavaScript實(shí)現(xiàn)文件樹(shù)完整代碼
一、文件樹(shù)
文件樹(shù)是現(xiàn)代文件管理系統(tǒng)中的核心組件,通過(guò)樹(shù)形結(jié)構(gòu)展示文件和文件夾的層級(jí)關(guān)系,讓用戶能夠直觀地瀏覽和管理文件。這種界面設(shè)計(jì)提供了清晰的層次結(jié)構(gòu),支持文件的展開(kāi)收起、選中、重命名、刪除等操作,極大提升了用戶體驗(yàn)和操作效率。本文將介紹如何使用 HTML、CSS 和 JavaScript 實(shí)現(xiàn)文件樹(shù)。
二、效果演示
文件樹(shù)具有豐富的交互功能。用戶可以通過(guò)單擊選中某個(gè)文件或文件夾,被選中的節(jié)點(diǎn)會(huì)高亮顯示。雙擊文件夾可以展開(kāi)或收起其子內(nèi)容,雙擊文件會(huì)觸發(fā)打開(kāi)操作。每個(gè)節(jié)點(diǎn)右側(cè)都有操作按鈕,點(diǎn)擊重命名按鈕可以修改文件名,刪除按鈕可以移除節(jié)點(diǎn)。鼠標(biāo)懸停在節(jié)點(diǎn)上時(shí),會(huì)顯示操作按鈕并改變背景顏色。界面還提供了統(tǒng)計(jì)信息,顯示當(dāng)前文件樹(shù)中項(xiàng)目的總數(shù)。

三、系統(tǒng)分析
1、頁(yè)面結(jié)構(gòu)
頁(yè)面主要包括以下幾個(gè)區(qū)域:
1.1 文件樹(shù)區(qū)域
展示文件和文件夾的樹(shù)形結(jié)構(gòu)。
<div class="file-tree" id="fileTree"> <div class="loading">正在加載文件樹(shù)...</div> </div>
1.2 統(tǒng)計(jì)信息區(qū)域
<div class="stats">
<span id="itemCount">共 0 個(gè)項(xiàng)目</span>
</div>
2、核心功能實(shí)現(xiàn)
2.1 數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)
文件樹(shù)的數(shù)據(jù)結(jié)構(gòu)使用嵌套對(duì)象表示,每個(gè)節(jié)點(diǎn)包含名稱、類型、大小和子節(jié)點(diǎn)等信息。
const mockFileData = [
{ name: "個(gè)人文檔", type: "folder", size: "856MB", children: [
{ name: "工作報(bào)告.docx", type: "file", size: "2.3MB" },
{ name: "會(huì)議記錄.pdf", type: "file", size: "1.8MB" },
]
},
];
2.2 節(jié)點(diǎn)渲染機(jī)制
renderNode 方法負(fù)責(zé)將數(shù)據(jù)渲染為 DOM 元素,遞歸處理子節(jié)點(diǎn)。根據(jù)節(jié)點(diǎn)類型顯示不同的圖標(biāo),對(duì)文件夾處理展開(kāi)收起狀態(tài)。
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;
}
2.3 交互事件處理
通過(guò) handleNodeClick 和 handleNodeDblClick 方法處理用戶的點(diǎn)擊和雙擊事件,實(shí)現(xiàn)節(jié)點(diǎn)選中和展開(kāi)收起功能。
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(`正在打開(kāi)文件: ${node.name}`);
}
}
2.4 節(jié)點(diǎn)操作功能
renameNode 和 deleteNode 方法分別實(shí)現(xiàn)重命名和刪除功能。重命名時(shí)將文本替換為輸入框,支持 Enter 確認(rèn)和 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(`確定要?jiǎng)h除"${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();
}
四、擴(kuò)展建議
- 添加拖拽功能實(shí)現(xiàn)文件移動(dòng)
- 增加搜索和過(guò)濾功能
- 添加多選操作支持
- 支持鍵盤(pán)快捷鍵操作
五、完整代碼
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>文件樹(shù)</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>文件樹(shù)</h1></header>
<div class="file-tree" id="fileTree">
<div class="loading">正在加載文件樹(shù)...</div>
</div>
<div class="stats">
<span id="itemCount">共 0 個(gè)項(xiàng)目</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: "個(gè)人文檔", type: "folder", size: "856MB", children: [
{ name: "工作報(bào)告.docx", type: "file", size: "2.3MB" },
{ name: "會(huì)議記錄.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-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(`正在打開(kāi)文件: ${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(`確定要?jiǎng)h除"${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} 個(gè)項(xiàng)目 (${folderCount} 個(gè)文件夾, ${fileCount} 個(gè)文件)`;
}
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>
總結(jié)
到此這篇關(guān)于使用HTML+JavaScript實(shí)現(xiàn)文件樹(shù)的文章就介紹到這了,更多相關(guān)HTML+JS實(shí)現(xiàn)文件樹(shù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
現(xiàn)代 JavaScript 開(kāi)發(fā)編程風(fēng)格Idiomatic.js指南中文版
下面的章節(jié)描述的是一個(gè) 合理 的現(xiàn)代 JavaScript 開(kāi)發(fā)風(fēng)格指南,并非硬性規(guī)定。其想送出的核心理念是高度統(tǒng)一的代碼風(fēng)格(the law of code style consistency)。2014-05-05
js 使用ajax設(shè)置和獲取自定義header信息的方法小結(jié)
這篇文章主要介紹了js 使用ajax設(shè)置和獲取自定義header信息的方法,結(jié)合實(shí)例形式總結(jié)分析了js 使用ajax自定義設(shè)置和獲取header響應(yīng)信息相關(guān)操作技巧與使用注意事項(xiàng),需要的朋友可以參考下2020-03-03
JS 調(diào)試中常見(jiàn)的報(bào)錯(cuò)問(wèn)題解決方法
下面小編就為大家?guī)?lái)一篇JS 調(diào)試中常見(jiàn)的報(bào)錯(cuò)問(wèn)題解決方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-05-05
JS回調(diào)函數(shù)的應(yīng)用簡(jiǎn)單實(shí)例
這篇文章主要分享了JS回調(diào)函數(shù)應(yīng)用的簡(jiǎn)單實(shí)例,比較實(shí)用,需要的朋友可以參考下2014-09-09
JS中LocalStorage與SessionStorage五種循序漸進(jìn)的使用方法
這篇文章主要介紹了JS中LocalStorage與SessionStorage五種循序漸進(jìn)的使用方法,需要的朋友可以參考下2017-07-07
小程序中設(shè)置緩存過(guò)期的實(shí)現(xiàn)方法
這篇文章主要介紹了小程序中設(shè)置緩存過(guò)期的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01

