/* ===== 文件柜前端逻辑 ===== */ (function () { 'use strict'; // ===== 状态 ===== let currentCode = ''; // 当前取件码输入 let selectedFiles = []; // 已选择的文件 // ===== DOM 元素 ===== const $ = (id) => document.getElementById(id); const dropZone = $('dropZone'); const fileInput = $('fileInput'); const selectBtn = $('selectBtn'); const uploadProgress = $('uploadProgress'); const progressList = $('progressList'); const uploadResult = $('uploadResult'); const resultList = $('resultList'); const codeDots = $('codeDots'); const codeClear = $('codeClear'); const codeDisplay = document.querySelector('.code-display'); const queryBtn = $('queryBtn'); const queryResult = $('queryResult'); const toast = $('toast'); // ===== 禁用扩展名(与后端一致)===== const FORBIDDEN_EXTS = [ '.exe', '.bat', '.cmd', '.vbs', '.ps1', '.jar', '.com', '.msi', '.scr', '.pif', '.dll', '.ocx', '.hta', '.wsf', '.cpl', '.run', '.app', '.deb', '.rpm' ]; const MAX_SIZE = 100 * 1024 * 1024; // 100MB // ===== Toast 提示 ===== let toastTimer = null; function showToast(msg, type = '') { toast.textContent = msg; toast.className = 'toast show ' + type; if (toastTimer) clearTimeout(toastTimer); toastTimer = setTimeout(() => { toast.className = 'toast'; }, 3000); } // ===== 格式化文件大小 ===== function formatSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB'; } // ===== 取件码输入处理 ===== function updateCodeDisplay() { if (currentCode.length === 0) { codeDots.textContent = '--------'; codeDots.classList.add('empty'); codeClear.classList.add('hidden'); } else { // 显示输入的数字 + 剩余位置用下划线 const display = currentCode + '-'.repeat(8 - currentCode.length); codeDots.textContent = display; codeDots.classList.remove('empty'); codeClear.classList.remove('hidden'); } queryBtn.disabled = currentCode.length !== 8; } function inputDigit(digit) { if (currentCode.length >= 8) return; currentCode += digit; updateCodeDisplay(); // 触发按键动画 const btn = document.querySelector(`.numpad-btn[data-key="${digit}"]`); if (btn) flashButton(btn); } function backspace() { currentCode = currentCode.slice(0, -1); updateCodeDisplay(); } function clearCode() { currentCode = ''; updateCodeDisplay(); queryResult.classList.add('hidden'); } function flashButton(btn) { btn.classList.add('pressed'); setTimeout(() => btn.classList.remove('pressed'), 120); } // ===== 小键盘事件 ===== document.querySelectorAll('.numpad-btn').forEach(btn => { btn.addEventListener('click', () => { const key = btn.dataset.key; if (key === 'clear') { clearCode(); } else if (key === 'back') { backspace(); flashButton(btn); } else { inputDigit(key); } }); }); // ===== 物理键盘事件 ===== document.addEventListener('keydown', (e) => { // 只处理数字键、退格、回车、Delete if (/^[0-9]$/.test(e.key)) { e.preventDefault(); inputDigit(e.key); } else if (e.key === 'Backspace') { e.preventDefault(); backspace(); } else if (e.key === 'Delete' || e.key === 'Escape') { e.preventDefault(); clearCode(); } else if (e.key === 'Enter') { e.preventDefault(); if (!queryBtn.disabled) doQuery(); } }); // ===== 粘贴监听:只提取 8 位数字 ===== document.addEventListener('paste', (e) => { // 如果焦点在上传区的 input 或 textarea,不拦截 const target = e.target; if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') && target.id !== 'hidden-code-input') { return; } const clipText = (e.clipboardData || window.clipboardData).getData('text'); if (!clipText) return; // 只保留数字 const digits = clipText.replace(/\D/g, ''); if (!digits) { showToast('剪贴板中没有数字内容', 'error'); return; } e.preventDefault(); // 清空再填入前 8 位 currentCode = digits.slice(0, 8); updateCodeDisplay(); showToast(`已粘贴 ${currentCode.length} 位数字`, 'success'); }); // ===== 取件码显示区点击 = 复制当前取件码 ===== codeDisplay.style.cursor = 'pointer'; codeDisplay.title = '点击复制当前取件码'; codeDisplay.addEventListener('click', () => { if (!currentCode) { showToast('还没有输入取件码', 'error'); return; } copyToClipboard(currentCode, '取件码已复制'); }); function copyToClipboard(text, toastMsg) { if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).then(() => showToast(toastMsg, 'success')); } else { // 兜底:hidden textarea + execCommand const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); showToast(toastMsg, 'success'); } catch { showToast('复制失败,请手动选择', 'error'); } document.body.removeChild(ta); } } // ===== 清空按钮 ===== codeClear.addEventListener('click', (e) => { e.stopPropagation(); clearCode(); }); // ===== 拖拽上传 ===== ['dragenter', 'dragover'].forEach(evt => { dropZone.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); dropZone.classList.add('dragover'); }); }); ['dragleave', 'drop'].forEach(evt => { dropZone.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); dropZone.classList.remove('dragover'); }); }); dropZone.addEventListener('drop', (e) => { const files = Array.from(e.dataTransfer.files); if (files.length) handleFiles(files); }); dropZone.addEventListener('click', () => fileInput.click()); selectBtn.addEventListener('click', (e) => { e.stopPropagation(); fileInput.click(); }); fileInput.addEventListener('change', (e) => { const files = Array.from(e.target.files); if (files.length) handleFiles(files); fileInput.value = ''; // 重置,允许重复选择同一文件 }); // ===== 文件校验 ===== function validateFile(file) { const ext = '.' + file.name.split('.').pop().toLowerCase(); if (FORBIDDEN_EXTS.includes(ext)) { return { ok: false, msg: `禁止上传 ${ext} 类型文件` }; } if (file.size > MAX_SIZE) { return { ok: false, msg: `文件过大(${formatSize(file.size)}),上限 ${formatSize(MAX_SIZE)}` }; } if (file.size === 0) { return { ok: false, msg: '文件为空' }; } return { ok: true }; } // ===== 上传处理 ===== function handleFiles(files) { const validFiles = []; const errors = []; files.forEach(f => { const v = validateFile(f); if (v.ok) validFiles.push(f); else errors.push(`${f.name}: ${v.msg}`); }); if (errors.length) { showToast(errors[0] + (errors.length > 1 ? `(另有 ${errors.length - 1} 个文件问题)` : ''), 'error'); } if (!validFiles.length) return; selectedFiles = validFiles; doUpload(); } // ===== 执行上传 ===== function doUpload() { uploadProgress.classList.remove('hidden'); uploadResult.classList.add('hidden'); progressList.innerHTML = ''; resultList.innerHTML = ''; const formData = new FormData(); selectedFiles.forEach(f => formData.append('files', f)); // 为每个文件创建进度条 const progressItems = selectedFiles.map((f, i) => { const div = document.createElement('div'); div.className = 'progress-item'; div.innerHTML = `
${f.name} 0%
`; progressList.appendChild(div); return div; }); const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/upload'); xhr.upload.onprogress = (e) => { if (!e.lengthComputable) return; // 近似估算(所有文件总进度) const totalPct = (e.loaded / e.total) * 100; // 将总进度分配给每个文件(简化:显示整体进度) progressItems.forEach((item, i) => { const pct = Math.min(100, totalPct + i * 0); // 简化处理 item.querySelector('.progress-fill').style.width = pct + '%'; item.querySelector('[data-status]').textContent = Math.round(pct) + '%'; }); }; xhr.onload = () => { progressItems.forEach(item => { item.querySelector('[data-status]').textContent = '完成'; item.querySelector('.progress-fill').style.width = '100%'; }); let resp; try { resp = JSON.parse(xhr.responseText); } catch { showToast('服务器返回异常', 'error'); return; } if (xhr.status === 200) { showToast(resp.message || '上传完成', resp.success ? 'success' : ''); renderUploadResult(resp.data || []); } else { showToast(resp.message || `上传失败 (${xhr.status})`, 'error'); } uploadProgress.classList.add('hidden'); uploadResult.classList.remove('hidden'); }; xhr.onerror = () => { showToast('网络错误,上传失败', 'error'); uploadProgress.classList.add('hidden'); }; xhr.send(formData); } // ===== 渲染上传结果 ===== function renderUploadResult(items) { resultList.innerHTML = ''; items.forEach(item => { const wrapper = document.createElement('div'); if (item.success) { wrapper.className = 'result-card success fade-in'; wrapper.innerHTML = `
✓ ${item.original_name}
您的取件码
${item.code}
到期时间:${item.expire_at}
`; // 绑定复制按钮 const btn = wrapper.querySelector('.copy-btn'); btn.addEventListener('click', () => { copyToClipboard(item.code, '取件码已复制'); }); // 取件码数字本身也可点击复制 wrapper.querySelector('.code-value').style.cursor = 'pointer'; wrapper.querySelector('.code-value').title = '点击复制'; wrapper.querySelector('.code-value').addEventListener('click', () => { copyToClipboard(item.code, '取件码已复制'); }); } else { wrapper.className = 'result-card error fade-in'; wrapper.innerHTML = `
✗ ${item.original_name}
${item.message}
`; } resultList.appendChild(wrapper); }); } // ===== 查询按钮 ===== queryBtn.addEventListener('click', doQuery); async function doQuery() { if (currentCode.length !== 8) return; queryResult.classList.add('hidden'); queryResult.innerHTML = `
查询中...
`; queryResult.classList.remove('hidden'); try { const resp = await fetch('/api/query', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code: currentCode }) }); const data = await resp.json(); if (data.success) { renderQueryResult(data.data); } else { renderQueryError(data.message); } } catch (e) { renderQueryError('网络错误,请稍后重试'); } } // ===== 渲染查询结果 ===== function renderQueryResult(file) { const size = formatSize(file.file_size); queryResult.innerHTML = `

${file.original_name}

📏 ${size}
📤 上传:${file.uploaded_at}
⏰ 到期:${file.expire_at}
⬇️ 已下载 ${file.download_count} 次
点击下载
`; } function renderQueryError(msg) { queryResult.innerHTML = `
查询失败
${msg}
`; } // ===== 页面加载完成 ===== document.addEventListener('DOMContentLoaded', () => { updateCodeDisplay(); }); })();