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:
@@ -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;
|
||||
Reference in New Issue
Block a user