import { NextRequest, NextResponse } from "next/server"; import mammoth from "mammoth"; import * as cheerio from "cheerio"; // 餐食类型 enum MealTypeCode { BREAKFAST = "breakfast", FRUIT = "fruit", LUNCH = "lunch", SNACK = "snack", SPECIAL = "special", } // 语言名称项 interface LangName { zh: string; en?: string; } // 单个菜品 interface Dish { id: string; name: LangName; } // 单日食谱 interface DailyMeal { dayCode: number; mealCode: MealTypeCode; dishList: Dish[]; } // 一周食谱 interface WeeklyMenu { menuId: string; weekNumber: string; title: LangName; days: { code: number; name: LangName }[]; mealCategories: { code: MealTypeCode; name: LangName; sort: number }[]; meals: DailyMeal[]; } // 餐食类型配置 const MEAL_TYPES = [ { code: MealTypeCode.BREAKFAST, label: "早餐" }, { code: MealTypeCode.FRUIT, label: "水果餐" }, { code: MealTypeCode.LUNCH, label: "中餐" }, { code: MealTypeCode.SPECIAL, label: "体弱儿专属" }, { code: MealTypeCode.SNACK, label: "午点" }, ]; const WEEK_DAYS = [ { code: 1, label: "周一" }, { code: 2, label: "周二" }, { code: 3, label: "周三" }, { code: 4, label: "周四" }, { code: 5, label: "周五" }, ]; /** * 使用 Cheerio 从 HTML 中提取表格原始数据 */ function extractRawDataFromHtml(html: string): string[][][] { const tableData: string[][][] = []; const $ = cheerio.load(html); const rows = $("table tr"); rows.each((rowIndex, row) => { const rowData: string[][] = []; const cells = $(row).find("td"); cells.each((cellIndex, cell) => { // 直接获取 HTML 并处理 const rawHtml = $(cell).html() || ""; // 替换换行标签为真实换行符 const processed = rawHtml .replace(/<\/p>
]*>/g, "\n") //
→ 换行
.replace(/
/gi, "\n")
.replace(/ /g, " ");
// 移除剩余 HTML 标签
const text = processed.replace(/<[^>]+>/g, "");
// 按换行分割
const lines = text.split("\n").filter((line) => line.trim() !== "");
// 对每行应用顿号/逗号/分号分割
const finalItems: string[] = [];
lines.forEach((line) => {
finalItems.push(...splitCellContent(line));
});
rowData.push(finalItems);
});
if (rowData.length > 0) {
tableData.push(rowData);
}
});
return tableData;
}
/**
* 分割单元格内容为菜品列表
* 支持分隔符:顿号(、)、逗号(,)、分号(;)
*/
function splitCellContent(content: string): string[] {
if (!content || content.trim() === "") return [];
// 统一分隔符
const normalized = content
.replace(/;/g, "|")
.replace(/,/g, "|")
.replace(/、/g, "|")
.trim();
const parts = normalized.split("|");
return parts
.map((item) => item.trim())
.filter(Boolean);
}
/**
* 确定餐食类型
*/
function getMealType(mealName: string): MealTypeCode | null {
if (mealName.includes("早")) return MealTypeCode.BREAKFAST;
if (mealName.includes("水果")) return MealTypeCode.FRUIT;
if (mealName.includes("中") || mealName.includes("午餐")) return MealTypeCode.LUNCH;
if (mealName.includes("午点") || mealName.includes("点心")) return MealTypeCode.SNACK;
if (mealName.includes("体弱") || mealName.includes("晚")) return MealTypeCode.SPECIAL;
return null;
}
/**
* 从表格数据转换为一周食谱结构
*/
function transformTableToWeeklyMenu(tableData: string[][][]): WeeklyMenu {
const meals: DailyMeal[] = [];
// 校验表格数据完整性
if (tableData.length < 2 || tableData[0].length < 2) {
console.warn("警告:表格数据不完整");
return createEmptyMenu();
}
// 提取表头数据(星期几)
const weekdays = tableData[0].slice(1).map((cell) => cell[0] || "");
// 提取餐型行数据(从第二行开始)
const mealTypeRows = tableData.slice(1);
// 遍历每一天(列)
for (let dayIndex = 0; dayIndex < weekdays.length; dayIndex++) {
const dayName = weekdays[dayIndex];
if (!dayName) continue;
const columnIndex = dayIndex + 1; // 对应星期几的列索引
// 遍历每一行餐型
for (const row of mealTypeRows) {
const mealName = row[0]?.[0] || ""; // 餐型名称
if (!mealName) continue;
const mealType = getMealType(mealName);
if (!mealType) continue;
// 获取当前餐型的菜品列表
const mealItems = row[columnIndex] || [];
// 添加到当天餐食列表
meals.push({
dayCode: dayIndex + 1,
mealCode: mealType,
dishList: mealItems.map((item, idx) => ({
id: `${dayIndex + 1}-${mealType}-${idx + 1}`,
name: { zh: item },
})),
});
}
}
// 确保每天每餐都有记录
for (const day of WEEK_DAYS) {
for (const meal of MEAL_TYPES) {
const exists = meals.find(
(m) => m.dayCode === day.code && m.mealCode === meal.code
);
if (!exists) {
meals.push({ dayCode: day.code, mealCode: meal.code, dishList: [] });
}
}
}
// 构建 days 数组,使用实际解析出的星期
const days = weekdays
.filter(Boolean)
.map((name, index) => ({ code: index + 1, name: { zh: name } }));
return {
menuId: `menu-${Date.now()}`,
weekNumber: "",
title: { zh: "食谱", en: "Recipe" },
days: days,
mealCategories: MEAL_TYPES.map((m, i) => ({
code: m.code,
name: { zh: m.label },
sort: i + 1,
})),
meals: meals.sort((a, b) => {
if (a.dayCode !== b.dayCode) return a.dayCode - b.dayCode;
const orderMap: Record