feat: 食堂菜单管理系统 + AI编程入门指南

This commit is contained in:
2026-05-25 18:16:28 +08:00
commit e2809cb56b
6 changed files with 832 additions and 0 deletions

119
AI编程入门指南.md Normal file

File diff suppressed because one or more lines are too long

713
index.html Normal file
View File

@@ -0,0 +1,713 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>食堂菜单管理系统</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #E8530E;
--primary-light: #FF7A3D;
--primary-dark: #C44200;
--bg: #FFF8F3;
--card: #FFFFFF;
--text: #2D2D2D;
--text-secondary: #666;
--border: #F0E0D6;
--shadow: 0 2px 12px rgba(232,83,14,0.08);
--radius: 12px;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, var(--primary), var(--primary-light));
color: white;
padding: 24px 32px;
box-shadow: 0 4px 20px rgba(232,83,14,0.2);
}
.header h1 { font-size: 28px; font-weight: 700; letter-spacing: 1px; }
.header p { font-size: 14px; opacity: 0.9; margin-top: 4px; }
.container { max-width: 1200px; margin: 0 auto; padding: 24px 16px; }
.controls {
display: flex; gap: 12px; align-items: center; flex-wrap: wrap;
margin-bottom: 24px; padding: 16px 20px;
background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow);
}
.controls label { font-weight: 600; font-size: 14px; color: var(--text-secondary); }
.controls select, .controls input[type="month"] {
padding: 8px 14px; border: 2px solid var(--border); border-radius: 8px;
font-size: 14px; background: white; cursor: pointer; outline: none;
transition: border-color 0.2s;
}
.controls select:focus, .controls input[type="month"]:focus { border-color: var(--primary); }
.btn {
padding: 8px 20px; border: none; border-radius: 8px; font-size: 14px;
font-weight: 600; cursor: pointer; transition: all 0.2s; white-space: nowrap;
}
.btn-primary { background: var(--primary); color: white; }
.btn-primary:hover { background: var(--primary-dark); transform: translateY(-1px); }
.btn-outline { background: white; color: var(--primary); border: 2px solid var(--primary); }
.btn-outline:hover { background: var(--primary); color: white; }
.btn-green { background: #2E7D32; color: white; }
.btn-green:hover { background: #1B5E20; transform: translateY(-1px); }
.tabs {
display: flex; gap: 0; margin-bottom: 24px;
background: var(--card); border-radius: var(--radius); overflow: hidden;
box-shadow: var(--shadow);
}
.tab {
flex: 1; padding: 14px 20px; text-align: center; font-weight: 600;
font-size: 15px; cursor: pointer; transition: all 0.3s;
border-bottom: 3px solid transparent; color: var(--text-secondary);
}
.tab.active { color: var(--primary); border-bottom-color: var(--primary); background: #FFF3ED; }
.tab:hover:not(.active) { background: #FFF8F3; }
/* Calendar View */
.calendar-grid {
display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px;
background: var(--card); border-radius: var(--radius); padding: 16px;
box-shadow: var(--shadow);
}
.cal-header { text-align: center; font-weight: 700; font-size: 14px; color: var(--primary); padding: 8px 0; }
.cal-header.weekend { color: #E53935; }
.cal-day {
min-height: 90px; border-radius: 8px; padding: 8px; font-size: 12px;
border: 2px solid transparent; cursor: pointer; transition: all 0.2s; position: relative;
}
.cal-day:hover { border-color: var(--primary-light); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
.cal-day.empty { background: #FAFAFA; cursor: default; }
.cal-day.empty:hover { border-color: transparent; transform: none; box-shadow: none; }
.cal-day .day-num { font-weight: 700; font-size: 16px; margin-bottom: 6px; color: var(--text); }
.cal-day.weekend .day-num { color: #E53935; }
.cal-day.today { border-color: var(--primary); background: #FFF3ED; }
.cal-day .meal-tag {
display: inline-block; padding: 1px 6px; border-radius: 4px; font-size: 11px;
margin: 1px 2px; font-weight: 600;
}
.tag-breakfast { background: #FFF3E0; color: #E65100; }
.tag-lunch { background: #E8F5E9; color: #2E7D32; }
.tag-dinner { background: #E3F2FD; color: #1565C0; }
/* Week View */
.week-view { background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; }
.week-nav {
display: flex; align-items: center; justify-content: space-between;
padding: 16px 24px; background: linear-gradient(135deg, #FFF3ED, #FFF8F3);
border-bottom: 1px solid var(--border);
}
.week-nav h3 { font-size: 18px; color: var(--primary); }
.week-table { width: 100%; border-collapse: collapse; }
.week-table th {
padding: 12px 8px; font-size: 13px; font-weight: 700; color: var(--primary);
background: #FFF8F3; border-bottom: 2px solid var(--border); text-align: center;
}
.week-table td {
padding: 10px 8px; font-size: 13px; text-align: center;
border-bottom: 1px solid #F5F0EC; vertical-align: top;
}
.week-table tr:hover td { background: #FFFAF7; }
.week-table .day-col { font-weight: 700; color: var(--primary-dark); white-space: nowrap; width: 70px; }
.week-table .day-col.weekend { color: #E53935; }
.dish-name {
display: inline-block; padding: 2px 8px; margin: 2px 1px; border-radius: 6px;
font-size: 12px; white-space: nowrap;
}
.dish-darou { background: #FFEBEE; color: #C62828; }
.dish-xiaorou { background: #FFF3E0; color: #E65100; }
.dish-su { background: #E8F5E9; color: #2E7D32; }
.dish-tang { background: #E3F2FD; color: #1565C0; }
/* Modal */
.modal-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5); z-index: 1000; display: none;
align-items: center; justify-content: center;
}
.modal-overlay.show { display: flex; }
.modal {
background: white; border-radius: 16px; width: 90%; max-width: 700px;
max-height: 85vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.2);
animation: modalIn 0.3s ease;
}
@keyframes modalIn { from { transform: translateY(30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
.modal-header {
padding: 20px 24px; border-bottom: 1px solid var(--border);
display: flex; align-items: center; justify-content: space-between;
position: sticky; top: 0; background: white; z-index: 1; border-radius: 16px 16px 0 0;
}
.modal-header h2 { font-size: 20px; color: var(--primary); }
.modal-close {
width: 36px; height: 36px; border-radius: 50%; border: none; background: #F5F0EC;
font-size: 18px; cursor: pointer; display: flex; align-items: center; justify-content: center;
transition: all 0.2s;
}
.modal-close:hover { background: var(--primary); color: white; }
.modal-body { padding: 24px; }
.meal-section { margin-bottom: 20px; }
.meal-section h4 {
font-size: 16px; margin-bottom: 10px; padding-bottom: 6px;
border-bottom: 2px solid var(--border);
}
.meal-section h4.breakfast { color: #E65100; border-color: #FFE0B2; }
.meal-section h4.lunch { color: #2E7D32; border-color: #C8E6C9; }
.meal-section h4.dinner { color: #1565C0; border-color: #BBDEFB; }
.dish-list { display: flex; flex-wrap: wrap; gap: 8px; }
.dish-item {
padding: 6px 14px; border-radius: 8px; font-size: 14px; font-weight: 500;
}
/* Export area */
.export-section {
background: var(--card); border-radius: var(--radius); padding: 24px;
box-shadow: var(--shadow); display: none;
}
.export-section.show { display: block; }
.export-content {
background: #FAFAFA; border: 1px solid var(--border); border-radius: 8px;
padding: 16px; font-family: 'Courier New', monospace; font-size: 13px;
max-height: 500px; overflow-y: auto; white-space: pre-wrap; line-height: 1.6;
}
.footer {
text-align: center; padding: 24px; color: var(--text-secondary); font-size: 13px;
}
.legend {
display: flex; gap: 16px; flex-wrap: wrap; padding: 12px 20px;
background: var(--card); border-radius: var(--radius); margin-bottom: 16px;
box-shadow: var(--shadow); font-size: 13px;
}
.legend-item { display: flex; align-items: center; gap: 6px; }
.legend-dot {
width: 14px; height: 14px; border-radius: 4px;
}
@media print {
.header, .controls, .tabs, .legend, .footer, .week-nav { display: none !important; }
.week-table { font-size: 11px; }
.week-table td, .week-table th { padding: 4px; }
}
@media (max-width: 768px) {
.container { padding: 12px 8px; }
.header { padding: 16px; }
.header h1 { font-size: 22px; }
.controls { flex-direction: column; align-items: stretch; }
.cal-day { min-height: 70px; }
.cal-day .meal-tag { font-size: 10px; }
.week-table { font-size: 11px; }
.week-table td { padding: 6px 4px; }
}
</style>
</head>
<body>
<div class="header">
<h1>🍱 食堂菜单管理系统</h1>
<p>智能生成每周不重样菜单,支持日历查看与导出</p>
</div>
<div class="container">
<div class="controls">
<label>选择月份:</label>
<input type="month" id="monthPicker" />
<button class="btn btn-primary" onclick="generateMenu()">🎲 生成菜单</button>
<button class="btn btn-outline" onclick="switchView('calendar')">📅 日历视图</button>
<button class="btn btn-outline" onclick="switchView('week')">📋 周视图</button>
<button class="btn btn-green" onclick="showExport()">📥 导出菜单</button>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:#FFEBEE;border:1px solid #C62828"></div> 大荤</div>
<div class="legend-item"><div class="legend-dot" style="background:#FFF3E0;border:1px solid #E65100"></div> 小荤</div>
<div class="legend-item"><div class="legend-dot" style="background:#E8F5E9;border:1px solid #2E7D32"></div> 素菜</div>
<div class="legend-item"><div class="legend-dot" style="background:#E3F2FD;border:1px solid #1565C0"></div></div>
</div>
<div id="calendarView"></div>
<div id="weekView" style="display:none"></div>
<div class="export-section" id="exportSection">
<h3 style="margin-bottom:16px;color:var(--primary)">📥 导出菜单</h3>
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap">
<button class="btn btn-primary" onclick="exportText()">导出文本</button>
<button class="btn btn-outline" onclick="exportCSV()">导出CSV</button>
<button class="btn btn-green" onclick="printMenu()">🖨️ 打印</button>
</div>
<div class="export-content" id="exportContent"></div>
</div>
</div>
<div class="modal-overlay" id="dayModal">
<div class="modal">
<div class="modal-header">
<h2 id="modalTitle">菜单详情</h2>
<button class="modal-close" onclick="closeModal()"></button>
</div>
<div class="modal-body" id="modalBody"></div>
</div>
</div>
<div class="footer">食堂菜单管理系统 © 2026 | 数据本地生成,无需服务器</div>
<script>
// ==================== 菜品库 ====================
const DISHES = {
darou: [
'红烧排骨', '糖醋里脊', '清蒸鲈鱼', '宫保鸡丁', '东坡肉',
'剁椒鱼头', '红烧牛肉', '烤鸭', '水煮肉片', '梅菜扣肉',
'辣子鸡', '回锅肉', '红烧狮子头', '酸菜鱼', '油焖大虾',
'蒜蓉粉丝蒸扇贝', '黑椒牛柳', '啤酒鸭', '粉蒸肉', '红烧猪蹄'
],
xiaorou: [
'番茄炒蛋', '青椒肉丝', '木须肉', '鱼香肉丝', '肉末豆腐',
'蒜苔炒肉', '芹菜炒肉丝', '土豆烧鸡', '蘑菇炒肉', '豆角炒肉',
'西兰花炒肉片', '黄瓜炒鸡丁', '洋葱炒牛肉', '木耳炒肉片', '韭菜炒鸡蛋',
'莴笋炒肉丝', '胡萝卜炒肉丝', '花菜炒肉片', '干煸四季豆炒肉', '香菇滑鸡',
'彩椒炒鸡柳', '冬瓜烧肉丸', '茄子烧肉', '莲藕炒肉片', '白菜炒肉片',
'荷兰豆炒腊肉', '苦瓜炒肉片', '丝瓜炒鸡蛋', '平菇炒肉片', '包菜炒肉片'
],
su: [
'清炒时蔬', '蒜蓉西兰花', '醋溜白菜', '干煸四季豆', '地三鲜',
'麻婆豆腐', '红烧茄子', '蒜蓉菠菜', '清炒豆芽', '凉拌黄瓜',
'虎皮青椒', '素炒丝瓜', '蒜蓉生菜', '炒土豆丝', '素炒冬瓜',
'蒜蓉秋葵', '素炒西葫芦', '清炒芦笋', '白灼菜心', '素炒藕片',
'蒜蓉娃娃菜', '素炒茭白', '清炒山药', '素炒木耳', '蒜蓉空心菜',
'素炒莴笋丝', '清炒油麦菜', '素炒豆苗', '蒜蓉苋菜', '素炒豆角'
],
tang: [
'番茄蛋花汤', '紫菜蛋汤', '冬瓜排骨汤', '玉米排骨汤', '海带豆腐汤',
'莲藕排骨汤', '萝卜牛腩汤', '菌菇鸡汤', '酸辣汤', '南瓜浓汤'
]
};
const MEAL_NAMES = { breakfast: '早餐', lunch: '午餐', dinner: '晚餐' };
const DAY_NAMES = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
const CATEGORIES = [
{ key: 'darou', name: '大荤', count: 2, css: 'dish-darou' },
{ key: 'xiaorou', name: '小荤', count: 3, css: 'dish-xiaorou' },
{ key: 'su', name: '素菜', count: 3, css: 'dish-su' },
{ key: 'tang', name: '汤', count: 1, css: 'dish-tang' }
];
// ==================== 随机工具 ====================
function seededRandom(seed) {
let s = seed;
return function() {
s = (s * 16807 + 0) % 2147483647;
return (s - 1) / 2147483646;
};
}
function shuffleWithSeed(arr, seed) {
const rng = seededRandom(seed);
const result = [...arr];
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(rng() * (i + 1));
[result[i], result[j]] = [result[j], result[i]];
}
return result;
}
// ==================== 菜单生成算法 ====================
// 菜单存储: menuData[year][month][day] = { breakfast: {...}, lunch: {...}, dinner: {...} }
let menuData = {};
let currentYear, currentMonth;
function init() {
const now = new Date();
currentYear = now.getFullYear();
currentMonth = now.getMonth() + 1;
document.getElementById('monthPicker').value = `${currentYear}-${String(currentMonth).padStart(2, '0')}`;
generateMenu();
}
function generateMenu() {
const val = document.getElementById('monthPicker').value;
if (val) {
const [y, m] = val.split('-').map(Number);
currentYear = y;
currentMonth = m;
}
menuData = {};
// 生成当月和下月
for (let offset = 0; offset <= 1; offset++) {
let y = currentYear, m = currentMonth + offset;
if (m > 12) { m = 1; y++; }
generateMonth(y, m);
}
renderCurrentView();
}
function generateMonth(year, month) {
const daysInMonth = new Date(year, month, 0).getDate();
// 找到本月所有周,按周生成
const firstDay = new Date(year, month - 1, 1);
const lastDay = new Date(year, month - 1, daysInMonth);
// 计算本月覆盖的所有周一
let startMonday = new Date(firstDay);
const dow = startMonday.getDay();
const diff = dow === 0 ? -6 : 1 - dow;
startMonday.setDate(startMonday.getDate() + diff);
const weeks = [];
let current = new Date(startMonday);
while (current <= lastDay || current.getMonth() === month - 1) {
const weekStart = new Date(current);
const weekDays = [];
for (let i = 0; i < 7; i++) {
const d = new Date(weekStart);
d.setDate(d.getDate() + i);
if (d.getMonth() === month - 1) {
weekDays.push(d.getDate());
}
}
if (weekDays.length > 0) {
weeks.push({ start: weekStart, days: weekDays });
}
current.setDate(current.getDate() + 7);
if (current.getMonth() > month - 1 + (year !== current.getFullYear() ? 12 : 0) && weekDays.length === 0) break;
if (current.getFullYear() > year && current.getMonth() >= month) break;
}
// 计算全局周编号(用于邻周去重)
const epoch = new Date(2026, 0, 5); // 2026-01-05 is a Monday
weeks.forEach((week, wi) => {
const weekNum = Math.floor((week.start - epoch) / (7 * 86400000));
generateWeek(year, month, week.days, weekNum);
});
}
function generateWeek(year, month, days, weekNum) {
if (!menuData[year]) menuData[year] = {};
if (!menuData[year][month]) menuData[year][month] = {};
const meals = ['breakfast', 'lunch', 'dinner'];
meals.forEach(meal => {
CATEGORIES.forEach(cat => {
const pool = DISHES[cat.key];
const needed = cat.count * 7; // 一周7天每顿所需
// 使用周编号和餐次作为种子,确保可复现且邻周不同
const baseSeed = weekNum * 1000 + meals.indexOf(meal) * 100 + ['darou','xiaorou','su','tang'].indexOf(cat.key);
// 用不同偏移生成多个排列来覆盖需求
// 对于每个餐次我们需要7天的菜品每天cat.count个
const shuffled1 = shuffleWithSeed(pool, baseSeed);
const shuffled2 = shuffleWithSeed(pool, baseSeed + 500);
// 合并两个排列以获得足够的菜品
let allDishes = [...shuffled1];
if (needed > pool.length) {
// 需要复用,但用不同顺序
allDishes = [...shuffled1, ...shuffled2];
}
// 从排列中依次取菜确保一周内不重复在同一个meal+category内
let assigned = [];
for (let d = 0; d < 7; d++) {
const dayDishes = [];
for (let c = 0; c < cat.count; c++) {
const idx = d * cat.count + c;
dayDishes.push(allDishes[idx % allDishes.length]);
}
assigned.push(dayDishes);
}
// 只为本月内的日期赋值
days.forEach((day, di) => {
// di 是在 weekDays 中的索引,但 weekDays 可能不满7天
// 需要找到该日在周中的实际位置
const dayOfWeek = new Date(year, month - 1, day).getDay();
const weekIdx = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // 0=周一...6=周日
if (!menuData[year][month][day]) {
menuData[year][month][day] = { breakfast: {}, lunch: {}, dinner: {} };
}
menuData[year][month][day][meal][cat.key] = assigned[weekIdx];
});
});
});
}
// ==================== 渲染 ====================
let currentView = 'calendar';
let currentWeekOffset = 0;
function switchView(view) {
currentView = view;
document.getElementById('calendarView').style.display = view === 'calendar' ? '' : 'none';
document.getElementById('weekView').style.display = view === 'week' ? '' : 'none';
document.getElementById('exportSection').classList.remove('show');
renderCurrentView();
}
function renderCurrentView() {
if (currentView === 'calendar') renderCalendar();
else renderWeek();
}
function renderCalendar() {
const container = document.getElementById('calendarView');
const y = currentYear, m = currentMonth;
const daysInMonth = new Date(y, m, 0).getDate();
const firstDayOfWeek = new Date(y, m - 1, 1).getDay();
const startOffset = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;
const today = new Date();
const isCurrentMonth = today.getFullYear() === y && today.getMonth() + 1 === m;
let html = '<div class="calendar-grid">';
// 星期头
DAY_NAMES.forEach((d, i) => {
html += `<div class="cal-header${i >= 5 ? ' weekend' : ''}">${d}</div>`;
});
// 空白填充
for (let i = 0; i < startOffset; i++) {
html += '<div class="cal-day empty"></div>';
}
// 日期
for (let d = 1; d <= daysInMonth; d++) {
const dayOfWeek = new Date(y, m - 1, d).getDay();
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
const isToday = isCurrentMonth && today.getDate() === d;
const data = getMenuDay(y, m, d);
let tags = '';
if (data) {
tags = `<div><span class="meal-tag tag-breakfast">早${(data.breakfast.darou||[]).length + (data.breakfast.xiaorou||[]).length + (data.breakfast.su||[]).length + (data.breakfast.tang||[]).length}道</span></div>`;
tags += `<div><span class="meal-tag tag-lunch">午${(data.lunch.darou||[]).length + (data.lunch.xiaorou||[]).length + (data.lunch.su||[]).length + (data.lunch.tang||[]).length}道</span></div>`;
tags += `<div><span class="meal-tag tag-dinner">晚${(data.dinner.darou||[]).length + (data.dinner.xiaorou||[]).length + (data.dinner.su||[]).length + (data.dinner.tang||[]).length}道</span></div>`;
}
html += `<div class="cal-day${isWeekend ? ' weekend' : ''}${isToday ? ' today' : ''}" onclick="showDayDetail(${y},${m},${d})">
<div class="day-num">${d}</div>${tags}
</div>`;
}
html += '</div>';
container.innerHTML = html;
}
function renderWeek() {
const container = document.getElementById('weekView');
const y = currentYear, m = currentMonth;
const daysInMonth = new Date(y, m, 0).getDate();
const firstDay = new Date(y, m - 1, 1);
const lastDay = new Date(y, m - 1, daysInMonth);
// 计算周列表
const weeks = [];
let startMonday = new Date(firstDay);
const dow = startMonday.getDay();
const diff = dow === 0 ? -6 : 1 - dow;
startMonday.setDate(startMonday.getDate() + diff);
let cur = new Date(startMonday);
while (cur <= lastDay || (cur.getMonth() === m - 1)) {
const ws = new Date(cur);
const weekDates = [];
for (let i = 0; i < 7; i++) {
const d = new Date(ws);
d.setDate(d.getDate() + i);
if (d.getMonth() === m - 1 && d.getFullYear() === y) {
weekDates.push(d.getDate());
} else {
weekDates.push(null);
}
}
if (weekDates.some(d => d !== null)) {
weeks.push({ start: ws, dates: weekDates });
}
cur.setDate(cur.getDate() + 7);
if (cur.getFullYear() > y || (cur.getFullYear() === y && cur.getMonth() > m - 1)) break;
}
if (currentWeekOffset < 0) currentWeekOffset = 0;
if (currentWeekOffset >= weeks.length) currentWeekOffset = weeks.length - 1;
const week = weeks[currentWeekOffset];
if (!week) { container.innerHTML = '<p style="text-align:center;padding:40px">暂无数据</p>'; return; }
const startDate = week.start;
const endDate = new Date(startDate);
endDate.setDate(endDate.getDate() + 6);
function fmt(d) { return `${d.getMonth()+1}/${d.getDate()}`; }
let html = `<div class="week-view">
<div class="week-nav">
<button class="btn btn-outline" onclick="currentWeekOffset--;renderWeek()">◀ 上一周</button>
<h3>${fmt(startDate)} ~ ${fmt(endDate)}(第${currentWeekOffset+1}周)</h3>
<button class="btn btn-outline" onclick="currentWeekOffset++;renderWeek()">下一周 ▶</button>
</div>
<div style="overflow-x:auto">
<table class="week-table">
<tr><th>餐次/类别</th>`;
for (let i = 0; i < 7; i++) {
const isWe = i >= 5;
html += `<th class="${isWe ? 'weekend' : ''}">${DAY_NAMES[i]}<br/>`;
if (week.dates[i]) {
html += `<span style="font-weight:400;font-size:11px">${m}/${week.dates[i]}</span>`;
}
html += '</th>';
}
html += '</tr>';
['breakfast', 'lunch', 'dinner'].forEach(meal => {
CATEGORIES.forEach(cat => {
const catLabel = cat.name;
html += `<tr><td style="font-weight:600;white-space:nowrap;color:var(--text-secondary)">${MEAL_NAMES[meal]}·${catLabel}</td>`;
for (let di = 0; di < 7; di++) {
const day = week.dates[di];
if (day) {
const data = getMenuDay(y, m, day);
if (data && data[meal] && data[meal][cat.key]) {
const dishes = data[meal][cat.key].map(d =>
`<span class="dish-name ${cat.css}">${d}</span>`
).join(' ');
html += `<td>${dishes}</td>`;
} else {
html += '<td>-</td>';
}
} else {
html += '<td style="color:#ccc">-</td>';
}
}
html += '</tr>';
});
});
html += '</table></div></div>';
container.innerHTML = html;
}
function getMenuDay(y, m, d) {
if (menuData[y] && menuData[y][m] && menuData[y][m][d]) return menuData[y][m][d];
return null;
}
// ==================== 日详情弹窗 ====================
function showDayDetail(y, m, d) {
const data = getMenuDay(y, m, d);
if (!data) return;
document.getElementById('modalTitle').textContent = `${y}${m}${d}日 菜单`;
let html = '';
['breakfast', 'lunch', 'dinner'].forEach(meal => {
const mealClass = meal === 'breakfast' ? 'breakfast' : meal === 'lunch' ? 'lunch' : 'dinner';
html += `<div class="meal-section"><h4 class="${mealClass}">🍳 ${MEAL_NAMES[meal]}</h4><div class="dish-list">`;
CATEGORIES.forEach(cat => {
if (data[meal][cat.key]) {
data[meal][cat.key].forEach(dish => {
html += `<span class="dish-item ${cat.css}">${dish}</span>`;
});
}
});
html += '</div></div>';
});
document.getElementById('modalBody').innerHTML = html;
document.getElementById('dayModal').classList.add('show');
}
function closeModal() {
document.getElementById('dayModal').classList.remove('show');
}
document.getElementById('dayModal').addEventListener('click', function(e) {
if (e.target === this) closeModal();
});
// ==================== 导出 ====================
function showExport() {
document.getElementById('exportSection').classList.add('show');
exportText();
}
function exportText() {
const y = currentYear, m = currentMonth;
const nextY = m === 12 ? y + 1 : y;
const nextM = m === 12 ? 1 : m + 1;
let text = `═══════════════════════════════════════\n`;
text += ` 食堂菜单 (${y}${m}月 ~ ${nextY}${nextM}月)\n`;
text += `═══════════════════════════════════════\n\n`;
[[y, m], [nextY, nextM]].forEach(([yy, mm]) => {
const days = new Date(yy, mm, 0).getDate();
text += `──── ${yy}${mm}月 ────\n\n`;
for (let d = 1; d <= days; d++) {
const data = getMenuDay(yy, mm, d);
const dow = new Date(yy, mm - 1, d).getDay();
const dayName = DAY_NAMES[dow === 0 ? 6 : dow - 1];
text += `${mm}${d}${dayName}\n`;
if (data) {
['breakfast', 'lunch', 'dinner'].forEach(meal => {
const items = [];
CATEGORIES.forEach(cat => {
if (data[meal][cat.key]) {
items.push(`${cat.name}: ${data[meal][cat.key].join('、')}`);
}
});
text += ` ${MEAL_NAMES[meal]} | ${items.join(' | ')}\n`;
});
}
text += '\n';
}
});
document.getElementById('exportContent').textContent = text;
}
function exportCSV() {
const y = currentYear, m = currentMonth;
const nextY = m === 12 ? y + 1 : y;
const nextM = m === 12 ? 1 : m + 1;
let csv = '\uFEFF日期,星期,餐次,大荤1,大荤2,小荤1,小荤2,小荤3,素菜1,素菜2,素菜3,汤\n';
[[y, m], [nextY, nextM]].forEach(([yy, mm]) => {
const days = new Date(yy, mm, 0).getDate();
for (let d = 1; d <= days; d++) {
const data = getMenuDay(yy, mm, d);
const dow = new Date(yy, mm - 1, d).getDay();
const dayName = DAY_NAMES[dow === 0 ? 6 : dow - 1];
const dateStr = `${yy}-${String(mm).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
if (data) {
['breakfast', 'lunch', 'dinner'].forEach(meal => {
const row = [dateStr, dayName, MEAL_NAMES[meal]];
CATEGORIES.forEach(cat => {
const dishes = data[meal][cat.key] || [];
for (let i = 0; i < cat.count; i++) {
row.push(dishes[i] || '');
}
});
csv += row.join(',') + '\n';
});
}
}
});
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `食堂菜单_${y}${m}月.csv`;
link.click();
document.getElementById('exportContent').textContent = 'CSV文件已下载';
}
function printMenu() {
// 切换到周视图并打印
const current = currentView;
currentWeekOffset = 0;
switchView('week');
setTimeout(() => {
window.print();
}, 300);
}
// ==================== 初始化 ====================
init();
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
打开文件夹-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
打开文件夹-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB