Initial commit - Zhangmenu Django project
A menu management application built with Django, featuring: - Dish management with cover images - Order system with party dates and participants - REST API with Django REST Framework - Bootstrap-styled frontend Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vendored
+5
File diff suppressed because one or more lines are too long
Vendored
+6
File diff suppressed because one or more lines are too long
Vendored
+7
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<rect width="64" height="64" rx="14" fill="#fff7ed"/>
|
||||
<circle cx="32" cy="34" r="18" fill="#c2410c"/>
|
||||
<path d="M22 24c0-3.5 2.5-6 5.5-6 1.8 0 3.3.8 4.5 2.3 1.2-1.5 2.7-2.3 4.5-2.3 3 0 5.5 2.5 5.5 6 0 2.9-1.6 5.2-4.8 7.8-1.8 1.5-3.8 2.9-5.2 4-1.4-1.1-3.4-2.5-5.2-4-3.2-2.6-4.8-4.9-4.8-7.8z" fill="#fff7ed"/>
|
||||
<path d="M18 46c3.5-4.2 8.6-6.4 14-6.4S42.5 41.8 46 46" fill="none" stroke="#fdba74" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 510 B |
Binary file not shown.
Binary file not shown.
Vendored
+20
File diff suppressed because one or more lines are too long
@@ -0,0 +1,90 @@
|
||||
// API 调用封装
|
||||
const API_BASE = '/api';
|
||||
|
||||
const API = {
|
||||
// 菜品相关
|
||||
async getDishes(params = {}) {
|
||||
const query = new URLSearchParams(params).toString();
|
||||
const res = await fetch(`${API_BASE}/dishes/${query ? '?' + query : ''}`);
|
||||
return res.json();
|
||||
},
|
||||
|
||||
async getDish(id) {
|
||||
const res = await fetch(`${API_BASE}/dishes/${id}/`);
|
||||
return res.json();
|
||||
},
|
||||
|
||||
async createDish(data) {
|
||||
const res = await fetch(`${API_BASE}/dishes/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
return res.json();
|
||||
},
|
||||
|
||||
async updateDish(id, data) {
|
||||
const res = await fetch(`${API_BASE}/dishes/${id}/`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
return res.json();
|
||||
},
|
||||
|
||||
async deleteDish(id) {
|
||||
await fetch(`${API_BASE}/dishes/${id}/`, { method: 'DELETE' });
|
||||
},
|
||||
|
||||
// 分类相关
|
||||
async getCategories() {
|
||||
const res = await fetch(`${API_BASE}/categories/`);
|
||||
return res.json();
|
||||
},
|
||||
|
||||
// 订单相关
|
||||
async getOrders(params = {}) {
|
||||
const query = new URLSearchParams(params).toString();
|
||||
const res = await fetch(`${API_BASE}/orders/${query ? '?' + query : ''}`);
|
||||
return res.json();
|
||||
},
|
||||
|
||||
async createOrder(data) {
|
||||
const res = await fetch(`${API_BASE}/orders/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
return res.json();
|
||||
},
|
||||
|
||||
async completeOrder(id) {
|
||||
const res = await fetch(`${API_BASE}/orders/${id}/complete/`, { method: 'POST' });
|
||||
return res.json();
|
||||
},
|
||||
|
||||
async deleteOrder(id) {
|
||||
await fetch(`${API_BASE}/orders/${id}/`, { method: 'DELETE' });
|
||||
},
|
||||
|
||||
// URL解析
|
||||
async parseUrl(url) {
|
||||
const res = await fetch(`${API_BASE}/parse-url/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url })
|
||||
});
|
||||
return res.json();
|
||||
},
|
||||
|
||||
// 图片上传
|
||||
async uploadImage(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
const res = await fetch('/upload-image/', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
// 应用入口
|
||||
|
||||
const App = {
|
||||
// 初始化应用
|
||||
async init() {
|
||||
console.log('应用初始化...');
|
||||
|
||||
// 初始化事件监听
|
||||
this.initEventListeners();
|
||||
|
||||
// 加载初始数据
|
||||
await this.loadInitialData();
|
||||
|
||||
// 显示默认页面
|
||||
this.switchPage('menu');
|
||||
|
||||
console.log('应用初始化完成');
|
||||
},
|
||||
|
||||
// 初始化事件监听
|
||||
initEventListeners() {
|
||||
// 底部导航点击
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const page = item.dataset.page;
|
||||
if (page) {
|
||||
App.switchPage(page);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// FAB按钮 - 根据当前页面决定行为
|
||||
const fab = document.getElementById('fab-add');
|
||||
if (fab) {
|
||||
fab.addEventListener('click', () => {
|
||||
if (AppState.currentPage === 'menu') {
|
||||
// 菜单页:创建新菜品
|
||||
showDishModal();
|
||||
} else if (AppState.currentPage === 'orders') {
|
||||
// 订单页:创建新订单
|
||||
showOrderModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 搜索按钮
|
||||
const searchBtn = document.getElementById('search-btn');
|
||||
if (searchBtn) {
|
||||
searchBtn.addEventListener('click', () => {
|
||||
const keyword = document.getElementById('search-input').value;
|
||||
AppState.setPage('search');
|
||||
document.getElementById('search-panel').classList.add('active');
|
||||
MenuPage.searchDishes(keyword);
|
||||
});
|
||||
}
|
||||
|
||||
// 搜索面板关闭
|
||||
const searchBackBtn = document.getElementById('search-panel-back');
|
||||
if (searchBackBtn) {
|
||||
searchBackBtn.addEventListener('click', () => {
|
||||
document.getElementById('search-panel').classList.remove('active');
|
||||
App.switchPage(AppState.currentPage || 'menu');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 加载初始数据
|
||||
async loadInitialData() {
|
||||
try {
|
||||
// 并行加载所有数据
|
||||
const [categoriesRes, dishesRes, ordersRes] = await Promise.all([
|
||||
fetch(`${API_BASE}/categories/`),
|
||||
fetch(`${API_BASE}/dishes/`),
|
||||
fetch(`${API_BASE}/orders/`)
|
||||
]);
|
||||
|
||||
const categories = await categoriesRes.json();
|
||||
const dishes = await dishesRes.json();
|
||||
const orders = await ordersRes.json();
|
||||
|
||||
AppState.setCategories(categories);
|
||||
AppState.setDishes(dishes);
|
||||
AppState.setOrders(orders);
|
||||
|
||||
} catch (e) {
|
||||
console.error('加载初始数据失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
// 切换页面
|
||||
switchPage(page) {
|
||||
// 更新状态
|
||||
AppState.setPage(page);
|
||||
|
||||
// 更新导航
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
item.classList.toggle('active', item.dataset.page === page);
|
||||
});
|
||||
|
||||
// 更新页面显示
|
||||
document.querySelectorAll('.page').forEach(p => {
|
||||
p.classList.remove('active');
|
||||
});
|
||||
|
||||
const pageEl = document.getElementById(`${page}-page`);
|
||||
if (pageEl) {
|
||||
pageEl.classList.add('active');
|
||||
}
|
||||
|
||||
// 根据页面执行对应初始化
|
||||
if (page === 'menu') {
|
||||
MenuPage.init();
|
||||
} else if (page === 'orders') {
|
||||
OrdersPage.init();
|
||||
} else if (page === 'stats') {
|
||||
StatsPage.init();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// DOM 加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 初始化全局变量(保持向后兼容)
|
||||
window.API_BASE = '/api';
|
||||
|
||||
// 初始化应用
|
||||
App.init();
|
||||
});
|
||||
|
||||
// 导出
|
||||
window.App = App;
|
||||
@@ -0,0 +1,43 @@
|
||||
// 菜品卡片组件
|
||||
|
||||
// 渲染菜品卡片
|
||||
function renderDishCard(dish, isInOrder = false) {
|
||||
const indicatorClass = isInOrder ? 'in-order' : 'not-in-order';
|
||||
const indicatorIcon = isInOrder ? '✓' : '+';
|
||||
|
||||
return `
|
||||
<div class="dish-card" data-id="${dish.id}">
|
||||
<div class="img-wrapper">
|
||||
<img src="${dish.cover_image || getDefaultImage()}" alt="${dish.name}">
|
||||
${dish.category_name ? `<span class="category-tag">${dish.category_name}</span>` : ''}
|
||||
<span class="order-indicator ${indicatorClass}">${indicatorIcon}</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="dish-name">${dish.name}</div>
|
||||
<div class="dish-meta">${dish.view_count || 0} 次浏览</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 渲染菜品网格
|
||||
function renderDishGrid(dishes, containerSelector, onCardClick) {
|
||||
const container = document.querySelector(containerSelector);
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = dishes.map(dish => renderDishCard(dish)).join('');
|
||||
|
||||
container.querySelectorAll('.dish-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
if (onCardClick) {
|
||||
onCardClick(card.dataset.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 导出
|
||||
window.DishCard = {
|
||||
render: renderDishCard,
|
||||
renderGrid: renderDishGrid
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
// 弹窗组件
|
||||
|
||||
const Modal = {
|
||||
// 显示弹窗
|
||||
show(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
},
|
||||
|
||||
// 隐藏弹窗
|
||||
hide(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
},
|
||||
|
||||
// 切换弹窗
|
||||
toggle(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
if (modal.style.display === 'none') {
|
||||
this.show(modalId);
|
||||
} else {
|
||||
this.hide(modalId);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 显示确认对话框
|
||||
confirm(message) {
|
||||
return confirm(message);
|
||||
},
|
||||
|
||||
// 显示加载状态
|
||||
showLoading(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
const footer = modal.querySelector('.modal-footer');
|
||||
if (footer) {
|
||||
footer.dataset.originalContent = footer.innerHTML;
|
||||
footer.innerHTML = '<span class="loading-text">加载中...</span>';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 隐藏加载状态
|
||||
hideLoading(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
const footer = modal.querySelector('.modal-footer');
|
||||
if (footer && footer.dataset.originalContent) {
|
||||
footer.innerHTML = footer.dataset.originalContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Bootstrap 模态框包装
|
||||
const BootstrapModal = {
|
||||
init(modalId) {
|
||||
return new bootstrap.Modal(document.getElementById(modalId));
|
||||
},
|
||||
|
||||
show(modalId) {
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById(modalId));
|
||||
modal.show();
|
||||
},
|
||||
|
||||
hide(modalId) {
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById(modalId));
|
||||
modal.hide();
|
||||
}
|
||||
};
|
||||
|
||||
// 导出
|
||||
window.Modal = Modal;
|
||||
window.BootstrapModal = BootstrapModal;
|
||||
@@ -0,0 +1,52 @@
|
||||
// 订单卡片组件
|
||||
|
||||
// 渲染订单卡片
|
||||
function renderOrderCard(order) {
|
||||
const statusClass = order.status === 'in_progress' ? 'status-progress' : 'status-done';
|
||||
const statusText = order.status === 'in_progress' ? '进行中' : '已完成';
|
||||
|
||||
// 显示聚会时间或创建时间
|
||||
const displayDate = order.party_date
|
||||
? new Date(order.party_date).toLocaleDateString()
|
||||
: (order.created_at ? new Date(order.created_at).toLocaleDateString() : '');
|
||||
|
||||
const dishCount = order.dishes_detail ? order.dishes_detail.length : 0;
|
||||
|
||||
return `
|
||||
<div class="order-card" data-id="${order.id}">
|
||||
<div class="order-top">
|
||||
<span class="order-name">${order.name}</span>
|
||||
<span class="order-status ${statusClass}">${statusText}</span>
|
||||
</div>
|
||||
<div class="order-date">${displayDate} · ${dishCount} 道菜</div>
|
||||
<div class="dish-list">
|
||||
${(order.dishes_detail || []).map(d => {
|
||||
const isCold = d.category_name && d.category_name.includes('凉菜');
|
||||
return `<span class="dish-tag ${isCold ? 'dish-tag-cold' : ''}">${d.name}</span>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 渲染订单列表
|
||||
function renderOrderList(orders, containerSelector, onCardClick) {
|
||||
const container = document.querySelector(containerSelector);
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = orders.map(order => renderOrderCard(order)).join('');
|
||||
|
||||
container.querySelectorAll('.order-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
if (onCardClick) {
|
||||
onCardClick(card.dataset.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 导出
|
||||
window.OrderCard = {
|
||||
render: renderOrderCard,
|
||||
renderList: renderOrderList
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
// 菜单页面模块
|
||||
|
||||
const MenuPage = {
|
||||
// 初始化
|
||||
init() {
|
||||
this.loadCategories();
|
||||
this.loadDishes();
|
||||
},
|
||||
|
||||
// 加载分类
|
||||
async loadCategories() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/categories/`);
|
||||
const data = await res.json();
|
||||
AppState.setCategories(data);
|
||||
this.renderCategories();
|
||||
} catch (e) {
|
||||
console.error('加载分类失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
// 加载菜品
|
||||
async loadDishes() {
|
||||
try {
|
||||
const params = {};
|
||||
if (AppState.currentCategory) {
|
||||
const category = AppState.categories.find(c => c.name === AppState.currentCategory);
|
||||
if (category) params.category = category.id;
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_BASE}/dishes/${new URLSearchParams(params).toString() ? '?' + new URLSearchParams(params).toString() : ''}`);
|
||||
const data = await res.json();
|
||||
AppState.setDishes(data);
|
||||
this.renderDishes();
|
||||
} catch (e) {
|
||||
console.error('加载菜品失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
// 渲染分类
|
||||
renderCategories() {
|
||||
const container = document.getElementById('category-filter');
|
||||
if (!container) return;
|
||||
|
||||
const html = `
|
||||
<span class="category-chip ${!AppState.currentCategory ? 'active' : ''}" data-category="">全部</span>
|
||||
${AppState.categories.map(c => `
|
||||
<span class="category-chip ${AppState.currentCategory === c.name ? 'active' : ''}" data-category="${c.name}">${c.name}</span>
|
||||
`).join('')}
|
||||
`;
|
||||
container.innerHTML = html;
|
||||
|
||||
// 绑定点击事件
|
||||
container.querySelectorAll('.category-chip').forEach(chip => {
|
||||
chip.addEventListener('click', () => {
|
||||
AppState.setCurrentCategory(chip.dataset.category);
|
||||
this.renderCategories();
|
||||
this.loadDishes();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 渲染菜品
|
||||
renderDishes() {
|
||||
const container = document.getElementById('dish-grid');
|
||||
if (!container) return;
|
||||
|
||||
const currentOrder = AppState.getCurrentOrder();
|
||||
const dishes = AppState.getFilteredDishes();
|
||||
|
||||
container.innerHTML = dishes.map(dish => {
|
||||
const isInOrder = currentOrder && currentOrder.dishes && currentOrder.dishes.includes(dish.id);
|
||||
return DishCard.render(dish, isInOrder);
|
||||
}).join('');
|
||||
|
||||
// 绑定点击事件
|
||||
container.querySelectorAll('.dish-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
showDishDetail(card.dataset.id);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 搜索菜品
|
||||
async searchDishes(keyword) {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/dishes/?search=${encodeURIComponent(keyword)}`);
|
||||
const data = await res.json();
|
||||
AppState.setDishes(data);
|
||||
this.renderDishes();
|
||||
} catch (e) {
|
||||
console.error('搜索失败:', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 导出
|
||||
window.MenuPage = MenuPage;
|
||||
@@ -0,0 +1,104 @@
|
||||
// 订单页面模块
|
||||
|
||||
const OrdersPage = {
|
||||
// 初始化
|
||||
init() {
|
||||
this.loadOrders();
|
||||
},
|
||||
|
||||
// 加载订单
|
||||
async loadOrders() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/orders/`);
|
||||
const data = await res.json();
|
||||
AppState.setOrders(data);
|
||||
this.renderOrders();
|
||||
} catch (e) {
|
||||
console.error('加载订单失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
// 渲染订单
|
||||
renderOrders() {
|
||||
const container = document.getElementById('order-list');
|
||||
if (!container) return;
|
||||
|
||||
const orders = AppState.getFilteredOrders();
|
||||
|
||||
if (orders.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">📋</div>
|
||||
<div class="empty-title">暂无聚会</div>
|
||||
<div class="empty-desc">点击下方按钮创建第一个聚会吧</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = orders.map(order => OrderCard.render(order)).join('');
|
||||
|
||||
// 绑定点击事件
|
||||
container.querySelectorAll('.order-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
showOrderDetail(card.dataset.id);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 筛选订单
|
||||
filterOrders(status) {
|
||||
AppState.setCurrentOrderFilter(status);
|
||||
this.renderOrders();
|
||||
},
|
||||
|
||||
// 创建订单
|
||||
async createOrder(name, dishIds, partyDate) {
|
||||
try {
|
||||
const data = {
|
||||
name,
|
||||
dish_ids: dishIds,
|
||||
party_date: partyDate
|
||||
};
|
||||
const res = await fetch(`${API_BASE}/orders/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const order = await res.json();
|
||||
await this.loadOrders();
|
||||
return order;
|
||||
} catch (e) {
|
||||
console.error('创建订单失败:', e);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
// 完成订单
|
||||
async completeOrder(orderId) {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/orders/${orderId}/complete/`, {
|
||||
method: 'POST'
|
||||
});
|
||||
await res.json();
|
||||
await this.loadOrders();
|
||||
} catch (e) {
|
||||
console.error('完成订单失败:', e);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
// 删除订单
|
||||
async deleteOrder(orderId) {
|
||||
try {
|
||||
await fetch(`${API_BASE}/orders/${orderId}/`, { method: 'DELETE' });
|
||||
await this.loadOrders();
|
||||
} catch (e) {
|
||||
console.error('删除订单失败:', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 导出
|
||||
window.OrdersPage = OrdersPage;
|
||||
@@ -0,0 +1,130 @@
|
||||
// 统计页面模块
|
||||
|
||||
const StatsPage = {
|
||||
// 初始化
|
||||
init() {
|
||||
this.loadStats();
|
||||
},
|
||||
|
||||
// 加载统计数据
|
||||
async loadStats() {
|
||||
try {
|
||||
const [dishesRes, ordersRes] = await Promise.all([
|
||||
fetch(`${API_BASE}/dishes/`),
|
||||
fetch(`${API_BASE}/orders/`)
|
||||
]);
|
||||
|
||||
const allDishes = await dishesRes.json();
|
||||
const allOrders = await ordersRes.json();
|
||||
|
||||
// 计算菜品出现次数
|
||||
const dishCompletedCounts = {};
|
||||
allOrders
|
||||
.filter(o => o.status === 'completed')
|
||||
.forEach(order => {
|
||||
(order.dishes_detail || []).forEach(dish => {
|
||||
dishCompletedCounts[dish.id] = (dishCompletedCounts[dish.id] || 0) + 1;
|
||||
});
|
||||
});
|
||||
|
||||
AppState.setDishCompletedCounts(dishCompletedCounts);
|
||||
|
||||
// 统计数据
|
||||
const totalDishes = allDishes.length;
|
||||
const completedOrders = allOrders.filter(o => o.status === 'completed').length;
|
||||
const inProgressOrders = allOrders.filter(o => o.status === 'in_progress').length;
|
||||
|
||||
// 统计参与人员
|
||||
const participantCounts = {};
|
||||
allOrders.forEach(order => {
|
||||
(order.participants || []).forEach(p => {
|
||||
participantCounts[p] = (participantCounts[p] || 0) + 1;
|
||||
});
|
||||
});
|
||||
const uniqueParticipants = Object.keys(participantCounts).length;
|
||||
|
||||
// 渲染统计卡片
|
||||
this.renderStatCards({ totalDishes, completedOrders, inProgressOrders, uniqueParticipants });
|
||||
|
||||
// 渲染菜品排行榜
|
||||
this.renderDishRank(allDishes, dishCompletedCounts);
|
||||
|
||||
} catch (e) {
|
||||
console.error('加载统计失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
// 渲染统计卡片
|
||||
renderStatCards(stats) {
|
||||
const container = document.getElementById('stats-cards');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, var(--primary-soft), #fef3c7); color: var(--primary);">
|
||||
<i class="bi bi-book"></i>
|
||||
</div>
|
||||
<div class="stat-value">${stats.totalDishes}</div>
|
||||
<div class="stat-label">菜品总数</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: var(--secondary-soft); color: var(--secondary);">
|
||||
<i class="bi bi-check-circle"></i>
|
||||
</div>
|
||||
<div class="stat-value">${stats.completedOrders}</div>
|
||||
<div class="stat-label">已完成聚会</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: var(--accent-soft); color: #b45309;">
|
||||
<i class="bi bi-clock"></i>
|
||||
</div>
|
||||
<div class="stat-value">${stats.inProgressOrders}</div>
|
||||
<div class="stat-label">进行中</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
// 渲染菜品排行榜
|
||||
renderDishRank(dishes, counts) {
|
||||
const container = document.getElementById('dish-rank-list');
|
||||
if (!container) return;
|
||||
|
||||
// 按出现次数排序
|
||||
const dishesWithCounts = dishes.map(dish => ({
|
||||
...dish,
|
||||
orderCount: counts[dish.id] || 0
|
||||
})).filter(d => d.orderCount > 0)
|
||||
.sort((a, b) => b.orderCount - a.orderCount);
|
||||
|
||||
if (dishesWithCounts.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-rank">
|
||||
<i class="bi bi-trophy"></i>
|
||||
<p>暂无数据</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = dishesWithCounts.slice(0, 10).map((dish, index) => {
|
||||
let rankClass = 'default';
|
||||
if (index === 0) rankClass = 'gold';
|
||||
else if (index === 1) rankClass = 'silver';
|
||||
else if (index === 2) rankClass = 'bronze';
|
||||
|
||||
return `
|
||||
<div class="rank-item">
|
||||
<span class="rank-number ${rankClass}">${index + 1}</span>
|
||||
<img class="rank-img" src="${dish.cover_image || getDefaultImage()}" alt="${dish.name}">
|
||||
<div class="rank-info">
|
||||
<div class="rank-name">${dish.name}</div>
|
||||
<div class="rank-count">出现 ${dish.orderCount} 次</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
};
|
||||
|
||||
// 导出
|
||||
window.StatsPage = StatsPage;
|
||||
@@ -0,0 +1,78 @@
|
||||
// 状态管理
|
||||
const state = {
|
||||
currentPage: 'menu',
|
||||
categories: [],
|
||||
dishes: [],
|
||||
orders: [],
|
||||
currentCategory: '',
|
||||
currentOrderFilter: '',
|
||||
dishCompletedCounts: {},
|
||||
|
||||
// 设置状态
|
||||
setPage(page) {
|
||||
this.currentPage = page;
|
||||
},
|
||||
|
||||
setCategories(categories) {
|
||||
this.categories = categories;
|
||||
},
|
||||
|
||||
setDishes(dishes) {
|
||||
this.dishes = dishes;
|
||||
},
|
||||
|
||||
setOrders(orders) {
|
||||
this.orders = orders;
|
||||
},
|
||||
|
||||
setCurrentCategory(category) {
|
||||
this.currentCategory = category;
|
||||
},
|
||||
|
||||
setCurrentOrderFilter(filter) {
|
||||
this.currentOrderFilter = filter;
|
||||
},
|
||||
|
||||
setDishCompletedCounts(counts) {
|
||||
this.dishCompletedCounts = counts;
|
||||
},
|
||||
|
||||
// 获取过滤后的数据
|
||||
getFilteredDishes() {
|
||||
let filtered = this.dishes;
|
||||
if (this.currentCategory) {
|
||||
filtered = filtered.filter(d => d.category_name === this.currentCategory);
|
||||
}
|
||||
return filtered;
|
||||
},
|
||||
|
||||
getFilteredOrders() {
|
||||
let filtered = this.orders;
|
||||
if (this.currentOrderFilter) {
|
||||
filtered = filtered.filter(o => o.status === this.currentOrderFilter);
|
||||
}
|
||||
return filtered;
|
||||
},
|
||||
|
||||
// 获取菜品在当前订单中的状态
|
||||
isDishInCurrentOrder(dishId) {
|
||||
const currentOrder = this.orders.find(o => o.status === 'in_progress');
|
||||
if (!currentOrder) return false;
|
||||
return currentOrder.dishes && currentOrder.dishes.includes(dishId);
|
||||
},
|
||||
|
||||
// 获取当前进行中的订单
|
||||
getCurrentOrder() {
|
||||
return this.orders.find(o => o.status === 'in_progress');
|
||||
},
|
||||
|
||||
// 重置状态
|
||||
reset() {
|
||||
this.currentPage = 'menu';
|
||||
this.currentCategory = '';
|
||||
this.currentOrderFilter = '';
|
||||
}
|
||||
};
|
||||
|
||||
// 导出供全局使用
|
||||
window.AppState = state;
|
||||
@@ -0,0 +1,36 @@
|
||||
// 工具函数
|
||||
|
||||
// 格式化日期时间
|
||||
function formatDateTime(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
// 防抖函数
|
||||
function debounce(fn, delay) {
|
||||
let timer;
|
||||
return function(...args) {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fn.apply(this, args), delay);
|
||||
};
|
||||
}
|
||||
|
||||
// 生成默认图片
|
||||
function getDefaultImage() {
|
||||
return 'data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%2248%22 height=%2248%22><rect fill=%22%23fed7aa%22 width=%2248%22 height=%2248%22/><text x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dy=%22.3em%22 fill=%22%23c2410c%22>🍳</text></svg>';
|
||||
}
|
||||
|
||||
// 确认对话框
|
||||
function confirmAction(message) {
|
||||
return confirm(message);
|
||||
}
|
||||
|
||||
// 显示提示
|
||||
function showToast(message, type = 'info') {
|
||||
// 简单实现,实际可以使用更优雅的toast库
|
||||
alert(message);
|
||||
}
|
||||
Vendored
+6
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user