diff --git a/app/api/recipe/route.ts b/app/api/recipe/route.ts index 54af381..87302c8 100644 --- a/app/api/recipe/route.ts +++ b/app/api/recipe/route.ts @@ -4,103 +4,103 @@ import * as cheerio from "cheerio"; // 餐食类型 enum MealTypeCode { - BREAKFAST = "breakfast", - FRUIT = "fruit", - LUNCH = "lunch", - SNACK = "snack", - SPECIAL = "special", + BREAKFAST = "breakfast", + FRUIT = "fruit", + LUNCH = "lunch", + SNACK = "snack", + SPECIAL = "special", } // 语言名称项 interface LangName { - zh: string; - en?: string; + zh: string; + en?: string; } // 单个菜品 interface Dish { - id: string; - name: LangName; + id: string; + name: LangName; } // 单日食谱 interface DailyMeal { - dayCode: number; - mealCode: MealTypeCode; - dishList: Dish[]; + 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[]; + 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: "午点" }, + {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: "周五" }, + {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 tableData: string[][][] = []; + const $ = cheerio.load(html); - const rows = $("table tr"); + const rows = $("table tr"); - rows.each((rowIndex, row) => { - const rowData: string[][] = []; - const cells = $(row).find("td"); + rows.each((rowIndex, row) => { + const rowData: string[][] = []; + const cells = $(row).find("td"); - cells.each((cellIndex, cell) => { - // 直接获取 HTML 并处理 - const rawHtml = $(cell).html() || ""; + cells.each((cellIndex, cell) => { + // 直接获取 HTML 并处理 + const rawHtml = $(cell).html() || ""; - // 替换换行标签为真实换行符 - const processed = rawHtml - .replace(/<\/p>
]*>/g, "\n") //
→ 换行
- .replace(/
/gi, "\n")
- .replace(/ /g, " ");
+ // 替换换行标签为真实换行符
+ const processed = rawHtml
+ .replace(/<\/p>
]*>/g, "\n") //
→ 换行
+ .replace(/
/gi, "\n")
+ .replace(/ /g, " ");
- // 移除剩余 HTML 标签
- const text = processed.replace(/<[^>]+>/g, "");
+ // 移除剩余 HTML 标签
+ const text = processed.replace(/<[^>]+>/g, "");
- // 按换行分割
- const lines = text.split("\n").filter((line) => line.trim() !== "");
+ // 按换行分割
+ const lines = text.split("\n").filter((line) => line.trim() !== "");
- // 对每行应用顿号/逗号/分号分割
- const finalItems: string[] = [];
- lines.forEach((line) => {
- finalItems.push(...splitCellContent(line));
- });
+ // 对每行应用顿号/逗号/分号分割
+ const finalItems: string[] = [];
+ lines.forEach((line) => {
+ finalItems.push(...splitCellContent(line));
+ });
- rowData.push(finalItems);
+ rowData.push(finalItems);
+ });
+
+ if (rowData.length > 0) {
+ tableData.push(rowData);
+ }
});
- if (rowData.length > 0) {
- tableData.push(rowData);
- }
- });
-
- return tableData;
+ return tableData;
}
/**
@@ -108,173 +108,176 @@ function extractRawDataFromHtml(html: string): string[][][] {
* 支持分隔符:顿号(、)、逗号(,)、分号(;)
*/
function splitCellContent(content: string): string[] {
- if (!content || content.trim() === "") return [];
+ if (!content || content.trim() === "") return [];
- // 统一分隔符
- const normalized = content
- .replace(/;/g, "|")
- .replace(/,/g, "|")
- .replace(/、/g, "|")
- .trim();
+ // 统一分隔符
+ const normalized = content
+ .replace(/;/g, "|")
+ .replace(/,/g, "|")
+ .replace(/、/g, "|")
+ .trim();
- const parts = normalized.split("|");
+ const parts = normalized.split("|");
- return parts
- .map((item) => item.trim())
- .filter(Boolean);
+ return parts
+ .map((item) => item.trim())
+ .filter(Boolean);
}
/**
* 确定餐食类型
*/
function getMealType(mealName: string): MealTypeCode | null {
- if (mealName.includes("早") && !mealName.includes("早点")) return MealTypeCode.BREAKFAST;
- if (mealName.includes("水果") || 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;
+ const header = mealName.trim();
+
+ if (header.includes("早餐") || header.includes("早 餐")) return MealTypeCode.BREAKFAST;
+ if (header.includes("早点") || header.includes("水果餐") || header.includes("早 点") || header.includes("早点/水果餐")) return MealTypeCode.FRUIT;
+ if (header.includes("中餐") || header.includes("中 餐")) return MealTypeCode.LUNCH;
+ if (header.includes("午点") || header.includes("午 点")) return MealTypeCode.SNACK;
+ if (header.includes("体弱儿餐") || header.includes("体弱儿")) return MealTypeCode.SPECIAL;
+
+ return null;
}
/**
* 从表格数据转换为一周食谱结构
*/
function transformTableToWeeklyMenu(tableData: string[][][]): WeeklyMenu {
- const meals: DailyMeal[] = [];
+ const meals: DailyMeal[] = [];
- // 校验表格数据完整性
- if (tableData.length < 2 || tableData[0].length < 2) {
- console.warn("警告:表格数据不完整");
- return createEmptyMenu();
- }
+ // 校验表格数据完整性
+ if (tableData.length < 2 || tableData[0].length < 2) {
+ console.warn("警告:表格数据不完整");
+ return createEmptyMenu();
+ }
- // 提取表头数据(星期几)
- const weekdays = tableData[0].slice(1).map((cell) => cell[0] || "");
+ // 提取表头数据(星期几)
+ const weekdays = tableData[0].slice(1).map((cell) => cell[0] || "");
- // 提取餐型行数据(从第二行开始)
- const mealTypeRows = tableData.slice(1);
+ // 提取餐型行数据(从第二行开始)
+ const mealTypeRows = tableData.slice(1);
- // 遍历每一天(列)
- for (let dayIndex = 0; dayIndex < weekdays.length; dayIndex++) {
- const dayName = weekdays[dayIndex];
- if (!dayName) continue;
+ // 遍历每一天(列)
+ for (let dayIndex = 0; dayIndex < weekdays.length; dayIndex++) {
+ const dayName = weekdays[dayIndex];
+ if (!dayName) continue;
- const columnIndex = dayIndex + 1; // 对应星期几的列索引
+ const columnIndex = dayIndex + 1; // 对应星期几的列索引
- // 遍历每一行餐型
- for (const row of mealTypeRows) {
- const mealName = row[0]?.[0] || ""; // 餐型名称
- if (!mealName) continue;
+ // 遍历每一行餐型
+ for (const row of mealTypeRows) {
+ const mealName = row[0]?.[0] || ""; // 餐型名称
+ if (!mealName) continue;
- const mealType = getMealType(mealName);
- if (!mealType) continue;
+ const mealType = getMealType(mealName);
+ if (!mealType) continue;
- // 获取当前餐型的菜品列表
- const mealItems = row[columnIndex] || [];
+ // 获取当前餐型的菜品列表
+ 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 },
+ // 添加到当天餐食列表
+ 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,
})),
- });
- }
- }
-
- // 确保每天每餐都有记录
- 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