UNPKG

hudada-cli

Version:

专为程序员准备的本地文档搜索,快捷开发工具

832 lines (723 loc) 27.2 kB
// 获取服务器端渲染的文件结构 const fileStructure = {{ fileStructure }}; let allFiles = []; // 存储所有文件和文件夹的全局变量 function searchFiles() { const searchInput = document.getElementById('searchInput').value.toLowerCase(); const searchType = document.getElementById('searchType').value; console.log(searchType, 'searchType'); // 如果搜索框为空,显示所有文件 if (!searchInput) { allFiles = fileStructure; renderFileTable(allFiles); return; } // 递归搜索函数 function recursiveSearch(items) { const result = []; items.forEach(item => { let matched = false; // 根据搜索类型匹配 switch (searchType) { case 'name': matched = item.name.toLowerCase().includes(searchInput); break; case 'type': matched = item.type.toLowerCase().includes(searchInput); break; case 'size': // 支持类似 ">10KB" 或 "<1MB" 的搜索 const sizeMatch = searchInput.match(/^([<>]=?)?(\d+)([KMGT]B)?$/i); if (sizeMatch && item.type !== 'directory') { const [, operator = '=', value, unit = 'B'] = sizeMatch; const fileSize = convertToBytes(item.size); const searchSize = convertToBytes(`${value}${unit}`); switch (operator) { case '>': matched = fileSize > searchSize; break; case '>=': matched = fileSize >= searchSize; break; case '<': matched = fileSize < searchSize; break; case '<=': matched = fileSize <= searchSize; break; default: matched = fileSize === searchSize; } } break; default: matched = false; } // 如果当前项匹配,添加到结果 if (matched) { result.push(item); } // 如果是目录,递归搜索子项 if (item.type === 'directory' && item.items) { const childResults = recursiveSearch(item.items); // 如果子项中有匹配的,或者目录名匹配,则添加整个目录 if (childResults.length > 0 || matched) { // 如果目录本身没有被添加,则添加 if (!matched) { result.push({ ...item, items: childResults }); } } } }); return result; } // 执行搜索 const filteredFiles = recursiveSearch(fileStructure); console.log(filteredFiles, 'filteredFiles'); renderFileTable(filteredFiles); } const fileInput = document.getElementById('fileInput'); const folderInput = document.getElementById('folderInput'); // 文件按钮点击处理 function handleFileButtonClick(event) { event.stopPropagation(); // 阻止事件冒泡 fileInput.click(); // 使用普通文件选择 } // 文件夹按钮点击处理 function handleFolderButtonClick(event) { event.stopPropagation(); // 阻止事件冒泡 folderInput.click(); // 使用文件夹选择 } // 添加文件浏览相关代码 // 添加按钮点击事件处理 document.addEventListener('DOMContentLoaded', function () { // 文件上传处理 fileInput.addEventListener('change', function (e) { e.stopPropagation(); const files = Array.from(e.target.files || []); if (files.length > 0) { handleFiles(files); } }); // 文件夹上传处理 folderInput.addEventListener('change', function (e) { e.stopPropagation(); const files = Array.from(e.target.files || []); if (files.length > 0) { handleFiles(files); } }); }); // 防止文件输入框的点击事件冒泡 fileInput.addEventListener('click', function (e) { e.stopPropagation(); }); // 防止文件夹输入框的点击事件冒泡 folderInput.addEventListener('click', function (e) { e.stopPropagation(); }); // 更新拖放区域的提示文本 const dropZone = document.getElementById('dropZone'); dropZone.addEventListener('dragover', function (e) { e.preventDefault(); this.classList.add('drag-over'); const items = e.dataTransfer?.items; if (items && items.length > 0) { if (items[0].webkitGetAsEntry()?.isDirectory) { this.querySelector('.upload-text').textContent = '释放以上传文件夹'; } else { this.querySelector('.upload-text').textContent = '释放以上传文件'; } } }); dropZone.addEventListener('dragleave', function () { this.classList.remove('drag-over'); this.querySelector('.upload-text').textContent = '拖放文件到这里或点击选择文件'; }); const folderStructureEl = document.getElementById('folderStructure'); function getFileIcon(filename) { const ext = filename.split('.').pop()?.toLowerCase(); const icons = { pdf: '📄', doc: '📄', docx: '📄', xls: '📊', xlsx: '📊', txt: '📝', jpg: '🖼️', jpeg: '🖼️', png: '🖼️', gif: '🖼️', mp3: '🎵', mp4: '🎥', zip: '📦', rar: '📦', default: '📄' }; return icons[ext] || icons.default; } // 添加当前路径状态 let currentPath = ''; // 添加面包屑导航渲染函数 function renderBreadcrumb() { const container = document.createElement('div'); container.className = 'breadcrumb'; const paths = currentPath.split('/').filter(Boolean); let html = `<span class="breadcrumb-item" onclick="navigateTo('')">根目录</span>`; let currentPathBuild = ''; paths.forEach(path => { currentPathBuild += '/' + path; html += ` <span class="breadcrumb-separator">/</span> <span class="breadcrumb-item" onclick="navigateTo('${currentPathBuild}')">${path}</span> `; }); container.innerHTML = html; return container; } // 添加导航函数 function navigateTo(path) { currentPath = path; renderFileTable(filterFilesByPath(fileStructure, path)); } // 过滤指定路径的文件 function filterFilesByPath(files, path) { if (!path) return files; const paths = path.split('/').filter(Boolean); let currentFiles = files; for (const p of paths) { const folder = currentFiles.find(f => f.type === 'directory' && f.name === p); if (folder) { currentFiles = folder.items; } else { return []; } } return currentFiles; } // 修改文件表格渲染函数 function renderFileTable(files) { const container = document.getElementById('fileTableContainer'); // 添加面包屑导航 container.innerHTML = ''; container.appendChild(renderBreadcrumb()); if (!files || files.length === 0) { container.innerHTML += '<div class="empty-message">暂无文件</div>'; return; } let html = ` <table class="file-table"> <thead> <tr> <th>文件名</th> <th>大小</th> <th>修改时间</th> <th>操作</th> </tr> </thead> <tbody> `; console.log(files,'files'); files.forEach(item => { const icon = item.type === 'directory' ? '📁' : getFileIcon(item.name); const size = item.type === 'directory' ? '-' : formatFileSize(item.size); const onClick = item.type === 'directory' ? `onclick="navigateTo('${currentPath}/${item.name}')"` : `onclick="openModal('/preview${currentPath}/${item.name}')"`; html += ` <tr> <td> <div class="file-name" ${onClick}> <span class="file-icon">${icon}</span> ${item.name} </div> </td> <td>${size}</td> <td>${item.mtime}</td> <td class="action-column"> ${item.type === 'directory' ? `<button class="download-btn" onclick="downloadFolder('${item.path}')"> <span class="btn-icon">📥</span> </button>` : `<a class="download-btn" href="/${item.path}" download> <span class="btn-icon">📥</span> </a>` } <button class="delete-btn" onclick="deleteFile('${item.path}')"> <span class="btn-icon">🗑️</span> </button> </td> </tr> `; }); html += ` </tbody> </table> `; container.innerHTML += html; } // 修改原有的文件加载逻辑,存储所有文件 function loadFiles(files) { allFiles = files; // 存储所有文件 renderFileTable(files); } // 初始化渲染 document.addEventListener('DOMContentLoaded', () => { renderFileTable(fileStructure); }); // 删除文件或文件夹 async function deleteFile(path) { if (!confirm('确定要删除这个文件/文件夹吗?')) { return; } try { const response = await fetch(`/delete?path=${encodeURIComponent(path)}`, { method: 'DELETE' }); if (response.ok) { location.reload(); } else { alert('删除失败,请重试'); } } catch (error) { console.error('删除失败:', error); alert('删除失败,请重试'); } } // 如果没有文件,显示提示信息 if (!fileStructure || fileStructure.length === 0) { folderStructureEl.innerHTML = '<div style="text-align: center; color: #666;">暂无文件</div>'; } else { folderStructureEl.innerHTML = renderFolder(fileStructure); } const fileList = document.getElementById('fileList'); const uploadProgress = document.getElementById('uploadProgress'); // 点击上传区域触发文件选择 dropZone.addEventListener('click', () => { // 如果点击的是上传区域本身(不是按钮),则默认触发文件选择 if (e.target === this || e.target.classList.contains('upload-area') || e.target.classList.contains('upload-icon') || e.target.classList.contains('upload-text')) { fileInput.click(); } }); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('dragover'); }); dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('dragover'); }); dropZone.addEventListener('drop', async (e) => { e.preventDefault(); dropZone.classList.remove('dragover'); // 处理拖拽的文件和文件夹 const items = Array.from(e.dataTransfer.items); for (const item of items) { if (item.webkitGetAsEntry) { const entry = item.webkitGetAsEntry(); if (entry) { await handleEntry(entry); } } } }); // 上传单个文件 async function uploadFile(file) { // 创建进度条元素 const progressItem = document.createElement('div'); progressItem.className = 'progress-item'; progressItem.innerHTML = ` <div class="progress-header"> <span class="file-name">${file.name}</span> <span class="file-size">${formatFileSize(file.size)}</span> </div> <div class="progress-bar"> <div class="progress-fill"></div> </div> `; uploadProgress.appendChild(progressItem); const progressFill = progressItem.querySelector('.progress-fill'); try { const formData = new FormData(); formData.append('file', file); const response = await fetch('/upload', { method: 'POST', body: formData }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); if (data.success) { progressFill.style.width = '100%'; progressFill.style.backgroundColor = '#4caf50'; // 上传成功后延迟刷新页面 setTimeout(() => { location.reload(); }, 1000); } else { throw new Error(data.message || '上传失败'); } } catch (error) { console.error('上传错误:', error); progressFill.style.backgroundColor = '#f44336'; progressItem.innerHTML += ` <div class="error-message"> 上传失败: ${error.message} </div> `; } } fileInput.addEventListener('change', () => { handleFiles(fileInput.files); }); // 处理文件系统入口 async function handleEntry(entry, path = '') { if (entry.isFile) { const file = await getFileFromEntry(entry); await uploadFile(file, path); } else if (entry.isDirectory) { const dirReader = entry.createReader(); const entries = await readEntriesPromise(dirReader); const dirPath = path ? path + '/' + entry.name : entry.name; // 创建文件夹显示 const folderItem = document.createElement('div'); folderItem.className = 'file-item'; folderItem.innerHTML = ` <div> <div>📁 ${dirPath} <span class="folder-info">文件夹</span></div> <div class="folder-structure"></div> </div> `; fileList.appendChild(folderItem); // 递归处理文件夹内容 for (const childEntry of entries) { await handleEntry(childEntry, dirPath); } } } // 将 FileEntry 转换为 File 对象 function getFileFromEntry(entry) { return new Promise((resolve) => { entry.file(resolve); }); } // 读取目录内容 function readEntriesPromise(dirReader) { return new Promise((resolve) => { const entries = []; function readEntries() { dirReader.readEntries((results) => { if (results.length === 0) { resolve(entries); } else { entries.push(...results); readEntries(); } }); } readEntries(); }); } function handleFiles(files) { Array.from(files).forEach(file => { const path = file.webkitRelativePath || ''; uploadFile(file, path); }); } // 更新下载文件夹函数 async function downloadFolder(folderPath) { try { // 显示下载进度提示 const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: #333; color: white; padding: 10px 20px; border-radius: 4px; z-index: 1000; `; notification.textContent = '正在准备下载...'; document.body.appendChild(notification); const response = await fetch(`/download-folder?path=${encodeURIComponent(folderPath)}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } // 获取文件名 const contentDisposition = response.headers.get('content-disposition'); let filename = folderPath.split('/').pop() + '.zip'; if (contentDisposition) { const matches = contentDisposition.match(/filename="(.+)"/); if (matches) { filename = decodeURIComponent(matches[1]); } } // 创建下载流 const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); // 清理 window.URL.revokeObjectURL(url); document.body.removeChild(a); notification.textContent = '下载完成!'; setTimeout(() => { document.body.removeChild(notification); }, 2000); } catch (error) { console.error('下载失败:', error); alert('下载失败,请重试'); } } // 添加文件大小格式化函数 function formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } async function uploadFile(file, path = '') { const fileItem = document.createElement('div'); fileItem.className = 'file-item'; const relativePath = path ? `${path}/${file.name}` : file.name; fileItem.innerHTML = ` <div> <div>📄 ${relativePath}</div> <div class="progress"> <div class="progress-bar"></div> </div> <div class="upload-info">${formatFileSize(file.size)}</div> </div> `; fileList.appendChild(fileItem); const progressBar = fileItem.querySelector('.progress-bar'); const formData = new FormData(); formData.append('file', file); formData.append('path', path); try { const response = await fetch('/upload', { method: 'POST', body: formData }); const data = await response.json(); if (data.success) { progressBar.style.width = '100%'; progressBar.style.background = '#4caf50'; setTimeout(() => { location.reload(); }, 1000); } else { progressBar.style.background = '#f44336'; } } catch (error) { console.error('Error:', error); progressBar.style.background = '#f44336'; } } function formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } function formatSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } // 添加计算文件夹大小的函数 function calculateFolderSize(items) { let totalSize = 0; items.forEach(item => { if (item.type === 'directory') { totalSize += calculateFolderSize(item.items); } else { totalSize += item.size; } }); return totalSize; } // 修改 renderFolder 函数 function renderFolder(items, level = 0) { let html = ''; items.forEach(item => { if (item.type === 'directory') { const folderSize = calculateFolderSize(item.items); html += ` <div class="folder-item" data-path="${item.path}"> <div class="folder-header"> <div> <span class="folder-icon">📁</span> ${item.name} <span class="size-info">${formatFileSize(folderSize)}</span> </div> <div class="folder-actions"> <button class="download-folder" onclick="downloadFolder('${item.path}')"> <span class="btn-icon">📥</span> </button> <button class="delete-btn" onclick="deleteFile('${item.path}')"> <span class="btn-icon">🗑️</span> </button> </div> </div> <div class="folder-content"> ${renderFolder(item.items, level + 1)} </div> </div> `; } else { html += ` <div class="folder-item"> <div class="file-header"> <div> <span class="file-icon">${getFileIcon(item.name)}</span> ${item.name} <span class="size-info">${formatFileSize(item.size)}</span> </div> <div class="file-actions"> <a class="download-btn" href="/${item.path}" download> <span class="btn-icon">📥</span> </a> <button class="delete-btn" onclick="deleteFile('${item.path}')"> <span class="btn-icon">🗑️</span> </button> </div> </div> </div> `; } }); return html; } // 添加新的样式 const style = document.createElement('style'); style.textContent += ` .folder-header, .file-header { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-radius: 4px; } .folder-header:hover, .file-header:hover { background-color: #f5f5f5; } .folder-actions, .file-actions { display: flex; gap: 8px; align-items: center; } .size-info { color: #666; font-size: 0.9em; margin-left: 8px; } .folder-icon, .file-icon { margin-right: 8px; } @media (max-width: 768px) { .folder-header, .file-header { flex-direction: column; gap: 8px; } .folder-actions, .file-actions { width: 100%; justify-content: flex-end; } } `; document.head.appendChild(style); // 渲染文件结构 folderStructureEl.innerHTML = folderStructure.length > 0 ? renderFolder(folderStructure) : '<div style="text-align: center; color: #666;">暂无文件</div>'; // 文件夹点击展开/收起 folderStructureEl.addEventListener('click', (e) => { const folderItem = e.target.closest('.folder-item'); if (folderItem && !e.target.closest('.download-folder')) { const content = folderItem.querySelector('.folder-content'); if (content) { content.classList.toggle('open'); } } }); // 下载文件夹 async function downloadFolder(folderPath) { try { const response = await fetch(`/download-folder?path=${encodeURIComponent(folderPath)}`); if (response.ok) { const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = folderPath.split('/').pop() + '.zip'; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); } } catch (error) { console.error('下载失败:', error); alert('下载失败,请重试'); } } function openModal(filePath) { const modal = document.getElementById('filePreviewModal'); const iframe = document.getElementById('filePreviewIframe'); // 根据文件类型选择预览方式 const fileExtension = filePath.split('.').pop().toLowerCase(); const previewableExtensions = ['md', 'txt', 'html', 'js', 'css', 'json', 'xml', 'log', 'ts', 'tsx', 'less', 'scss','yaml','conf','jpg','png','webp','jpeg','gif','bmp','log']; const monacoExtensions = ['js', 'css', 'ts', 'tsx', 'less', 'scss', 'json','mjs','cjs']; // 对于可预览的文本文件 if (previewableExtensions.includes(fileExtension)) { // 如果是需要使用 Monaco Editor 的文件类型 if (monacoExtensions.includes(fileExtension)) { console.log(filePath,'filePath123'); fetch(filePath) .then(response => response.text()) .then(content => { // 映射文件扩展名到 Monaco 语言 const languageMap = { 'js': 'javascript', 'ts': 'typescript', 'tsx': 'typescript', 'css': 'css', 'less': 'less', 'scss': 'scss', 'json': 'json' }; const language = languageMap[fileExtension] || 'plaintext'; const fileName = filePath.split('/').pop(); // 使用 Monaco Editor 打开 openMonacoEditor(content, language, fileName); }) .catch(error => { console.error('文件加载错误:', error); alert('无法加载文件'); }); } else { // 对于其他文本文件,使用 iframe iframe.src = filePath; modal.style.display = 'block'; } } else { // 对于不可预览的文件,直接下载 window.open(filePath, '_blank'); } } function closeModal() { const modal = document.getElementById('filePreviewModal'); const iframe = document.getElementById('filePreviewIframe'); modal.style.display = 'none'; iframe.src = ''; } // 点击模态框外部关闭 window.onclick = function(event) { const modal = document.getElementById('filePreviewModal'); if (event.target === modal) { closeModal(); } }