// ========== 写死的选项数据 ========== const ALL_CAMPS = []; for (let i = 13; i <= 26; i++) { ALL_CAMPS.push(`训练营${i}期`); } const ALL_CLASSES = []; for (let i = 1; i <= 25; i++) { ALL_CLASSES.push(`${i}班`); } const ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp']; const MAX_FILE_SIZE_MB = 20; const MAX_FILES_COUNT = 100; // ========== 状态管理 ========== let state = { selectedCamp: '', selectedClass: '', selectedFiles: [], // { file, id, previewUrl } uploadResults: [], // { file, id, status, filename, reason, previewUrl } isUploading: false, }; // ========== DOM 元素 ========== const $ = (sel) => document.querySelector(sel); const campSelect = $('#camp-select'); const classSelect = $('#class-select'); const fileInput = $('#file-input'); const uploadSection = $('.upload-section'); const selectArea = $('#select-area'); const previewSection = $('#preview-section'); const previewGrid = $('#preview-grid'); const selectedCount = $('#selected-count'); const btnUpload = $('#btn-upload'); const btnClearAll = $('#btn-clear-all'); const progressSection = $('#progress-section'); const progressText = $('#progress-text'); const progressPercent = $('#progress-percent'); const progressFill = $('#progress-fill'); const fileProgressList = $('#file-progress-list'); const resultSection = $('#result-section'); const resultSummary = $('#result-summary'); const resultGrid = $('#result-grid'); const btnRetry = $('#btn-retry'); const btnNewUpload = $('#btn-new-upload'); // ========== 初始化 ========== function init() { loadFolders(); bindEvents(); } // ========== 文件夹加载(前端写死选项) ========== function loadFolders() { campSelect.innerHTML = ''; ALL_CAMPS.forEach(camp => { const opt = document.createElement('option'); opt.value = camp; opt.textContent = camp; campSelect.appendChild(opt); }); } function onCampChange() { const campName = campSelect.value; state.selectedCamp = campName; classSelect.innerHTML = ''; classSelect.disabled = true; state.selectedClass = ''; if (!campName) { classSelect.innerHTML = ''; return; } classSelect.disabled = false; classSelect.innerHTML = ''; ALL_CLASSES.forEach(cls => { const opt = document.createElement('option'); opt.value = cls; opt.textContent = cls; classSelect.appendChild(opt); }); } function onClassChange() { state.selectedClass = classSelect.value; } // ========== 文件选择 ========== function onFileSelect(e) { const files = Array.from(e.target.files); if (!files.length) return; const remaining = MAX_FILES_COUNT - state.selectedFiles.length; if (remaining <= 0) { alert(`最多只能选择 ${MAX_FILES_COUNT} 张图片`); fileInput.value = ''; return; } const filesToAdd = files.slice(0, remaining); let rejected = 0; filesToAdd.forEach(file => { if (file.size > MAX_FILE_SIZE_MB * 1024 * 1024) { rejected++; return; } const ext = file.name.split('.').pop().toLowerCase(); if (!ALLOWED_EXTENSIONS.includes(ext)) { rejected++; return; } const id = Date.now() + '_' + Math.random().toString(36).substr(2, 6); const previewUrl = URL.createObjectURL(file); state.selectedFiles.push({ file, id, previewUrl }); }); if (rejected > 0) { alert(`已跳过 ${rejected} 个文件(格式不支持或超过 20MB)`); } fileInput.value = ''; renderPreview(); } function removeFile(id) { const idx = state.selectedFiles.findIndex(f => f.id === id); if (idx !== -1) { URL.revokeObjectURL(state.selectedFiles[idx].previewUrl); state.selectedFiles.splice(idx, 1); } renderPreview(); } function clearAllFiles() { state.selectedFiles.forEach(f => URL.revokeObjectURL(f.previewUrl)); state.selectedFiles = []; renderPreview(); } function renderPreview() { const count = state.selectedFiles.length; if (count === 0) { previewSection.style.display = 'none'; selectArea.style.display = 'block'; return; } selectArea.style.display = 'none'; previewSection.style.display = 'block'; selectedCount.textContent = count; btnUpload.disabled = !state.selectedCamp || !state.selectedClass || state.isUploading; previewGrid.innerHTML = ''; state.selectedFiles.forEach(item => { const div = document.createElement('div'); div.className = 'preview-item'; div.innerHTML = ` ${item.file.name}
${item.file.name}
`; previewGrid.appendChild(div); }); } // ========== 上传逻辑(真实API) ========== function uploadFile(file, campName, className, onProgress) { return new Promise((resolve) => { const formData = new FormData(); formData.append('folder', campName); formData.append('class', className); formData.append('files', file); const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/upload'); xhr.upload.onprogress = (e) => { if (e.lengthComputable) { onProgress(Math.round((e.loaded / e.total) * 100)); } }; xhr.onload = () => { try { const data = JSON.parse(xhr.responseText); if (xhr.status === 200 && data.uploaded && data.uploaded.length > 0) { resolve({ type: 'success', filename: data.uploaded[0].filename }); } else if (data.failed && data.failed.length > 0) { resolve({ type: 'failed', reason: data.failed[0].reason }); } else { resolve({ type: 'failed', reason: data.error || '上传失败' }); } } catch (e) { resolve({ type: 'failed', reason: '服务器响应异常' }); } }; xhr.onerror = () => { resolve({ type: 'failed', reason: '网络错误' }); }; xhr.ontimeout = () => { resolve({ type: 'failed', reason: '请求超时' }); }; xhr.timeout = 120000; // 2分钟超时 xhr.send(formData); }); } async function startUpload() { if (!state.selectedCamp || !state.selectedClass) { alert('请先选择训练营和班级'); return; } if (state.selectedFiles.length === 0) { alert('请先选择图片'); return; } state.isUploading = true; btnUpload.disabled = true; btnUpload.textContent = '上传中...'; btnUpload.classList.add('uploading'); // 切换到进度视图 previewSection.style.display = 'none'; resultSection.style.display = 'none'; progressSection.style.display = 'block'; // 初始化进度列表 fileProgressList.innerHTML = ''; state.selectedFiles.forEach(item => { const div = document.createElement('div'); div.className = 'file-progress-item'; div.id = `progress-${item.id}`; div.innerHTML = `
${item.file.name}
等待上传...
`; fileProgressList.appendChild(div); }); // 逐张上传 let completed = 0; const total = state.selectedFiles.length; state.uploadResults = []; for (const item of state.selectedFiles) { const el = $(`#progress-${item.id}`); const icon = el.querySelector('.status-icon'); const detail = el.querySelector('.detail'); const miniFill = el.querySelector('.mini-fill'); // 上传中 icon.textContent = '📤'; detail.textContent = '上传中...'; // 调用真实API const result = await uploadFile(item.file, state.selectedCamp, state.selectedClass, (percent) => { miniFill.style.width = `${percent}%`; }); if (result.type === 'success') { icon.textContent = '✅'; detail.textContent = result.filename; el.classList.add('status-success'); miniFill.style.width = '100%'; miniFill.style.background = '#52c41a'; state.uploadResults.push({ file: item.file, id: item.id, previewUrl: item.previewUrl, status: 'success', filename: result.filename, }); } else if (result.type === 'failed') { icon.textContent = '❌'; detail.textContent = `上传失败:${result.reason}`; el.classList.add('status-failed'); miniFill.style.width = '100%'; miniFill.style.background = '#ff4d4f'; state.uploadResults.push({ file: item.file, id: item.id, previewUrl: item.previewUrl, status: 'failed', filename: item.file.name, reason: result.reason, }); } completed++; const percent = Math.round((completed / total) * 100); progressText.textContent = `上传中... ${completed} / ${total}`; progressPercent.textContent = `${percent}%`; progressFill.style.width = `${percent}%`; } // 上传完成 state.isUploading = false; btnUpload.textContent = '开始上传'; btnUpload.classList.remove('uploading'); setTimeout(() => { progressSection.style.display = 'none'; uploadSection.style.display = 'none'; showResults(); }, 500); } // ========== 结果展示 ========== function showResults() { resultSection.style.display = 'block'; const successList = state.uploadResults.filter(r => r.status === 'success'); const failedList = state.uploadResults.filter(r => r.status === 'failed'); let summaryHtml = `共上传 ${state.uploadResults.length} 张图片,`; summaryHtml += `成功 ${successList.length} 张`; if (failedList.length > 0) { summaryHtml += `,失败 ${failedList.length} 张`; } resultSummary.innerHTML = summaryHtml; btnRetry.style.display = failedList.length > 0 ? 'inline-block' : 'none'; resultGrid.innerHTML = ''; state.uploadResults.forEach(item => { const div = document.createElement('div'); div.className = `result-item ${item.status}`; div.innerHTML = ` ${item.filename}
${item.status === 'success' ? item.filename : item.filename}
${item.status === 'success' ? '✅' : '❌'} `; resultGrid.appendChild(div); }); } function retryFailed() { const failedItems = state.uploadResults.filter(r => r.status === 'failed'); state.selectedFiles = failedItems.map(item => ({ file: item.file, id: item.id, previewUrl: item.previewUrl, })); state.uploadResults = []; resultSection.style.display = 'none'; progressSection.style.display = 'none'; uploadSection.style.display = 'block'; renderPreview(); } function newUpload() { state.selectedFiles.forEach(f => URL.revokeObjectURL(f.previewUrl)); state.selectedFiles = []; state.uploadResults = []; state.isUploading = false; resultSection.style.display = 'none'; progressSection.style.display = 'none'; uploadSection.style.display = 'block'; previewSection.style.display = 'none'; selectArea.style.display = 'block'; btnUpload.disabled = true; btnUpload.textContent = '开始上传'; } // ========== 事件绑定 ========== function bindEvents() { campSelect.addEventListener('change', onCampChange); classSelect.addEventListener('change', onClassChange); fileInput.addEventListener('change', onFileSelect); btnUpload.addEventListener('click', startUpload); btnClearAll.addEventListener('click', clearAllFiles); btnRetry.addEventListener('click', retryFailed); btnNewUpload.addEventListener('click', newUpload); campSelect.addEventListener('change', () => { if (state.selectedFiles.length > 0) renderPreview(); }); classSelect.addEventListener('change', () => { if (state.selectedFiles.length > 0) renderPreview(); }); } // ========== 启动 ========== document.addEventListener('DOMContentLoaded', init);