document.addEventListener('DOMContentLoaded', () => { // --- Global Variables --- const uploadArea = document.getElementById('upload-zone'); const fileInput = document.getElementById('file-picker'); const progressArea = document.getElementById('prog-zone'); const doneArea = document.getElementById('done-zone'); const searchInput = document.getElementById('file-search'); // --- Copy Link Delegation --- document.addEventListener('click', (e) => { const btn = e.target.closest('.copy-link-btn'); if (!btn) return; // Prevent default if it's a link (though it's a button) e.preventDefault(); e.stopPropagation(); const item = btn.closest('.file-item, .image-card'); if (!item) return; // Should exist // 如果按钮上有 onclick 属性(旧代码或特殊情况),优先执行 onclick,这里不处理 if (btn.hasAttribute('onclick')) return; const shortId = item.dataset.shortId; const fileId = item.dataset.fileId; const filename = item.dataset.filename; // 核心策略:优先从 DOM 中获取真实可用的绝对 URL let url = ''; // 1. 尝试获取下载按钮的链接 (文件列表模式) // 查找 href 以 /d/ 开头的 a 标签 const downloadLink = item.querySelector('a[href^="/d/"]'); if (downloadLink && downloadLink.href) { url = downloadLink.href; } // 2. 尝试获取图片的 src (图床模式) if (!url) { const img = item.querySelector('img[src^="/d/"]'); if (img && img.src) { url = img.src; } } // 3. Fallback: 使用 dataset 中的 fileUrl (如果存在且非空且不是 undefined 字符串) if (!url) { const dsUrl = item.dataset.fileUrl; if (dsUrl && dsUrl !== 'undefined') { url = dsUrl; // 确保是绝对路径 if (url.startsWith('/')) { url = window.location.origin + url; } } } // 4. Final Fallback: 构造 /d/{id} if (!url || url.includes('undefined')) { const id = (shortId && shortId !== 'None' && shortId !== '') ? shortId : fileId; url = window.location.origin + `/d/${id}`; } // 安全检查:如果最终结果包含 undefined,强制重构 if (url.includes('undefined')) { // 最后的兜底,哪怕 fileId 也是 undefined (极低概率),也比 http://...undefined 好 console.warn('Constructed URL contained undefined, falling back to raw fileId'); url = window.location.origin + '/d/' + (fileId || 'error'); } if (window.copyLink) { Utils.copy(url); } else { Utils.copy(url); } }); // --- Search Functionality --- if (searchInput) { searchInput.addEventListener('input', (e) => { const term = e.target.value.toLowerCase(); // Select both file list items and image grid cards const items = document.querySelectorAll('.file-item, .image-card'); items.forEach(item => { const name = (item.dataset.filename || '').toLowerCase(); if (name.includes(term)) { item.style.display = ''; // Reset to default (grid or flex) } else { item.style.display = 'none'; } }); }); } // --- Upload Logic --- if (uploadArea && fileInput) { // Prevent double dialog by stopping propagation from input fileInput.addEventListener('click', (e) => e.stopPropagation()); uploadArea.addEventListener('click', (e) => { // Only trigger if not clicking the input itself (though propagation stop handles it, this is extra safety) if (e.target !== fileInput) { fileInput.click(); } }); uploadArea.addEventListener('dragover', (event) => { event.preventDefault(); uploadArea.style.borderColor = 'var(--primary-color)'; uploadArea.style.backgroundColor = 'var(--bg-surface-hover)'; }); uploadArea.addEventListener('dragleave', () => { uploadArea.style.borderColor = ''; uploadArea.style.backgroundColor = ''; }); uploadArea.addEventListener('drop', (event) => { event.preventDefault(); uploadArea.style.borderColor = ''; uploadArea.style.backgroundColor = ''; const files = event.dataTransfer.files; if (files.length > 0) { handleFiles(files); } }); fileInput.addEventListener('change', ({ target }) => { if (target.files.length > 0) { handleFiles(target.files); } }); } // Queue system for uploads const uploadQueue = []; let isUploading = false; function handleFiles(files) { if (progressArea) progressArea.innerHTML = ''; for (const file of files) { uploadQueue.push(file); } processQueue(); } function processQueue() { if (isUploading || uploadQueue.length === 0) return; isUploading = true; const file = uploadQueue.shift(); uploadFile(file).then(() => { isUploading = false; processQueue(); }); } function uploadFile(file) { return new Promise((resolve) => { const formData = new FormData(); formData.append('file', file, file.name); const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/upload', true); const fileId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`; // Initial Progress UI // 使用新版 UI 风格 const progressHTML = `
${file.name} 0%
`; if (progressArea) progressArea.insertAdjacentHTML('beforeend', progressHTML); const progressEl = document.querySelector(`#progress-${fileId} .progress-bar`); const percentEl = document.querySelector(`#progress-${fileId} .percent`); xhr.upload.onprogress = ({ loaded, total }) => { const percent = Math.floor((loaded / total) * 100); if (progressEl) progressEl.style.width = `${percent}%`; if (percentEl) percentEl.textContent = `${percent}%`; }; xhr.onload = () => { const progressRow = document.getElementById(`progress-${fileId}`); if (progressRow) progressRow.remove(); if (xhr.status === 200) { const response = JSON.parse(xhr.responseText); const fileUrl = response.url; // Success Toast if (window.Toast) Toast.show(`${file.name} 上传成功`); // Add to done area const successHTML = `
${file.name}
${fileUrl}
`; if (doneArea) doneArea.insertAdjacentHTML('afterbegin', successHTML); } else { let errorMsg = "上传失败"; try { const parsed = JSON.parse(xhr.responseText); const detail = parsed && parsed.detail; if (typeof detail === 'string') { errorMsg = detail; } else if (detail && typeof detail === 'object') { errorMsg = detail.message || errorMsg; } else if (parsed && parsed.message) { errorMsg = parsed.message; } } catch (e) {} if (window.Toast) Toast.show(errorMsg, 'error'); } resolve(); }; xhr.onerror = () => { const progressRow = document.getElementById(`progress-${fileId}`); if (progressRow) progressRow.remove(); if (window.Toast) Toast.show('网络错误', 'error'); resolve(); }; xhr.send(formData); }); } // --- Batch Actions --- const selectAllCheckbox = document.getElementById('select-all-checkbox'); const batchDeleteBtn = document.getElementById('batch-delete-btn'); const copyLinksBtn = document.getElementById('copy-links-btn'); const selectionCounter = document.getElementById('selection-counter'); const batchActionsBar = document.getElementById('batch-actions-bar'); const formatOptions = document.querySelectorAll('.format-option'); function updateBatchControls() { const checkboxes = document.querySelectorAll('.file-checkbox'); const checked = document.querySelectorAll('.file-checkbox:checked'); const count = checked.length; if (selectionCounter) selectionCounter.textContent = count > 0 ? `${count} 项已选` : '0 项已选'; if (batchActionsBar) { if (count > 0) { batchActionsBar.classList.remove('hidden'); } else { batchActionsBar.classList.add('hidden'); } } if (selectAllCheckbox) selectAllCheckbox.checked = (count > 0 && count === checkboxes.length); } if (selectAllCheckbox) { selectAllCheckbox.addEventListener('change', (e) => { document.querySelectorAll('.file-checkbox').forEach(cb => { cb.checked = e.target.checked; }); updateBatchControls(); }); } // Delegation for dynamic checkboxes document.addEventListener('change', (e) => { if (e.target.classList.contains('file-checkbox')) { updateBatchControls(); } }); // Format selection (Image Hosting) if (formatOptions) { formatOptions.forEach(opt => { opt.addEventListener('click', () => { formatOptions.forEach(o => o.classList.remove('active')); opt.classList.add('active'); }); }); } // Batch Copy if (copyLinksBtn) { copyLinksBtn.addEventListener('click', () => { const checked = document.querySelectorAll('.file-checkbox:checked'); if (checked.length === 0) return; const activeFormatBtn = document.querySelector('.format-option.active'); const format = activeFormatBtn ? activeFormatBtn.dataset.format : 'url'; const links = Array.from(checked).map(cb => { const item = cb.closest('.file-item, .image-card'); let url = ''; // 1. 尝试获取下载按钮 const downloadLink = item.querySelector('a[href^="/d/"]'); if (downloadLink && downloadLink.href) { url = downloadLink.href; } // 2. 尝试获取图片 src if (!url) { const img = item.querySelector('img[src^="/d/"]'); if (img && img.src) { url = img.src; } } // 3. Fallback: Dataset if (!url) { const dsUrl = item.dataset.fileUrl; if (dsUrl && dsUrl !== 'undefined') { url = dsUrl; if (url.startsWith('/')) { url = window.location.origin + url; } } } // 4. Final Fallback if (!url || url.includes('undefined')) { const shortId = item.dataset.shortId; const fileId = item.dataset.fileId; const id = (shortId && shortId !== 'None' && shortId !== '') ? shortId : fileId; url = window.location.origin + `/d/${id}`; } const name = item.dataset.filename; if (format === 'markdown') return `![${name}](${url})`; if (format === 'html') return `${name}`; return url; }); Utils.copy(links.join('\n')); }); } // Batch Delete if (batchDeleteBtn) { batchDeleteBtn.addEventListener('click', async () => { const checked = document.querySelectorAll('.file-checkbox:checked'); if (checked.length === 0) return; const confirmed = await Modal.confirm('批量删除', `确定要删除选中的 ${checked.length} 个文件吗?`); if (!confirmed) return; const fileIds = Array.from(checked).map(cb => cb.dataset.fileId); fetch('/api/batch_delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file_ids: fileIds }) }) .then(res => res.json()) .then(data => { if (data.deleted) { data.deleted.forEach(item => { const id = item.details?.file_id || item; removeFileElement(id); }); if (window.Toast) Toast.show(`已删除 ${data.deleted.length} 个文件`); } updateBatchControls(); }); }); } // --- SSE & Realtime Updates --- const fileListContainer = document.getElementById('file-list-disk'); if (fileListContainer) { let eventSource = null; const connectSSE = () => { if (eventSource) { eventSource.close(); } eventSource = new EventSource('/api/file-updates'); eventSource.onmessage = (event) => { const msg = JSON.parse(event.data); const action = msg && msg.action ? msg.action : 'add'; if (action === 'delete') { removeFileElement(msg.file_id); updateBatchControls(); return; } addNewFileElement(msg); }; eventSource.onerror = () => { try { eventSource.close(); } catch (_) {} setTimeout(connectSSE, 5000); }; }; connectSSE(); } function formatDateValue(value) { if (!value) return ''; const d = new Date(value); if (!isNaN(d.getTime())) return d.toISOString().split('T')[0]; const s = String(value); return s.split(' ')[0].split('T')[0]; } function addNewFileElement(file) { const isGridView = document.querySelector('.image-grid') !== null; const container = document.getElementById('file-list-disk'); // Remove empty state if exists const emptyState = container.querySelector('div[style*="text-align: center"]'); if (emptyState) emptyState.remove(); const formattedSize = (file.filesize / (1024 * 1024)).toFixed(2) + " MB"; const formattedDate = formatDateValue(file.upload_date); const safeId = file.file_id.replace(':', '-'); // URL construction: Always use /d/{file_id} (short_id preferred) // 回滚:只使用 /d/{id} 格式,不再拼接文件名或 slug let fileUrl = `/d/${file.short_id || file.file_id}`; let html = ''; if (isGridView) { html = `
${file.filename}
${file.filename}
${formattedSize}
`; } else { html = `
${file.filename}
${formattedSize} ${formattedDate}
`; } container.insertAdjacentHTML('afterbegin', html); } // --- Global Helpers --- window.deleteFile = async (fileId) => { const confirmed = await Modal.confirm('删除文件', '确定要删除此文件吗?'); if (!confirmed) return; fetch(`/api/files/${fileId}`, { method: 'DELETE' }) .then(async (res) => { let data = null; try { data = await res.json(); } catch (e) {} return { ok: res.ok, data }; }) .then(({ ok, data }) => { if (ok && data && data.status === 'ok') { removeFileElement(fileId); if (window.Toast) Toast.show('文件已删除'); updateBatchControls(); } else { const msg = data?.detail?.message || data?.message || '删除失败'; if (window.Toast) Toast.show(msg, 'error'); } }); }; function removeFileElement(fileId) { const el = document.getElementById(`file-item-${fileId.replace(':', '-')}`); if (el) el.remove(); // Check if empty const container = document.getElementById('file-list-disk'); if (container && container.children.length === 0) { // Re-render empty state logic if needed, or let user refresh // Simple text fallback const isGridView = document.querySelector('.image-grid') !== null; if (isGridView) { container.innerHTML = `

暂无图片

`; } else { container.innerHTML = `
暂无文件
`; } } } });