// 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; // 1. Calculate Unique Days (based on YYYY-MM-DD) const uniqueDates = new Set(); // 2. Track Unique Products const uniqueProducts = new Set(); data.daily_summary.forEach(day => { // Extract YYYY-MM-DD from timestamp "YYYY-MM-DD HH:MM:SS" if (day.date && day.date.length >= 10) { uniqueDates.add(day.date.substring(0, 10)); } // Sum up totals // Priority: Use the sum of products (day.total_...) if available. // If products are missing/zero but header info (summary_info) exists, use that. if (day.products && day.products.length > 0) { // Use calculated sum from products totalQty += day.total_quantity; totalAmt += day.total_amount; } else if (day.summary_info) { // Fallback to header info if no products parsed totalQty += day.summary_info.total_quantity; totalAmt += day.summary_info.total_amount; } else { totalQty += day.total_quantity; totalAmt += day.total_amount; } // Collect unique products if (day.products) { day.products.forEach(p => uniqueProducts.add(p.product)); } }); // Count Animation animateValue(elements.totalAmount, totalAmt, '¥'); animateValue(elements.totalQuantity, totalQty, ''); animateValue(elements.totalDays, uniqueDates.size, ''); // Use unique dates count 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); } // Pagination State const ITEMS_PER_PAGE = 20; let displayedItems = ITEMS_PER_PAGE; function renderDailyList(data) { const listContainer = elements.dailyData; const totalItems = data.daily_summary.length; // Slice data based on current limit const visibleData = data.daily_summary.slice(0, displayedItems); // Render list const listHTML = visibleData.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 `
`; }).join(''); // Append "Show More" button if needed let buttonHTML = ''; if (displayedItems < totalItems) { buttonHTML = `