Download static/js/app.js from WACATW/pan: direct link, hf CLI and curl.
- Browser
- Download file 19.8 kB
-
https://huggingface.co/spaces/WACATW/pan/resolve/main/static/js/app.js
- Command line
-
hf download hf://spaces/WACATW/pan/static/js/app.js
-
curl -L -o app.js https://huggingface.co/spaces/WACATW/pan/resolve/main/static/js/app.js
19.8 kB
| // DOM Elements | |
| const uploadBtn = document.getElementById('uploadBtn'); | |
| const mkdirBtn = document.getElementById('mkdirBtn'); | |
| const refreshBtn = document.getElementById('refreshBtn'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const uploadZone = document.getElementById('uploadZone'); | |
| const fileList = document.getElementById('fileList'); | |
| // Modals | |
| const mkdirModal = document.getElementById('mkdirModal'); | |
| const deleteModal = document.getElementById('deleteModal'); | |
| const folderNameInput = document.getElementById('folderNameInput'); | |
| const deleteFileName = document.getElementById('deleteFileName'); | |
| // Progress | |
| const uploadProgress = document.getElementById('uploadProgress'); | |
| const progressText = document.getElementById('progressText'); | |
| const progressFill = document.getElementById('progressFill'); | |
| // 👇 新增这两行(对应你 index.html 里新增的 span) | |
| const progressPercent = document.getElementById('progressPercent'); | |
| const uploadSpeed = document.getElementById('uploadSpeed'); | |
| // Toast | |
| const toast = document.getElementById('toast'); | |
| const toastIcon = document.getElementById('toastIcon'); | |
| const toastMessage = document.getElementById('toastMessage'); | |
| // State | |
| let deleteTarget = null; | |
| let deleteType = null; | |
| // Toast function | |
| function showToast(message, type = 'success') { | |
| const toastEl = document.getElementById('toast'); | |
| const msgEl = document.getElementById('toastMessage'); | |
| const iconEl = document.getElementById('toastIcon'); | |
| // 容错处理:如果找不到 Toast 元素,直接在控制台打印,避免阻塞后续代码 | |
| if (!toastEl || !msgEl || !iconEl) { | |
| console.warn('Toast elements not found. Message:', message); | |
| return; | |
| } | |
| msgEl.textContent = message; | |
| iconEl.className = 'toast-icon fas '; | |
| if (type === 'success') { | |
| iconEl.classList.add('fa-check-circle', 'success'); | |
| } else if (type === 'error') { | |
| iconEl.classList.add('fa-exclamation-circle', 'error'); | |
| } else { | |
| iconEl.classList.add('fa-info-circle', 'info'); | |
| } | |
| toastEl.classList.add('show'); | |
| setTimeout(() => { | |
| toastEl.classList.remove('show'); | |
| }, 3000); | |
| } | |
| // Upload functions (修改版:支持百分比和网速) | |
| function uploadFiles(files) { | |
| if (files.length === 0) return; | |
| uploadProgress.classList.add('show'); | |
| let uploaded = 0; | |
| let failed = 0; | |
| const uploadNext = (index) => { | |
| if (index >= files.length) { | |
| setTimeout(() => { | |
| uploadProgress.classList.remove('show'); | |
| if (failed > 0) { | |
| showToast(`${uploaded} 个文件上传成功,${failed} 个失败`, 'error'); | |
| } else { | |
| showToast(`${uploaded} 个文件上传成功!`, 'success'); | |
| } | |
| updateStorageInfo(); // 上传完成后更新容量 | |
| refreshFileList(); | |
| }, 500); | |
| return; | |
| } | |
| const file = files[index]; | |
| progressText.textContent = `正在上传: ${file.name} (${index + 1}/${files.length})`; | |
| progressFill.style.width = '0%'; | |
| progressPercent.textContent = '0%'; | |
| uploadSpeed.textContent = '0 KB/s'; | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| formData.append('path', currentPath); | |
| const xhr = new XMLHttpRequest(); | |
| let lastLoaded = 0; | |
| let lastTime = Date.now(); | |
| // 监听上传进度 | |
| xhr.upload.onprogress = function(event) { | |
| if (event.lengthComputable) { | |
| const percent = Math.round((event.loaded / event.total) * 100); | |
| progressPercent.textContent = percent + '%'; | |
| progressFill.style.width = percent + '%'; | |
| const now = Date.now(); | |
| const timeDiff = (now - lastTime) / 1000; | |
| if (timeDiff > 0.5) { | |
| const byteDiff = event.loaded - lastLoaded; | |
| const speedBps = byteDiff / timeDiff; | |
| uploadSpeed.textContent = formatSpeed(speedBps); | |
| lastLoaded = event.loaded; | |
| lastTime = now; | |
| } | |
| } | |
| }; | |
| xhr.onload = function() { | |
| if (xhr.status === 200) { | |
| try { | |
| const data = JSON.parse(xhr.responseText); | |
| if (data.success) { | |
| uploaded++; | |
| } else { | |
| failed++; | |
| console.error('后端上传失败:', data.error); | |
| } | |
| } catch (e) { | |
| failed++; | |
| console.error('解析响应失败:', e); | |
| } | |
| } else { | |
| failed++; | |
| console.error('上传请求失败, 状态码:', xhr.status); | |
| } | |
| uploadNext(index + 1); // 无论成功失败,继续下一个 | |
| }; | |
| xhr.open('POST', '/api/upload', true); | |
| xhr.send(formData); | |
| }; // 👈 这里闭合 uploadNext 箭头函数 | |
| uploadNext(0); | |
| } // 👈 这里闭合 uploadFiles 函数 | |
| // 网速格式化辅助函数 | |
| function formatSpeed(bytesPerSecond) { | |
| if (bytesPerSecond < 1024) return bytesPerSecond.toFixed(1) + ' B/s'; | |
| if (bytesPerSecond < 1024 * 1024) return (bytesPerSecond / 1024).toFixed(1) + ' KB/s'; | |
| return (bytesPerSecond / (1024 * 1024)).toFixed(2) + ' MB/s'; | |
| } | |
| // Refresh file list | |
| function refreshFileList() { | |
| fetch(`/api/list?path=${encodeURIComponent(currentPath)}`) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| renderFileList(data.files); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Refresh error:', error); | |
| showToast('刷新失败', 'error'); | |
| }); | |
| } | |
| // Render file list | |
| function renderFileList(files) { | |
| const container = document.querySelector('.file-list-container'); | |
| if (!files || files.length === 0) { | |
| container.innerHTML = ` | |
| <div class="empty-state"> | |
| <i class="fas fa-folder-open empty-icon"></i> | |
| <p class="empty-text">此文件夹为空</p> | |
| <p class="empty-subtext">上传文件或创建文件夹开始使用</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| // Sort: directories first | |
| files.sort((a, b) => { | |
| if (a.type === b.type) { | |
| return a.name.localeCompare(b.name); | |
| } | |
| return a.type === 'directory' ? -1 : 1; | |
| }); | |
| const tableHtml = ` | |
| <table class="file-table"> | |
| <thead> | |
| <tr> | |
| <th>名称</th> | |
| <th>大小</th> | |
| <th>修改时间</th> | |
| <th>操作</th> | |
| </tr> | |
| </thead> | |
| <tbody id="fileList"> | |
| ${files.map(file => ` | |
| <tr class="file-item" data-name="${file.name}" data-type="${file.type}"> | |
| <td class="file-name-cell"> | |
| ${file.type === 'directory' | |
| ? `<i class="fas fa-folder folder-icon"></i> | |
| <a href="/?path=${encodeURIComponent(currentPath ? currentPath + '/' + file.name : file.name)}" class="file-name">${file.name}</a>` | |
| : `<i class="fas fa-file file-icon"></i> | |
| <span class="file-name">${file.name}</span>` | |
| } | |
| </td> | |
| <td class="file-size">${formatSize(file.size)}</td> | |
| <td class="file-modified">${file.modified || '-'}</td> | |
| <td class="file-actions"> | |
| ${file.type === 'file' | |
| ? `<button class="action-btn download-btn" title="下载" data-filename="${file.name}"> | |
| <i class="fas fa-download"></i> | |
| </button> | |
| <button class="action-btn view-btn" title="查看" data-filename="${file.name}"> | |
| <i class="fas fa-eye"></i> | |
| </button>` | |
| : '' | |
| } | |
| <button class="action-btn move-btn" title="移动" data-filename="${file.name}"> | |
| <i class="fas fa-arrows-alt"></i> | |
| </button> | |
| <button class="action-btn delete-btn" title="删除" data-filename="${file.name}" data-type="${file.type}"> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| </td> | |
| </tr> | |
| `).join('')} | |
| </tbody> | |
| </table> | |
| `; | |
| container.innerHTML = tableHtml; | |
| // Re-attach event listeners | |
| attachFileEventListeners(); | |
| } | |
| // Format file size | |
| function formatSize(sizeBytes) { | |
| if (!sizeBytes || sizeBytes === 0 || sizeBytes === '-') { | |
| return '-'; | |
| } | |
| try { | |
| const size = parseInt(sizeBytes); | |
| const units = ['B', 'KB', 'MB', 'GB', 'TB']; | |
| let i = 0; | |
| let s = size; | |
| while (s >= 1024 && i < units.length - 1) { | |
| s /= 1024; | |
| i++; | |
| } | |
| return s.toFixed(2) + ' ' + units[i]; | |
| } catch { | |
| return '-'; | |
| } | |
| } | |
| // Attach event listeners to file items | |
| function attachFileEventListeners() { | |
| // Download buttons | |
| document.querySelectorAll('.download-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| const filename = btn.dataset.filename; | |
| downloadFile(filename); | |
| }); | |
| }); | |
| // 👇 新增:查看按钮监听 | |
| document.querySelectorAll('.view-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| viewFile(btn.dataset.filename); | |
| }); | |
| }); | |
| // 👇 新增:移动按钮监听 | |
| document.querySelectorAll('.move-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| moveFile(btn.dataset.filename); | |
| }); | |
| }); | |
| // Delete buttons | |
| document.querySelectorAll('.delete-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| const filename = btn.dataset.filename; | |
| const type = btn.dataset.type; | |
| showDeleteModal(filename, type); | |
| }); | |
| }); | |
| } | |
| // Download file | |
| function downloadFile(filename) { | |
| const url = `/api/download?filename=${encodeURIComponent(filename)}&path=${encodeURIComponent(currentPath)}`; | |
| window.location.href = url; | |
| } | |
| // 移动文件 | |
| function moveFile(filename) { | |
| if (!filename || filename === 'undefined') return; | |
| const targetDir = prompt(`将 [${filename}] 移动到哪个文件夹?(如:documents,留空表示根目录)`, ""); | |
| if (targetDir === null) return; | |
| fetch('/api/move', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ filename: filename, source_path: currentPath, target_dir: targetDir }) | |
| }) | |
| .then(res => { | |
| if (!res.ok) throw new Error('接口不存在或请求失败'); | |
| return res.json(); | |
| }) | |
| .then(data => { | |
| if (data.success) { | |
| showToast('移动成功', 'success'); | |
| refreshFileList(); | |
| } else { | |
| showToast(data.error || '移动失败', 'error'); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Move error:', error); | |
| showToast('移动失败,接口未找到', 'error'); | |
| }); | |
| } | |
| // 查看文本文件 | |
| function viewFile(filename) { | |
| if (!filename || filename === 'undefined') return; | |
| fetch(`/api/view?filename=${encodeURIComponent(filename)}&path=${encodeURIComponent(currentPath)}`) | |
| .then(res => { | |
| if (!res.ok) throw new Error('接口不存在或请求失败'); | |
| return res.json(); | |
| }) | |
| .then(data => { | |
| if (data.success) { | |
| // 如果 index.html 中有 view-modal,则显示 | |
| const viewModal = document.getElementById('view-modal'); | |
| if (viewModal) { | |
| document.getElementById('view-filename').textContent = data.filename; | |
| document.getElementById('view-content').textContent = data.content; | |
| viewModal.style.display = 'block'; | |
| } else { | |
| // 如果还没有在 HTML 中添加模态框,先用 alert 简单展示 | |
| alert(`文件名: ${data.filename}\n\n${data.content}`); | |
| } | |
| } else { | |
| showToast(data.error || '无法查看该文件', 'error'); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('View error:', error); | |
| showToast('查看失败,接口未找到', 'error'); | |
| }); | |
| } | |
| // Create directory | |
| function createDirectory(dirname) { | |
| fetch('/api/mkdir', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| dirname: dirname, | |
| path: currentPath | |
| }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| showToast('文件夹创建成功!', 'success'); | |
| updateStorageInfo(); // 👈 加在这里,创建成功后更新容量 | |
| closeMkdirModal(); | |
| refreshFileList(); | |
| } else { | |
| showToast(data.error || '创建失败', 'error'); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Mkdir error:', error); | |
| showToast('创建失败', 'error'); | |
| }); | |
| } | |
| // Delete file/directory | |
| function deleteItem(filename) { | |
| fetch('/api/delete', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| filename: filename, | |
| path: currentPath | |
| }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| showToast('删除成功!', 'success'); | |
| updateStorageInfo(); // 👈 加在这里,删除成功后更新容量 | |
| closeDeleteModal(); | |
| refreshFileList(); | |
| } else { | |
| showToast(data.error || '删除失败', 'error'); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Delete error:', error); | |
| showToast('删除失败', 'error'); | |
| }); | |
| } | |
| // Modal functions | |
| function showMkdirModal() { | |
| mkdirModal.classList.add('show'); | |
| folderNameInput.value = ''; | |
| setTimeout(() => folderNameInput.focus(), 100); | |
| } | |
| function closeMkdirModal() { | |
| mkdirModal.classList.remove('show'); | |
| } | |
| function showDeleteModal(filename, type) { | |
| deleteTarget = filename; | |
| deleteType = type; | |
| deleteFileName.textContent = filename; | |
| deleteModal.classList.add('show'); | |
| } | |
| function closeDeleteModal() { | |
| deleteModal.classList.remove('show'); | |
| deleteTarget = null; | |
| deleteType = null; | |
| } | |
| // Event Listeners | |
| // Upload button | |
| uploadBtn.addEventListener('click', () => { | |
| fileInput.click(); | |
| }); | |
| fileInput.addEventListener('change', (e) => { | |
| if (e.target.files.length > 0) { | |
| uploadFiles(e.target.files); | |
| fileInput.value = ''; | |
| } | |
| }); | |
| // Upload zone click | |
| uploadZone.addEventListener('click', () => { | |
| fileInput.click(); | |
| }); | |
| // Drag and drop | |
| uploadZone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| uploadZone.classList.add('drag-over'); | |
| }); | |
| uploadZone.addEventListener('dragleave', (e) => { | |
| e.preventDefault(); | |
| uploadZone.classList.remove('drag-over'); | |
| }); | |
| uploadZone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| uploadZone.classList.remove('drag-over'); | |
| if (e.dataTransfer.files.length > 0) { | |
| uploadFiles(e.dataTransfer.files); | |
| } | |
| }); | |
| // Mkdir button | |
| mkdirBtn.addEventListener('click', showMkdirModal); | |
| document.getElementById('closeMkdirModal').addEventListener('click', closeMkdirModal); | |
| document.getElementById('cancelMkdir').addEventListener('click', closeMkdirModal); | |
| document.getElementById('confirmMkdir').addEventListener('click', () => { | |
| const dirname = folderNameInput.value.trim(); | |
| if (dirname) { | |
| createDirectory(dirname); | |
| } else { | |
| showToast('请输入文件夹名称', 'error'); | |
| } | |
| }); | |
| folderNameInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| const dirname = folderNameInput.value.trim(); | |
| if (dirname) { | |
| createDirectory(dirname); | |
| } | |
| } | |
| }); | |
| // Delete modal | |
| document.getElementById('closeDeleteModal').addEventListener('click', closeDeleteModal); | |
| document.getElementById('cancelDelete').addEventListener('click', closeDeleteModal); | |
| document.getElementById('confirmDelete').addEventListener('click', () => { | |
| if (deleteTarget) { | |
| deleteItem(deleteTarget); | |
| } | |
| }); | |
| // Refresh button | |
| refreshBtn.addEventListener('click', () => { | |
| refreshBtn.querySelector('i').classList.add('fa-spin'); | |
| refreshFileList(); | |
| setTimeout(() => { | |
| refreshBtn.querySelector('i').classList.remove('fa-spin'); | |
| }, 500); | |
| }); | |
| // Close modals on outside click | |
| mkdirModal.addEventListener('click', (e) => { | |
| if (e.target === mkdirModal) { | |
| closeMkdirModal(); | |
| } | |
| }); | |
| deleteModal.addEventListener('click', (e) => { | |
| if (e.target === deleteModal) { | |
| closeDeleteModal(); | |
| } | |
| }); | |
| // Keyboard shortcuts | |
| document.addEventListener('keydown', (e) => { | |
| // Escape to close modals | |
| if (e.key === 'Escape') { | |
| closeMkdirModal(); | |
| closeDeleteModal(); | |
| } | |
| }); | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| attachFileEventListeners(); | |
| initUserFeatures(); | |
| }); | |
| // User features | |
| function initUserFeatures() { | |
| // Logout button | |
| const logoutBtn = document.getElementById('logoutBtn'); | |
| if (logoutBtn) { | |
| logoutBtn.addEventListener('click', logout); | |
| } | |
| } | |
| // Logout function | |
| function logout() { | |
| fetch('/api/logout', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| } | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| showToast('已退出登录', 'success'); | |
| setTimeout(() => { | |
| window.location.href = '/login'; | |
| }, 500); | |
| } else { | |
| showToast(data.error || '退出失败', 'error'); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Logout error:', error); | |
| showToast('退出失败', 'error'); | |
| }); | |
| } | |
| // Update storage info | |
| function updateStorageInfo() { | |
| fetch('/api/storage-info') | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| const storageFill = document.querySelector('.storage-fill'); | |
| const storageText = document.querySelector('.storage-text'); | |
| if (storageFill) { | |
| storageFill.style.width = data.storage_percent + '%'; | |
| } | |
| if (storageText) { | |
| storageText.textContent = `${data.used_storage_formatted} / ${data.max_storage_formatted}`; | |
| } | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Storage info error:', error); | |
| }); | |
| } | |