document.addEventListener('DOMContentLoaded', function() { // --- Global Variables & State --- let jwtToken = localStorage.getItem('jwtToken'); // Load token from storage let currentUserId = null; // Will be set after login or token validation let milkDataCache = []; // Cache loaded milk data // --- DOM Elements --- const addMilkBtn = document.getElementById('add-milk-btn'); const recordConsumptionBtn = document.getElementById('record-consumption-btn'); const settingsBtn = document.getElementById('settings-btn'); const actionsContainer = document.querySelector('.actions'); const backToDashboardBtn = document.getElementById('back-to-dashboard-btn'); const logoutBtn = document.getElementById('logout-btn'); const addMilkModal = document.getElementById('add-milk-modal'); const recordConsumptionModal = document.getElementById('record-consumption-modal'); const authModal = document.getElementById('auth-modal'); // Auth modal const closeAddModalBtn = document.getElementById('close-add-modal'); const closeRecordModalBtn = document.getElementById('close-record-modal'); const closeAuthModalBtn = document.getElementById('close-auth-modal'); // Auth close const dashboardPage = document.getElementById('dashboard'); const settingsPage = document.getElementById('settings-page'); const addMilkForm = document.getElementById('add-milk-form'); const recordConsumptionForm = document.getElementById('record-consumption-form'); const settingsForm = document.getElementById('settings-form'); const authForm = document.getElementById('auth-form'); // Auth form const loginBtn = document.getElementById('login-btn'); // Login button const registerBtn = document.getElementById('register-btn'); // Register button const milkListContainer = document.getElementById('milk-list'); const noMilkMessage = document.getElementById('no-milk-message'); const consumeDateInput = document.getElementById('consume-date'); const authStatusDiv = document.getElementById('auth-status'); // Auth status display const authErrorP = document.getElementById('auth-error'); // Auth error display // --- API Helper --- async function fetchApi(endpoint, options = {}) { const headers = { 'Content-Type': 'application/json', ...options.headers, // Allow overriding headers }; // Add JWT token if available if (jwtToken) { headers['Authorization'] = `Bearer ${jwtToken}`; } const config = { ...options, headers: headers, }; // Construct full API URL (assuming API is served from the same origin) const url = `/api${endpoint}`; // Prepend /api try { const response = await fetch(url, config); // Handle unauthorized errors (e.g., expired token) if (response.status === 401 || response.status === 422) { // 422 Unprocessable Entity (often used by flask-jwt for missing/bad token) console.error('Authentication error:', response.status); handleLogout(true); // Force logout and show login modal throw new Error('Authentication Required'); // Stop further processing } // Get response body (if any) const responseData = response.headers.get('Content-Type')?.includes('application/json') ? await response.json() // Parse JSON if applicable : null; // Handle non-JSON responses if necessary if (!response.ok) { // Try to get error message from response body const errorMessage = responseData?.error || responseData?.message || `HTTP error ${response.status}`; console.error(`API Error (${response.status}) on ${endpoint}:`, errorMessage, responseData); throw new Error(errorMessage); } console.log(`API Success on ${endpoint}: Status ${response.status}`); return responseData; // Return JSON data or null } catch (error) { console.error(`Workspace failed for ${endpoint}:`, error); // Display error to user? (e.g., a general banner) displayGlobalError(`请求失败: ${error.message}`); throw error; // Re-throw error so calling function knows it failed } } // --- Authentication Functions --- function showLoginModal() { clearAuthForm(); authErrorP.style.display = 'none'; openModal(authModal); } function clearAuthForm() { const usernameInput = document.getElementById('auth-username'); const passwordInput = document.getElementById('auth-password'); if (usernameInput) usernameInput.value = ''; if (passwordInput) passwordInput.value = ''; } async function handleLogin(event) { event?.preventDefault(); // Prevent default if called from button click const username = document.getElementById('auth-username').value; const password = document.getElementById('auth-password').value; authErrorP.style.display = 'none'; if (!username || !password) { displayAuthError('请输入用户名和密码'); return; } try { const data = await fetchApi('/auth/login', { method: 'POST', body: JSON.stringify({ username, password }), }); if (data && data.access_token) { jwtToken = data.access_token; localStorage.setItem('jwtToken', jwtToken); console.log('Login successful'); closeModal(authModal); initializeApp(); // Reload data and update UI for logged-in user } else { displayAuthError('登录失败,请检查凭证'); } } catch (error) { // Error display handled by fetchApi or display specific message displayAuthError(`登录失败: ${error.message}`); } } async function handleRegister(event) { event?.preventDefault(); const username = document.getElementById('auth-username').value; const password = document.getElementById('auth-password').value; authErrorP.style.display = 'none'; if (!username || !password) { displayAuthError('请输入用户名和密码'); return; } if (password.length < 4) { // Example basic validation displayAuthError('密码长度至少需要4位'); return; } try { await fetchApi('/auth/register', { method: 'POST', body: JSON.stringify({ username, password }), }); // Registration successful, now attempt login alert('注册成功!请现在登录。'); // Maybe prefill username? document.getElementById('auth-password').value = ''; // Clear password // Or automatically log in: await handleLogin(); } catch (error) { displayAuthError(`注册失败: ${error.message}`); } } function handleLogout(showLogin = false) { jwtToken = null; currentUserId = null; localStorage.removeItem('jwtToken'); console.log('Logged out'); // Reset UI milkListContainer.innerHTML = ''; noMilkMessage.textContent = '请先登录以查看或添加牛奶记录。'; noMilkMessage.style.display = 'block'; updateOverview('-- L / -- 盒', '--', '--'); updateAuthStatus(); document.getElementById('settings-form').reset(); // Clear settings form hideMainContent(); // Hide main buttons/content if logged out if (showLogin) { showLoginModal(); } } function updateAuthStatus() { if (jwtToken) { // Try to get username if needed (decode token client-side - simple way) // Or make a quick /api/user/profile call, but might fail if token JUST expired let username = '已登录'; // Placeholder try { // Basic decoding (assumes standard JWT structure) - NOT FOR VERIFICATION const payload = JSON.parse(atob(jwtToken.split('.')[1])); currentUserId = payload.sub; // 'sub' usually holds the user ID/identity // Fetch username from profile API if needed for display username = `用户 ${currentUserId}`; // Placeholder until profile loaded fetchApi('/user/profile').then(profile => { if(profile && profile.username){ authStatusDiv.textContent = `已登录: ${profile.username}`; } }).catch(() => {}); // Ignore errors here, keep placeholder } catch (e) { console.error("Error decoding token:", e); } authStatusDiv.textContent = username; logoutBtn.style.display = 'inline-block'; showMainContent(); } else { authStatusDiv.textContent = '未登录'; logoutBtn.style.display = 'none'; hideMainContent(); // Optionally show login modal immediately if no token on load // showLoginModal(); } } function hideMainContent(){ // Hide elements that require login if(actionsContainer) { actionsContainer.style.display = 'none'; // <-- 修改这里,隐藏容器 } // (下面几行也可以删掉) // if(addMilkBtn) addMilkBtn.style.display = 'none'; // if(recordConsumptionBtn) recordConsumptionBtn.style.display = 'none'; // if(settingsBtn) settingsBtn.style.display = 'none'; // Could also hide the summary container etc. } function showMainContent(){ // Show elements after login if(actionsContainer) { actionsContainer.style.display = 'flex'; // <-- 修改这里,显示容器 (用 flex 因为 CSS 里是 flex 布局) } // if(addMilkBtn) addMilkBtn.style.display = 'inline-block'; // if(recordConsumptionBtn) recordConsumptionBtn.style.display = 'inline-block'; // if(settingsBtn) settingsBtn.style.display = 'inline-block'; } function displayAuthError(message) { authErrorP.textContent = message; authErrorP.style.display = 'block'; } function displayGlobalError(message){ // Implement a more robust global error display if needed console.error("Global Error:", message); alert(`操作失败: ${message}`); // Simple alert for now } // --- Core Data Loading & Rendering --- async function loadMilkData() { console.log("Fetching milk data and overview..."); if (!jwtToken) { console.log("No token found, skipping data load."); noMilkMessage.textContent = '请先登录以查看或添加牛奶记录。'; noMilkMessage.style.display = 'block'; milkListContainer.innerHTML = ''; updateOverview('-- L / -- 盒', '--', '--'); return; } try { // Fetch overview and milk list in parallel const [overviewData, milkBatches] = await Promise.all([ fetchApi('/overview'), fetchApi('/milk?onlyActive=false') // Get all non-deleted batches initially ]); milkDataCache = milkBatches || []; // Store fetched data // Update Overview Section if (overviewData) { const nearest = overviewData.nearestExpiry; const nearestStr = nearest ? `${nearest.note || '牛奶'} - ${nearest.expiryDate} (${nearest.daysRemaining}天)` : '无临期牛奶'; updateOverview( `${overviewData.totalQuantityLiters} L / ${overviewData.totalQuantityItemsApprox} 盒`, nearestStr, overviewData.estimatedFinishDate || '--' ); } else { updateOverview('-- L / -- 盒', '--', '--'); } // Render Milk List milkListContainer.innerHTML = ''; // Clear existing list if (milkBatches && milkBatches.length > 0) { noMilkMessage.style.display = 'none'; milkBatches.sort((a, b) => new Date(a.expiryDate) - new Date(b.expiryDate)); // Ensure sorted milkBatches.forEach(renderMilkCard); populateConsumeDropdown(milkBatches.filter(b => b.remainingVolume > 0.001 && b.status !== 'expired')); // Populate with active, non-expired } else { noMilkMessage.textContent = '还没有添加牛奶记录哦。'; noMilkMessage.style.display = 'block'; populateConsumeDropdown([]); // Clear dropdown } } catch (error) { console.error("Failed to load initial data:", error); noMilkMessage.textContent = '加载牛奶数据失败,请稍后重试。'; noMilkMessage.style.display = 'block'; milkListContainer.innerHTML = ''; updateOverview('错误', '错误', '错误'); // Error should have been handled by fetchApi, maybe logout occurred } } function renderMilkCard(milk) { const card = document.createElement('article'); card.className = `milk-card ${milk.status || 'normal'}`; // 'normal', 'warning', 'expired' card.dataset.id = milk.id; const progressValue = milk.initialTotalVolume > 0 ? (milk.remainingVolume / milk.initialTotalVolume) * 100 : 0; let expiryText; if (milk.daysRemaining === undefined || milk.daysRemaining === null){ expiryText = `过期: ${milk.expiryDate}`; } else if (milk.daysRemaining < 0){ expiryText = `已过期 ${Math.abs(milk.daysRemaining)} 天 (${milk.expiryDate})`; } else if (milk.daysRemaining === 0){ expiryText = `今天过期 (${milk.expiryDate})`; } else { expiryText = `剩 ${milk.daysRemaining} 天 (${milk.expiryDate})`; } card.innerHTML = `
${milk.note || '牛奶'} (${milk.volumePerItem}${milk.volumeUnit}) ${expiryText}
剩余: ${milk.remainingVolume.toFixed(1)}${milk.volumeUnit} / ${milk.remainingItemsApprox.toFixed(1)} 盒
`; milkListContainer.appendChild(card); // Add event listeners for buttons on this card const consumeBtn = card.querySelector('.consume-from-card-btn'); if(consumeBtn){ consumeBtn.addEventListener('click', (e) => { if(!e.target.disabled) { openRecordConsumptionModal(milk.id); } }); } const deleteBtn = card.querySelector('.delete-milk-btn'); if(deleteBtn){ deleteBtn.addEventListener('click', () => deleteMilkBatch(milk.id, milk.note)); } } function populateConsumeDropdown(activeBatches) { const selectMilk = document.getElementById('select-milk'); selectMilk.innerHTML = ''; // Clear existing if (activeBatches && activeBatches.length > 0) { // Sort by expiry, soonest first activeBatches.sort((a, b) => new Date(a.expiryDate) - new Date(b.expiryDate)); activeBatches.forEach(milk => { const option = document.createElement('option'); option.value = milk.id; option.textContent = `${milk.note || '牛奶'} (${milk.volumePerItem}${milk.volumeUnit}) - Exp: ${milk.expiryDate} (剩 ${milk.remainingVolume.toFixed(1)}${milk.volumeUnit})`; selectMilk.appendChild(option); }); } } function updateOverview(totalQtyStr, nearestExpStr, estimatedFinishStr) { document.getElementById('total-quantity').textContent = totalQtyStr; document.getElementById('nearest-expiry').textContent = nearestExpStr; document.getElementById('estimated-finish').textContent = estimatedFinishStr; } // --- Form Handling Functions --- async function handleAddMilkSubmit(event) { event.preventDefault(); const formData = new FormData(addMilkForm); const data = Object.fromEntries(formData.entries()); // Basic validation & data prep const payload = { productionDate: data.productionDate || null, shelfLife: data.shelfLife || null, shelfUnit: data.shelfUnit || null, expiryDate: data.expiryDate || null, quantity: parseInt(data.quantity, 10), volumePerItem: parseFloat(data.volumePerItem), unit: data.unit, note: data.note || null, }; if (!payload.quantity || !payload.volumePerItem || !payload.unit) { alert("请填写数量、单件容量和单位。"); return; } if (!payload.expiryDate && !(payload.productionDate && payload.shelfLife)) { alert("请提供过期日期,或同时提供生产日期和保质期。"); return; } if (payload.expiryDate && payload.productionDate && payload.shelfLife) { // If all 3 provided, maybe prefer expiryDate or warn? Let API handle/prefer expiry. payload.productionDate = null; // Example: Clear others if expiryDate is set payload.shelfLife = null; payload.shelfUnit = null; } console.log("Submitting Add Milk:", payload); try { await fetchApi('/milk', { method: 'POST', body: JSON.stringify(payload), }); alert('牛奶入库成功!'); closeModal(addMilkModal); loadMilkData(); // Refresh list and overview } catch (error) { // Error already logged by fetchApi alert(`入库失败: ${error.message}`); } } async function handleRecordConsumptionSubmit(event) { event.preventDefault(); const formData = new FormData(recordConsumptionForm); const data = Object.fromEntries(formData.entries()); const payload = { milkBatchId: parseInt(data.milkBatchId, 10), amountConsumed: parseFloat(data.amountConsumed), unitConsumed: data.unitConsumed, dateConsumed: data.dateConsumed ? new Date(data.dateConsumed).toISOString() : new Date().toISOString(), }; if (!payload.milkBatchId || isNaN(payload.milkBatchId) || !payload.amountConsumed || !payload.unitConsumed) { alert("请选择牛奶、填写消耗量和单位。"); return; } console.log("Submitting Record Consumption:", payload); try { await fetchApi('/consumption', { method: 'POST', body: JSON.stringify(payload), }); alert('消耗记录成功!'); closeModal(recordConsumptionModal); loadMilkData(); // Refresh list and overview } catch (error) { alert(`记录失败: ${error.message}`); } } async function loadSettings() { console.log("Loading user settings..."); if (!jwtToken) return; // Should not happen if settings button is shown only when logged in try { const profile = await fetchApi('/user/profile'); if (profile) { document.getElementById('expiry-warning-days').value = profile.expiryWarningDays ?? 2; document.getElementById('low-stock-threshold').value = profile.lowStockThresholdValue ?? ''; document.getElementById('low-stock-unit').value = profile.lowStockThresholdUnit ?? 'ml'; const notifications = profile.notificationsEnabled || {}; document.querySelector('input[name="enableExpiryWarning"]').checked = notifications.expiry ?? true; document.querySelector('input[name="enableExpiredAlert"]').checked = notifications.expired ?? true; document.querySelector('input[name="enableLowStock"]').checked = notifications.lowStock ?? true; // Update auth status with correct username if not already done if (profile.username && authStatusDiv.textContent.startsWith('用户')) { authStatusDiv.textContent = `已登录: ${profile.username}`; } } } catch (error) { console.error("Failed to load settings:", error); alert('加载设置失败。'); // Don't switch page if load fails showPage('dashboard'); } } async function handleSettingsSubmit(event) { event.preventDefault(); const formData = new FormData(settingsForm); const payload = { expiryWarningDays: parseInt(formData.get('expiryWarningDays'), 10) || 2, lowStockThresholdValue: formData.get('lowStockThresholdValue') ? parseFloat(formData.get('lowStockThresholdValue')) : null, lowStockThresholdUnit: formData.get('lowStockThresholdValue') ? formData.get('lowStockThresholdUnit') : null, // Only set unit if value exists notificationsEnabled: { expiry: formData.has('enableExpiryWarning'), expired: formData.has('enableExpiredAlert'), // Match key used in User.to_dict lowStock: formData.has('enableLowStock'), } }; // Basic validation for threshold if(payload.lowStockThresholdValue !== null && payload.lowStockThresholdValue < 0){ alert("低库存阈值不能为负数。"); return; } if(payload.lowStockThresholdValue !== null && !payload.lowStockThresholdUnit){ alert("设置低库存阈值时请选择单位。"); return; } console.log("Saving Settings:", payload); try { await fetchApi('/user/profile', { method: 'PUT', body: JSON.stringify(payload), }); alert('设置已保存!'); showPage('dashboard'); // Go back to dashboard } catch (error) { alert(`保存设置失败: ${error.message}`); } } async function deleteMilkBatch(batchId, milkNote) { const note = milkNote || `ID ${batchId}`; if (confirm(`确定要删除这条牛奶记录吗?\n(${note})`)) { console.log("Deleting milk batch:", batchId); try { await fetchApi(`/milk/${batchId}`, { method: 'DELETE' }); alert('记录已删除。'); loadMilkData(); // Refresh the list } catch (error) { alert(`删除失败: ${error.message}`); } } } // --- Modal Control Functions --- function openModal(modalElement) { if (modalElement) modalElement.style.display = 'block'; } function closeModal(modalElement) { if (modalElement) modalElement.style.display = 'none'; } function openRecordConsumptionModal(preselectedMilkId = null) { // Refresh dropdown with currently active batches from cache const activeBatches = milkDataCache.filter(b => !b.isDeleted && b.remainingVolume > 0.001 && b.status !== 'expired'); populateConsumeDropdown(activeBatches); const selectMilk = document.getElementById('select-milk'); if (preselectedMilkId && selectMilk) { selectMilk.value = preselectedMilkId; // Try to pre-select } else if (selectMilk && selectMilk.options.length > 1) { selectMilk.selectedIndex = 1; // Select the first available milk (soonest expiry) if none preselected } // Reset form fields recordConsumptionForm.reset(); consumeDateInput.value = new Date().toISOString().split('T')[0]; // Reset date to today openModal(recordConsumptionModal); } // --- Page Navigation --- function showPage(pageId) { document.querySelectorAll('.page').forEach(page => page.style.display = 'none'); dashboardPage.style.display = 'none'; const pageToShow = document.getElementById(pageId); if (pageToShow) { // If settings page, load settings first if (pageId === 'settings-page') { loadSettings(); // Load settings when showing the page } pageToShow.style.display = 'block'; } else { dashboardPage.style.display = 'block'; // Default to dashboard } } // --- Event Listeners --- if (addMilkBtn) { addMilkBtn.addEventListener('click', () => { addMilkForm.reset(); // Clear form openModal(addMilkModal); }); } if (recordConsumptionBtn) { recordConsumptionBtn.addEventListener('click', () => openRecordConsumptionModal()); } if (settingsBtn) { settingsBtn.addEventListener('click', () => showPage('settings-page')); } if (backToDashboardBtn) { backToDashboardBtn.addEventListener('click', () => showPage('dashboard')); } if (logoutBtn) { logoutBtn.addEventListener('click', () => handleLogout(true)); // Show login after logout } // Auth Modal Buttons if (loginBtn) loginBtn.addEventListener('click', handleLogin); if (registerBtn) registerBtn.addEventListener('click', handleRegister); // Close Modal Buttons if (closeAddModalBtn) closeAddModalBtn.addEventListener('click', () => closeModal(addMilkModal)); if (closeRecordModalBtn) closeRecordModalBtn.addEventListener('click', () => closeModal(recordConsumptionModal)); if (closeAuthModalBtn) closeAuthModalBtn.addEventListener('click', () => closeModal(authModal)); // Close Modal on outside click window.addEventListener('click', (event) => { if (event.target == addMilkModal) closeModal(addMilkModal); if (event.target == recordConsumptionModal) closeModal(recordConsumptionModal); if (event.target == authModal) closeModal(authModal); }); // Form Submissions if (addMilkForm) addMilkForm.addEventListener('submit', handleAddMilkSubmit); if (recordConsumptionForm) recordConsumptionForm.addEventListener('submit', handleRecordConsumptionSubmit); if (settingsForm) settingsForm.addEventListener('submit', handleSettingsSubmit); // Quick Consume Buttons Logic document.querySelectorAll('.consume-preset').forEach(button => { button.addEventListener('click', function() { const amount = this.dataset.amount; const unit = this.dataset.unit; document.getElementById('consume-amount').value = amount; document.getElementById('consume-unit').value = unit; }); }); // --- Initial App Load --- function initializeApp(){ console.log("Initializing app..."); updateAuthStatus(); if(jwtToken){ showPage('dashboard'); // Show dashboard if logged in loadMilkData(); // Load data if logged in } else { // Optionally show login immediately on load if not logged in showLoginModal(); hideMainContent(); noMilkMessage.textContent = '请先登录以查看或添加牛奶记录。'; noMilkMessage.style.display = 'block'; } // Set current year in footer document.getElementById('current-year').textContent = new Date().getFullYear(); // Set default consume date consumeDateInput.value = new Date().toISOString().split('T')[0]; } initializeApp(); // Start the app }); // End DOMContentLoaded