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("早") && !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; } /** * 从表格数据转换为一周食谱结构 */ 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 = { [MealTypeCode.BREAKFAST]: 0, [MealTypeCode.FRUIT]: 1, [MealTypeCode.LUNCH]: 2, [MealTypeCode.SPECIAL]: 3, [MealTypeCode.SNACK]: 4, }; return orderMap[a.mealCode] - orderMap[b.mealCode]; }), }; } function createEmptyMenu(): WeeklyMenu { return { menuId: `menu-${Date.now()}`, weekNumber: "", title: { zh: "食谱", en: "Recipe" }, days: WEEK_DAYS.map((d) => ({ code: d.code, name: { zh: d.label } })), mealCategories: MEAL_TYPES.map((m, i) => ({ code: m.code, name: { zh: m.label }, sort: i + 1, })), meals: [], }; } export async function POST(request: NextRequest) { try { const formData = await request.formData(); const file = formData.get("file") as File | null; if (!file) { return NextResponse.json({ error: "未上传文件" }, { status: 400 }); } if (!file.name.endsWith(".docx")) { return NextResponse.json({ error: "仅支持 .docx 格式" }, { status: 400 }); } // 读取文件为 ArrayBuffer const arrayBuffer = await file.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); // 使用 mammoth 转换为 HTML(保留表格结构) const { value: htmlContent } = await mammoth.convertToHtml({ buffer }); // 使用 cheerio 提取表格数据 const tableData = extractRawDataFromHtml(htmlContent); // 转换为结构化数据 const weeklyMenu = transformTableToWeeklyMenu(tableData); return NextResponse.json({ success: true, data: weeklyMenu, }); } catch (error) { console.error("Parse error:", error); return NextResponse.json( { error: "文件解析失败", details: String(error) }, { status: 500 } ); } }