// DOM Elements const elements = { uploadArea: document.getElementById('uploadArea'), fileInput: document.getElementById('fileInput'), loadingOverlay: document.getElementById('loadingOverlay'), loadingText: document.getElementById('loadingText'), noData: document.getElementById('noData'), dataDisplay: document.getElementById('dataDisplay'), uploadModal: document.getElementById('uploadModal'), fileList: document.getElementById('fileList'), fileSelector: document.getElementById('fileSelector'), dailyData: document.getElementById('dailyData'), searchInput: document.getElementById('searchInput'), // Stats totalAmount: document.getElementById('totalAmount'), totalQuantity: document.getElementById('totalQuantity'), totalDays: document.getElementById('totalDays'), totalProducts: document.getElementById('totalProducts') }; // State let state = { allData: null, filteredData: null, currentFilter: 'all', searchTerm: '', currentFile: null, fileList: [], currentPage: 1, itemsPerPage: 50 }; // --- Event Listeners --- document.addEventListener('DOMContentLoaded', () => { loadFileList(); if (elements.searchInput) { elements.searchInput.addEventListener('input', (e) => { state.searchTerm = e.target.value.toLowerCase(); applyFilters(); }); } }); if (elements.fileInput) { elements.fileInput.addEventListener('change', (e) => handleFile(e.target.files[0])); } if (elements.uploadArea) { elements.uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); elements.uploadArea.classList.add('active'); }); elements.uploadArea.addEventListener('dragleave', () => { elements.uploadArea.classList.remove('active'); }); elements.uploadArea.addEventListener('drop', (e) => { e.preventDefault(); elements.uploadArea.classList.remove('active'); if (e.dataTransfer.files.length > 0) { handleFile(e.dataTransfer.files[0]); } }); elements.uploadArea.addEventListener('click', () => { elements.fileInput.click(); }); } // --- Core Functions --- function handleFile(file) { if (!file) return; if (!file.name.match(/\.(xlsx|xls)$/i)) { alert('请选择 Excel 文件 (.xlsx 或 .xls)'); return; } closeUploadModal(); uploadFile(file); } function uploadFile(file) { const formData = new FormData(); formData.append('file', file); showLoading(true, '正在上传并分析...'); fetch('/upload', { method: 'POST', body: formData }) .then(res => res.json()) .then(data => { setTimeout(() => { showLoading(false); if (data.success) { state.currentFile = data.filename; displayData(data.data); loadFileList(); } else { alert(data.error || '上传失败'); } }, 500); }) .catch(err => { showLoading(false); alert('上传错误: ' + err.message); }); } function loadFileList() { fetch('/files') .then(res => res.json()) .then(data => { if (data.files && data.files.length > 0) { state.fileList = data.files; renderFileList(data.files); if (!state.currentFile) { loadFile(data.files[0].filename); } } else { elements.fileSelector.style.display = 'none'; } }); } function loadFile(filename) { showLoading(true, '加载数据中...'); fetch(`/load/${filename}`) .then(res => res.json()) .then(data => { setTimeout(() => { showLoading(false); if (data.success) { state.currentFile = filename; displayData(data.data); renderFileList(state.fileList); // Update active state } else { elements.noData.style.display = 'flex'; elements.dataDisplay.style.display = 'none'; } }, 300); }) .catch(() => { showLoading(false); elements.noData.style.display = 'flex'; }); } function displayData(data) { state.allData = data; elements.noData.style.display = 'none'; elements.dataDisplay.style.display = 'block'; applyFilters(); } function renderStats(data) { let totalQty = 0; let totalAmt = 0; const days = data.daily_summary.length; const uniqueProducts = new Set(); // Simplified unique product count logic based on daily summary data structure might differ, here we approximate or iterate raw if needed. // Correction: Use raw_data for unique products count if available, or iterate all dailies. data.daily_summary.forEach(day => { if (day.summary_info) { totalQty += day.summary_info.total_quantity; totalAmt += day.summary_info.total_amount; } else { totalQty += day.total_quantity; totalAmt += day.total_amount; } day.products.forEach(p => uniqueProducts.add(p.product)); }); // Count Animation animateValue(elements.totalAmount, totalAmt, '¥'); animateValue(elements.totalQuantity, totalQty, ''); animateValue(elements.totalDays, days, ''); animateValue(elements.totalProducts, uniqueProducts.size, ''); } function animateValue(obj, end, prefix = '') { if (!obj) return; let startTimestamp = null; const duration = 1000; const start = 0; const step = (timestamp) => { if (!startTimestamp) startTimestamp = timestamp; const progress = Math.min((timestamp - startTimestamp) / duration, 1); const value = Math.floor(progress * (end - start) + start); if (prefix === '¥') { obj.innerHTML = prefix + (progress * end).toFixed(2); } else { obj.innerHTML = prefix + value; } if (progress < 1) { window.requestAnimationFrame(step); } else { if (prefix === '¥') { obj.innerHTML = prefix + end.toFixed(2); } else { obj.innerHTML = prefix + end; } } }; window.requestAnimationFrame(step); } function renderDailyList(data) { elements.dailyData.innerHTML = data.daily_summary.map(day => { const totalAmt = day.summary_info ? day.summary_info.total_amount : day.total_amount; const totalQty = day.summary_info ? day.summary_info.total_quantity : day.total_quantity; return `
${day.date} ¥${totalAmt.toFixed(2)} / ${totalQty}件
${day.products.map(p => `
${p.product}
¥${p.price.toFixed(2)} x${p.quantity} ¥${p.amount.toFixed(2)}
`).join('')}
`; }).join(''); } function toggleDayDetails(header) { const card = header.parentElement; card.classList.toggle('expanded'); } function applyFilters() { if (!state.allData) return; let filtered = JSON.parse(JSON.stringify(state.allData)); // Search Filter if (state.searchTerm) { filtered.daily_summary = filtered.daily_summary.map(day => { const matchedProducts = day.products.filter(p => p.product.toLowerCase().includes(state.searchTerm)); if (matchedProducts.length > 0) { const tQty = matchedProducts.reduce((a, b) => a + b.quantity, 0); const tAmt = matchedProducts.reduce((a, b) => a + b.amount, 0); return { ...day, products: matchedProducts, total_quantity: tQty, total_amount: tAmt, summary_info: null }; } return null; }).filter(d => d); } // Amount Filter if (state.currentFilter !== 'all') { filtered.daily_summary = filtered.daily_summary.filter(day => { const amt = day.summary_info ? day.summary_info.total_amount : day.total_amount; if (state.currentFilter === 'high') return amt >= 1000; if (state.currentFilter === 'medium') return amt >= 100 && amt < 1000; if (state.currentFilter === 'low') return amt < 100; return true; }); } state.filteredData = filtered; renderStats(filtered); renderDailyList(filtered); } function filterByAmount(type) { state.currentFilter = type; document.querySelectorAll('.filter-chip').forEach(btn => btn.classList.remove('active')); event.target.classList.add('active'); applyFilters(); } function renderFileList(files) { elements.fileSelector.style.display = 'flex'; elements.fileList.innerHTML = files.map(f => `
${f.original_name}
`).join(''); } // --- Utils --- function openUploadModal() { elements.uploadModal.classList.add('active'); } function closeUploadModal() { elements.uploadModal.classList.remove('active'); } function showLoading(show, text) { elements.loadingOverlay.style.display = show ? 'flex' : 'none'; if (text) elements.loadingText.textContent = text; } function cleanupFiles() { if (!confirm('确认清理旧文件?')) return; fetch('/cleanup', { method: 'POST' }) .then(res => res.json()) .then(data => { alert(data.message); loadFileList(); if (data.deleted_count > 0 && state.currentFile) { // Check if current file still exists? Simple approach: reload list // Logic omitted for brevity, user can re-upload } }); } function exportData() { if (!state.allData) return; const data = state.filteredData || state.allData; let csv = "Date,Product,Quantity,Price,Amount\n"; data.daily_summary.forEach(day => { day.products.forEach(p => { csv += `${day.date},${p.product},${p.quantity},${p.price},${p.amount}\n`; }); }); const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'sales_data.csv'; a.click(); }