Files
tmp/index.html

714 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>