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:
Developer
2026-03-12 11:27:23 +08:00
co-authored by Claude Opus 4.6
commit 0a50c09dba
47 changed files with 10268 additions and 0 deletions
+98
View File
@@ -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;
+104
View File
@@ -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;
+130
View File
@@ -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;