Files
2026-04-18 15:08:57 +08:00

414 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ========== 写死的选项数据 ==========
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 = '<option value="">-- 请选择训练营 --</option>';
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 = '<option value="">-- 请先选择训练营 --</option>';
return;
}
classSelect.disabled = false;
classSelect.innerHTML = '<option value="">-- 请选择班级 --</option>';
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 = `
<img src="${item.previewUrl}" alt="${item.file.name}">
<button class="remove-btn" onclick="removeFile('${item.id}')">×</button>
<div class="file-name">${item.file.name}</div>
`;
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 = `
<span class="status-icon">⏳</span>
<div class="file-info">
<div class="name">${item.file.name}</div>
<div class="detail">等待上传...</div>
</div>
<div class="mini-progress"><div class="mini-fill" style="width:0%"></div></div>
`;
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 = `共上传 <strong>${state.uploadResults.length}</strong> 张图片,`;
summaryHtml += `<span class="success-count">成功 ${successList.length} 张</span>`;
if (failedList.length > 0) {
summaryHtml += `<span class="fail-count">失败 ${failedList.length} 张</span>`;
}
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 = `
<img src="${item.previewUrl}" alt="${item.filename}">
<div class="result-name">${item.status === 'success' ? item.filename : item.filename}</div>
<span class="result-status">${item.status === 'success' ? '✅' : '❌'}</span>
`;
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);