pan / static /js /app.js
WACATW's picture
Update static/js/app.js
6c469c8 verified
Raw History Blame Contribute Delete
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);
});
}