/** * 文件上传网站 - 前端交互逻辑 */ (function () { 'use strict'; // ── 常量 ──────────────────────────────────────────────── const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20MB const MAX_FILES = 9; const ALLOWED_EXTENSIONS = ['.txt', '.doc', '.docx', '.pdf', '.rtf', '.odt', '.html', '.csv', '.xls', '.xlsx']; // ── DOM 元素 ──────────────────────────────────────────── const $name = document.getElementById('name'); const $dropzone = document.getElementById('dropzone'); const $fileInput = document.getElementById('fileInput'); const $fileList = document.getElementById('fileList'); const $uploadBtn = document.getElementById('uploadBtn'); const $clearBtn = document.getElementById('clearBtn'); const $uploadForm = document.getElementById('uploadForm'); const $resultPanel = document.getElementById('resultPanel'); // 弹窗相关 const $modal = document.getElementById('captchaModal'); const $modalClose = document.getElementById('modalClose'); const $modalCancel = document.getElementById('modalCancel'); const $modalConfirm = document.getElementById('modalConfirm'); const $captchaImg = document.getElementById('captchaImg'); const $refreshCaptcha = document.getElementById('refreshCaptcha'); const $captchaInput = document.getElementById('captchaInput'); const $captchaError = document.getElementById('captchaError'); // ── 状态 ──────────────────────────────────────────────── let selectedFiles = []; // { file, id, status, error } let fileIdCounter = 0; let isUploading = false; // ── 工具函数 ──────────────────────────────────────────── function formatSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } function getFileExt(filename) { const idx = filename.lastIndexOf('.'); return idx >= 0 ? filename.substring(idx).toLowerCase() : ''; } function validateFile(file) { const ext = getFileExt(file.name); if (!ALLOWED_EXTENSIONS.includes(ext)) { return '不支持的文件格式'; } if (file.size > MAX_FILE_SIZE) { return '文件超过 20MB 限制'; } if (file.size === 0) { return '文件为空'; } return null; } // ── 弹窗控制 ──────────────────────────────────────────── function openModal() { $captchaInput.value = ''; $captchaError.textContent = ''; refreshCaptcha(); $modal.classList.add('active'); setTimeout(() => $captchaInput.focus(), 200); } function closeModal() { $modal.classList.remove('active'); } function refreshCaptcha() { $captchaImg.src = '/api/captcha?' + Date.now(); $captchaInput.value = ''; $captchaError.textContent = ''; } // 弹窗事件绑定 $modalClose.addEventListener('click', closeModal); $modalCancel.addEventListener('click', closeModal); $captchaImg.addEventListener('click', refreshCaptcha); $refreshCaptcha.addEventListener('click', refreshCaptcha); // 点击遮罩层关闭 $modal.addEventListener('click', function (e) { if (e.target === $modal) closeModal(); }); // ESC 关闭弹窗 document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && $modal.classList.contains('active')) { closeModal(); } }); // 弹窗内回车 = 确认 $captchaInput.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); $modalConfirm.click(); } }); // 确认上传按钮 $modalConfirm.addEventListener('click', function () { const captchaVal = $captchaInput.value.trim(); if (!captchaVal) { $captchaError.textContent = '请输入验证码'; shakeElement($captchaInput); return; } const nameVal = $name.value.trim(); const validFiles = selectedFiles.filter(f => f.status !== 'error'); closeModal(); startUpload(nameVal, captchaVal, validFiles); }); // ── 文件选择(点击) ──────────────────────────────────── $dropzone.addEventListener('click', function () { if (!isUploading) { $fileInput.click(); } }); $fileInput.addEventListener('change', function () { addFiles(Array.from($fileInput.files)); $fileInput.value = ''; // 清空以便重复选择同名文件 }); // ── 文件拖拽 ──────────────────────────────────────────── $dropzone.addEventListener('dragover', function (e) { e.preventDefault(); if (!isUploading) { $dropzone.classList.add('dragover'); } }); $dropzone.addEventListener('dragleave', function (e) { e.preventDefault(); $dropzone.classList.remove('dragover'); }); $dropzone.addEventListener('drop', function (e) { e.preventDefault(); $dropzone.classList.remove('dragover'); if (!isUploading) { addFiles(Array.from(e.dataTransfer.files)); } }); // ── 添加文件到列表 ────────────────────────────────────── function addFiles(files) { if (isUploading) return; for (const file of files) { if (selectedFiles.length >= MAX_FILES) { showToast(`单次最多上传 ${MAX_FILES} 个文件`); break; } const error = validateFile(file); const item = { file: file, id: ++fileIdCounter, status: error ? 'error' : 'waiting', error: error, progress: 0 }; selectedFiles.push(item); } renderFileList(); updateButtonState(); } // ── 渲染文件列表 ──────────────────────────────────────── function renderFileList() { if (selectedFiles.length === 0) { $fileList.innerHTML = ''; return; } $fileList.innerHTML = selectedFiles.map(item => { const statusText = { waiting: '等待中', uploading: `${item.progress}%`, done: '已完成', error: item.error || '失败' }[item.status]; const statusClass = { waiting: 'status-waiting', uploading: 'status-uploading', done: 'status-done', error: 'status-error' }[item.status]; const iconSvg = item.status === 'done' ? '' : item.status === 'error' ? '' : ''; const progressBar = item.status === 'uploading' ? `
` : ''; const removeBtn = !isUploading ? `` : ''; return `
${iconSvg}
${item.file.name}
${formatSize(item.file.size)}
${progressBar}
${statusText} ${removeBtn}
`; }).join(''); // 绑定移除按钮 $fileList.querySelectorAll('.file-item-remove').forEach(btn => { btn.addEventListener('click', function (e) { e.stopPropagation(); const id = parseInt(this.dataset.id); selectedFiles = selectedFiles.filter(f => f.id !== id); renderFileList(); updateButtonState(); }); }); } // ── 按钮状态 ──────────────────────────────────────────── function updateButtonState() { const nameValid = $name.value.trim().length >= 2; const hasFiles = selectedFiles.some(f => f.status !== 'error'); $uploadBtn.disabled = !nameValid || !hasFiles || isUploading; } $name.addEventListener('input', updateButtonState); // ── 清空列表 ──────────────────────────────────────────── $clearBtn.addEventListener('click', function () { if (isUploading) return; selectedFiles = []; renderFileList(); updateButtonState(); $resultPanel.innerHTML = ''; }); // ── 点击"开始上传" → 打开验证码弹窗 ───────────────────── $uploadForm.addEventListener('submit', function (e) { e.preventDefault(); if (isUploading) return; // 前端校验姓名 const nameVal = $name.value.trim(); if (nameVal.length < 2) { shakeElement($name); return; } // 校验文件 const validFiles = selectedFiles.filter(f => f.status !== 'error'); if (validFiles.length === 0) { showToast('没有可上传的文件'); return; } // 打开验证码弹窗 openModal(); }); // ── 执行上传 ──────────────────────────────────────────── function startUpload(name, captcha, files) { isUploading = true; $uploadBtn.disabled = true; $clearBtn.disabled = true; $resultPanel.innerHTML = ''; // 重置文件状态 files.forEach(f => { f.status = 'uploading'; f.progress = 0; }); renderFileList(); const formData = new FormData(); formData.append('name', name); formData.append('captcha', captcha); files.forEach(f => formData.append('files', f.file)); const xhr = new XMLHttpRequest(); // 进度事件 xhr.upload.addEventListener('progress', function (e) { if (e.lengthComputable) { const pct = Math.round((e.loaded / e.total) * 100); files.forEach(f => { f.progress = pct; }); renderFileList(); } }); // 完成事件 xhr.addEventListener('load', function () { isUploading = false; $clearBtn.disabled = false; if (xhr.status === 200) { const data = JSON.parse(xhr.responseText); handleUploadSuccess(data, files); } else { let errMsg = '上传失败,请重试'; try { const errData = JSON.parse(xhr.responseText); errMsg = errData.error || errMsg; } catch (_) { /* ignore */ } // 如果是验证码错误,重新弹窗让用户输入 if (errMsg.includes('验证码')) { files.forEach(f => { f.status = 'waiting'; f.progress = 0; }); renderFileList(); updateButtonState(); showToast(errMsg); // 延迟一下再弹窗,让用户看到 toast setTimeout(openModal, 600); } else { handleUploadError(errMsg, files); } } updateButtonState(); }); xhr.addEventListener('error', function () { isUploading = false; $clearBtn.disabled = false; handleUploadError('网络错误,请检查连接后重试', files); updateButtonState(); }); xhr.open('POST', '/api/upload'); xhr.send(formData); } // ── 上传成功处理 ──────────────────────────────────────── function handleUploadSuccess(data, files) { // 更新每个文件的状态 data.results.forEach((result, idx) => { if (idx < files.length) { files[idx].status = result.success ? 'done' : 'error'; files[idx].error = result.success ? null : result.error; files[idx].progress = 100; } }); renderFileList(); // 显示结果面板 const hasSuccess = data.success_count > 0; const hasFail = data.fail_count > 0; let resultHtml = `
${hasSuccess ? '' : '' }
${hasSuccess && !hasFail ? '上传成功' : hasSuccess ? '部分上传成功' : '上传失败'}
${hasSuccess ? `

已上传 ${data.success_count} 个文件至 ${data.storage_path}

` : ''} ${hasFail ? `

${data.fail_count} 个文件上传失败

` : ''} ${hasSuccess ? `

该目录累计文件数:${data.total_files}

` : ''}
`; // 文件详情 if (data.results.length > 0) { resultHtml += '
'; data.results.forEach(r => { if (r.success) { resultHtml += `
${r.filename} → ${r.storage_filename}
`; } else { resultHtml += `
${r.filename} — ${r.error}
`; } }); resultHtml += '
'; } resultHtml += '
'; $resultPanel.innerHTML = resultHtml; } // ── 上传失败处理 ──────────────────────────────────────── function handleUploadError(message, files) { files.forEach(f => { f.status = 'error'; f.error = message; }); renderFileList(); $resultPanel.innerHTML = `
上传失败

${message}

`; } // ── 抖动效果 ──────────────────────────────────────────── function shakeElement(el) { el.classList.add('shake', 'input-error'); setTimeout(() => { el.classList.remove('shake'); }, 400); setTimeout(() => { el.classList.remove('input-error'); }, 2000); } // ── Toast 提示 ────────────────────────────────────────── function showToast(message) { const existing = document.querySelector('.toast'); if (existing) existing.remove(); const toast = document.createElement('div'); toast.className = 'toast'; toast.textContent = message; toast.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: #1a1816; color: white; padding: 0.6rem 1.2rem; border-radius: 8px; font-size: 0.85rem; font-weight: 500; z-index: 1100; box-shadow: 0 4px 20px rgba(0,0,0,0.15); `; document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; toast.style.transition = 'opacity 0.3s'; setTimeout(() => toast.remove(), 300); }, 2500); } // ── 初始化 ────────────────────────────────────────────── updateButtonState(); })();