const API_BASE = '/api'; let currentPage = 'menu'; let categories = []; let dishes = []; let orders = []; let currentCategory = ''; let currentOrderFilter = ''; let dishCompletedCounts = {}; // Store how many times each dish appears in completed orders function getErrorMessage(error, fallback = '操作没有成功,请稍后再试。') { if (!error) return fallback; if (typeof error === 'string') return error; if (Array.isArray(error)) return error.join(';'); if (typeof error === 'object') { const values = Object.values(error).flatMap(value => Array.isArray(value) ? value : [value]); const text = values.filter(Boolean).join(';'); return text || fallback; } return fallback; } function showAppToast(message, type = 'success') { const stack = document.getElementById('toast-stack'); if (!stack || !message) return; const toast = document.createElement('div'); toast.className = `app-toast ${type === 'error' ? 'is-error' : 'is-success'}`; toast.innerHTML = `
`; toast.querySelector('.app-toast-copy').textContent = message; stack.appendChild(toast); window.setTimeout(() => { toast.remove(); }, 2600); } function confirmAction(message, title = '确认一下这步操作') { return new Promise(resolve => { const modalEl = document.getElementById('confirm-modal'); const titleEl = document.getElementById('confirm-modal-title'); const messageEl = document.getElementById('confirm-modal-message'); const cancelBtn = document.getElementById('confirm-modal-cancel'); const confirmBtn = document.getElementById('confirm-modal-confirm'); const modal = bootstrap.Modal.getOrCreateInstance(modalEl); let settled = false; titleEl.textContent = title; messageEl.textContent = message; const cleanup = result => { if (settled) return; settled = true; cancelBtn.removeEventListener('click', onCancel); confirmBtn.removeEventListener('click', onConfirm); modalEl.removeEventListener('hidden.bs.modal', onHidden); resolve(result); }; const onCancel = () => { modal.hide(); cleanup(false); }; const onConfirm = () => { modal.hide(); cleanup(true); }; const onHidden = () => cleanup(false); cancelBtn.addEventListener('click', onCancel); confirmBtn.addEventListener('click', onConfirm); modalEl.addEventListener('hidden.bs.modal', onHidden); modal.show(); }); } document.addEventListener('DOMContentLoaded', () => { loadCategories(); loadDishes(); loadOrders(); initEventListeners(); switchPage('menu'); }); function initEventListeners() { // Navigation document.querySelectorAll('.nav-item').forEach(item => { item.addEventListener('click', () => switchPage(item.dataset.page)); }); // Search - click on search box to open search panel const openSearchPanel = () => { document.getElementById('search-panel').classList.add('active'); document.getElementById('search-input').focus(); }; const menuSearchBox = document.getElementById('menu-search-box'); if (menuSearchBox) { menuSearchBox.addEventListener('click', openSearchPanel); } const heroSearchTrigger = document.getElementById('hero-search-trigger'); if (heroSearchTrigger) { heroSearchTrigger.addEventListener('click', openSearchPanel); } const heroOrderTrigger = document.getElementById('hero-order-trigger'); if (heroOrderTrigger) { heroOrderTrigger.addEventListener('click', () => switchPage('orders')); } document.getElementById('btn-close-search').addEventListener('click', () => { document.getElementById('search-panel').classList.remove('active'); document.getElementById('search-input').value = ''; document.getElementById('search-results').innerHTML = ''; const searchFeedback = document.getElementById('search-feedback'); if (searchFeedback) { searchFeedback.textContent = '输入关键词后,这里会显示匹配到的菜谱。'; } }); const searchInput = document.getElementById('search-input'); if (searchInput) { searchInput.addEventListener('input', debounce(loadSearchResults, 300)); } // FAB document.getElementById('fab-add').addEventListener('click', () => { if (currentPage === 'menu') showDishForm(); else if (currentPage === 'orders') showOrderForm(); }); // Order filter document.querySelectorAll('.filter-pill').forEach(pill => { pill.addEventListener('click', () => filterOrders(pill.dataset.status)); }); // Profile document.getElementById('btn-export').addEventListener('click', exportData); // Dish form document.getElementById('btn-add-ingredient').addEventListener('click', addIngredientRow); document.getElementById('btn-save-dish').addEventListener('click', saveDish); document.getElementById('dish-image').addEventListener('change', handleImageUpload); // Order form document.getElementById('btn-save-order').addEventListener('click', saveOrder); document.getElementById('btn-complete-order').addEventListener('click', () => { const orderId = document.getElementById('order-detail-modal').dataset.orderId; completeOrder(orderId); }); document.getElementById('btn-delete-order').addEventListener('click', () => { const orderId = document.getElementById('order-detail-modal').dataset.orderId; deleteOrder(orderId); }); // Parse document.getElementById('btn-apply-parse').addEventListener('click', applyParse); document.getElementById('parse-url').addEventListener('change', debounce(parseUrl, 800)); } // 存储每个页面的滚动位置 const pageScrollPositions = {}; function switchPage(page) { // 保存当前页面的滚动位置 if (currentPage) { pageScrollPositions[currentPage] = window.scrollY; } currentPage = page; document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); document.getElementById(`page-${page}`).classList.add('active'); document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); document.querySelector(`.nav-item[data-page="${page}"]`).classList.add('active'); // 恢复目标页面的滚动位置(无动画) if (pageScrollPositions[page] !== undefined) { window.scroll(0, pageScrollPositions[page]); } else { window.scroll(0, 0); } // Show/hide FAB based on page const fab = document.getElementById('fab-add'); const fabTop = document.getElementById('fab-top'); const fabLabel = document.getElementById('fab-label'); if (page === 'stats' || page === 'profile') { fab.style.display = 'none'; fabTop.style.display = 'none'; } else if (page === 'menu') { fab.style.display = 'flex'; fabTop.style.display = 'flex'; if (fabLabel) fabLabel.textContent = '记菜'; fab.setAttribute('aria-label', '新增菜品'); } else { fab.style.display = 'flex'; fabTop.style.display = 'none'; if (fabLabel) fabLabel.textContent = '建单'; fab.setAttribute('aria-label', '新建聚会单'); } if (page === 'orders') loadOrders(); if (page === 'stats') loadStats(); } async function loadStats() { try { // Load dishes const dishesRes = await fetch(`${API_BASE}/dishes/`); const allDishes = await dishesRes.json(); // Load orders const ordersRes = await fetch(`${API_BASE}/orders/`); const allOrders = await ordersRes.json(); // Calculate dish completed order counts (only completed orders) const dishCompletedCounts = {}; allOrders.filter(o => o.status === 'completed').forEach(order => { (order.dishes || []).forEach(dishId => { dishCompletedCounts[dishId] = (dishCompletedCounts[dishId] || 0) + 1; }); }); // Add order counts to dishes const dishesWithCounts = allDishes.map(dish => ({ ...dish, orderCount: dishCompletedCounts[dish.id] || 0 })); // Sort by order count dishesWithCounts.sort((a, b) => b.orderCount - a.orderCount); // Filter out dishes with 0 order count const dishesWithOrders = dishesWithCounts.filter(dish => dish.orderCount > 0); // Calculate unique participants and their participation counts const participantCounts = {}; allOrders.forEach(order => { if (order.participants && Array.isArray(order.participants)) { order.participants.forEach(name => { participantCounts[name] = (participantCounts[name] || 0) + 1; }); } }); const uniqueParticipants = Object.keys(participantCounts).length; // Store for ranking modal window.participantRanking = Object.entries(participantCounts) .sort((a, b) => b[1] - a[1]) .map(([name, count]) => ({ name, count })); // Update stats document.getElementById('total-dishes').textContent = allDishes.length; document.getElementById('total-gatherings').textContent = allOrders.length; document.getElementById('total-participants').textContent = uniqueParticipants; // Render ranking const rankList = document.getElementById('dish-rank-list'); if (dishesWithOrders.length === 0) { rankList.innerHTML = `暂时还没有汇总出食材,等选好菜后这里会自动整理。
'; } // Render participants with edit capability window.detailOrderParticipants = order && order.participants ? [...order.participants] : []; renderDetailParticipantTags(); setupDetailParticipantInput(); // Update summary to reflect changes updateDetailSummary(); document.getElementById('btn-complete-order').style.display = order.status === 'in_progress' ? 'block' : 'none'; // Hide ingredients section for completed orders const ingredientsSection = document.getElementById('order-ingredients-section'); ingredientsSection.style.display = order.status === 'completed' ? 'none' : 'block'; // Render party images renderOrderImages(order); // Add share button handler document.getElementById('btn-share-order').onclick = () => shareOrderAsImage(order); new bootstrap.Modal(document.getElementById('order-detail-modal')).show(); } function renderDetailParticipantTags() { const container = document.getElementById('detail-participants-tags'); if (!container) return; container.innerHTML = window.detailOrderParticipants.map(name => ` ${name} `).join(''); } function setupDetailParticipantInput() { const input = document.getElementById('detail-participant-input'); if (!input) return; input.onkeypress = function(e) { if (e.key === 'Enter' && this.value.trim()) { e.preventDefault(); const name = this.value.trim(); if (name && !window.detailOrderParticipants.includes(name)) { window.detailOrderParticipants.push(name); renderDetailParticipantTags(); saveDetailParticipants(); } this.value = ''; } }; } function removeDetailParticipant(name) { window.detailOrderParticipants = window.detailOrderParticipants.filter(p => p !== name); renderDetailParticipantTags(); saveDetailParticipants(); } async function saveDetailParticipants() { const modal = document.getElementById('order-detail-modal'); const orderId = modal.dataset.orderId; if (!orderId) return; try { const res = await fetch(`${API_BASE}/orders/${orderId}/`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ participants: window.detailOrderParticipants }) }); if (res.ok) { const updatedOrder = await res.json(); // Update local orders cache const idx = orders.findIndex(o => o.id == orderId); if (idx !== -1) { orders[idx] = updatedOrder; } updateDetailSummary(); // Refresh ranking if exists if (window.participantRanking) { calculateParticipantRanking(); if (document.getElementById('participant-ranking-modal').classList.contains('show')) { showParticipantRanking(); } } } } catch (err) { console.error('Failed to save participants:', err); } } function updateDetailSummary() { const modal = document.getElementById('order-detail-modal'); const orderId = modal.dataset.orderId; const order = orders.find(o => o.id == orderId); if (!order) return; const dishCount = order.dishes_detail ? order.dishes_detail.length : 0; const participantCount = window.detailOrderParticipants ? window.detailOrderParticipants.length : 0; const summaryEl = document.getElementById('order-detail-summary'); if (summaryEl) { summaryEl.textContent = order.status === 'in_progress' ? `这张备菜清单里有 ${dishCount} 道菜${participantCount > 0 ? `,共 ${participantCount} 位参与人` : ''}。` : `这次聚会已经完成,留下了 ${dishCount} 道上桌菜品${participantCount > 0 ? `和 ${participantCount} 位参与人` : ''}的记录。`; } } // Render party images function renderOrderImages(order) { const container = document.getElementById('order-detail-images'); const images = order.images || []; if (images.length === 0) { container.innerHTML = '还没有留下聚会照片,饭桌热闹时记得补一张。
'; } else { container.innerHTML = images.map((img, idx) => `${new Date(order.created_at).toLocaleDateString()} · ${allDishes.length}道菜
暂无菜品
'}暂无参与记录
'; } else { const maxCount = Math.max(...ranking.map(r => r.count)); container.innerHTML = ranking.map((item, index) => { const percentage = maxCount > 0 ? (item.count / maxCount) * 100 : 0; let rankClass = ''; let gradient = ''; if (index === 0) { rankClass = 'rank-gold'; gradient = 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 50%, #d97706 100%)'; } else if (index === 1) { rankClass = 'rank-silver'; gradient = 'linear-gradient(135deg, #94a3b8 0%, #64748b 100%)'; } else if (index === 2) { rankClass = 'rank-bronze'; gradient = 'linear-gradient(135deg, #d97706 0%, #b45309 100%)'; } else { gradient = 'linear-gradient(90deg, #6366f1 0%, #8b5cf6 100%)'; } return `