Compare commits

..

1 Commits

Author SHA1 Message Date
hanhan 76ba61d5bb fix(1.0.8)修复构建bug:修复适配餐点类型 2026-07-03 21:58:23 +08:00
2 changed files with 201 additions and 194 deletions
+197 -194
View File
@@ -4,103 +4,103 @@ import * as cheerio from "cheerio";
// 餐食类型 // 餐食类型
enum MealTypeCode { enum MealTypeCode {
BREAKFAST = "breakfast", BREAKFAST = "breakfast",
FRUIT = "fruit", FRUIT = "fruit",
LUNCH = "lunch", LUNCH = "lunch",
SNACK = "snack", SNACK = "snack",
SPECIAL = "special", SPECIAL = "special",
} }
// 语言名称项 // 语言名称项
interface LangName { interface LangName {
zh: string; zh: string;
en?: string; en?: string;
} }
// 单个菜品 // 单个菜品
interface Dish { interface Dish {
id: string; id: string;
name: LangName; name: LangName;
} }
// 单日食谱 // 单日食谱
interface DailyMeal { interface DailyMeal {
dayCode: number; dayCode: number;
mealCode: MealTypeCode; mealCode: MealTypeCode;
dishList: Dish[]; dishList: Dish[];
} }
// 一周食谱 // 一周食谱
interface WeeklyMenu { interface WeeklyMenu {
menuId: string; menuId: string;
weekNumber: string; weekNumber: string;
title: LangName; title: LangName;
days: { code: number; name: LangName }[]; days: { code: number; name: LangName }[];
mealCategories: { code: MealTypeCode; name: LangName; sort: number }[]; mealCategories: { code: MealTypeCode; name: LangName; sort: number }[];
meals: DailyMeal[]; meals: DailyMeal[];
} }
// 餐食类型配置 // 餐食类型配置
const MEAL_TYPES = [ const MEAL_TYPES = [
{ code: MealTypeCode.BREAKFAST, label: "早餐" }, {code: MealTypeCode.BREAKFAST, label: "早餐"},
{ code: MealTypeCode.FRUIT, label: "早点/水果餐" }, {code: MealTypeCode.FRUIT, label: "早点/水果餐"},
{ code: MealTypeCode.LUNCH, label: "中餐" }, {code: MealTypeCode.LUNCH, label: "中餐"},
{ code: MealTypeCode.SPECIAL, label: "体弱儿餐" }, {code: MealTypeCode.SPECIAL, label: "体弱儿餐"},
{ code: MealTypeCode.SNACK, label: "午点" }, {code: MealTypeCode.SNACK, label: "午点"},
]; ];
const WEEK_DAYS = [ const WEEK_DAYS = [
{ code: 1, label: "周一" }, {code: 1, label: "周一"},
{ code: 2, label: "周二" }, {code: 2, label: "周二"},
{ code: 3, label: "周三" }, {code: 3, label: "周三"},
{ code: 4, label: "周四" }, {code: 4, label: "周四"},
{ code: 5, label: "周五" }, {code: 5, label: "周五"},
]; ];
/** /**
* 使用 Cheerio 从 HTML 中提取表格原始数据 * 使用 Cheerio 从 HTML 中提取表格原始数据
*/ */
function extractRawDataFromHtml(html: string): string[][][] { function extractRawDataFromHtml(html: string): string[][][] {
const tableData: string[][][] = []; const tableData: string[][][] = [];
const $ = cheerio.load(html); const $ = cheerio.load(html);
const rows = $("table tr"); const rows = $("table tr");
rows.each((rowIndex, row) => { rows.each((rowIndex, row) => {
const rowData: string[][] = []; const rowData: string[][] = [];
const cells = $(row).find("td"); const cells = $(row).find("td");
cells.each((cellIndex, cell) => { cells.each((cellIndex, cell) => {
// 直接获取 HTML 并处理 // 直接获取 HTML 并处理
const rawHtml = $(cell).html() || ""; const rawHtml = $(cell).html() || "";
// 替换换行标签为真实换行符 // 替换换行标签为真实换行符
const processed = rawHtml const processed = rawHtml
.replace(/<\/p><p[^>]*>/g, "\n") // </p><p> → 换行 .replace(/<\/p><p[^>]*>/g, "\n") // </p><p> → 换行
.replace(/<br\s*\/?>/gi, "\n") .replace(/<br\s*\/?>/gi, "\n")
.replace(/&nbsp;/g, " "); .replace(/&nbsp;/g, " ");
// 移除剩余 HTML 标签 // 移除剩余 HTML 标签
const text = processed.replace(/<[^>]+>/g, ""); 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[] = []; const finalItems: string[] = [];
lines.forEach((line) => { lines.forEach((line) => {
finalItems.push(...splitCellContent(line)); finalItems.push(...splitCellContent(line));
}); });
rowData.push(finalItems); rowData.push(finalItems);
});
if (rowData.length > 0) {
tableData.push(rowData);
}
}); });
if (rowData.length > 0) { return tableData;
tableData.push(rowData);
}
});
return tableData;
} }
/** /**
@@ -108,173 +108,176 @@ function extractRawDataFromHtml(html: string): string[][][] {
* 支持分隔符:顿号(、)、逗号(,)、分号(;) * 支持分隔符:顿号(、)、逗号(,)、分号(;)
*/ */
function splitCellContent(content: string): string[] { function splitCellContent(content: string): string[] {
if (!content || content.trim() === "") return []; if (!content || content.trim() === "") return [];
// 统一分隔符 // 统一分隔符
const normalized = content const normalized = content
.replace(//g, "|") .replace(//g, "|")
.replace(/,/g, "|") .replace(/,/g, "|")
.replace(/、/g, "|") .replace(/、/g, "|")
.trim(); .trim();
const parts = normalized.split("|"); const parts = normalized.split("|");
return parts return parts
.map((item) => item.trim()) .map((item) => item.trim())
.filter(Boolean); .filter(Boolean);
} }
/** /**
* 确定餐食类型 * 确定餐食类型
*/ */
function getMealType(mealName: string): MealTypeCode | null { function getMealType(mealName: string): MealTypeCode | null {
if (mealName.includes("早") && !mealName.includes("早点")) return MealTypeCode.BREAKFAST; const header = mealName.trim();
if (mealName.includes("水果") || mealName.includes("早点")) return MealTypeCode.FRUIT;
if (mealName.includes("") || mealName.includes("餐")) return MealTypeCode.LUNCH; if (header.includes("早餐") || header.includes("餐")) return MealTypeCode.BREAKFAST;
if (mealName.includes("点") || mealName.includes("点心")) return MealTypeCode.SNACK; if (header.includes("点") || header.includes("水果餐") || header.includes("早 点") || header.includes("早点/水果餐")) return MealTypeCode.FRUIT;
if (mealName.includes("体弱") || mealName.includes("")) return MealTypeCode.SPECIAL; if (header.includes("中餐") || header.includes("中 餐")) return MealTypeCode.LUNCH;
return null; if (header.includes("午点") || header.includes("午 点")) return MealTypeCode.SNACK;
if (header.includes("体弱儿餐") || header.includes("体弱儿")) return MealTypeCode.SPECIAL;
return null;
} }
/** /**
* 从表格数据转换为一周食谱结构 * 从表格数据转换为一周食谱结构
*/ */
function transformTableToWeeklyMenu(tableData: string[][][]): WeeklyMenu { function transformTableToWeeklyMenu(tableData: string[][][]): WeeklyMenu {
const meals: DailyMeal[] = []; const meals: DailyMeal[] = [];
// 校验表格数据完整性 // 校验表格数据完整性
if (tableData.length < 2 || tableData[0].length < 2) { if (tableData.length < 2 || tableData[0].length < 2) {
console.warn("警告:表格数据不完整"); console.warn("警告:表格数据不完整");
return createEmptyMenu(); 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++) { for (let dayIndex = 0; dayIndex < weekdays.length; dayIndex++) {
const dayName = weekdays[dayIndex]; const dayName = weekdays[dayIndex];
if (!dayName) continue; if (!dayName) continue;
const columnIndex = dayIndex + 1; // 对应星期几的列索引 const columnIndex = dayIndex + 1; // 对应星期几的列索引
// 遍历每一行餐型 // 遍历每一行餐型
for (const row of mealTypeRows) { for (const row of mealTypeRows) {
const mealName = row[0]?.[0] || ""; // 餐型名称 const mealName = row[0]?.[0] || ""; // 餐型名称
if (!mealName) continue; if (!mealName) continue;
const mealType = getMealType(mealName); const mealType = getMealType(mealName);
if (!mealType) continue; if (!mealType) continue;
// 获取当前餐型的菜品列表 // 获取当前餐型的菜品列表
const mealItems = row[columnIndex] || []; const mealItems = row[columnIndex] || [];
// 添加到当天餐食列表 // 添加到当天餐食列表
meals.push({ meals.push({
dayCode: dayIndex + 1, dayCode: dayIndex + 1,
mealCode: mealType, mealCode: mealType,
dishList: mealItems.map((item, idx) => ({ dishList: mealItems.map((item, idx) => ({
id: `${dayIndex + 1}-${mealType}-${idx + 1}`, id: `${dayIndex + 1}-${mealType}-${idx + 1}`,
name: { zh: item }, 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, number> = {
[MealTypeCode.BREAKFAST]: 0,
// 确保每天每餐都有记录 [MealTypeCode.FRUIT]: 1,
for (const day of WEEK_DAYS) { [MealTypeCode.LUNCH]: 2,
for (const meal of MEAL_TYPES) { [MealTypeCode.SPECIAL]: 3,
const exists = meals.find( [MealTypeCode.SNACK]: 4,
(m) => m.dayCode === day.code && m.mealCode === meal.code };
); return orderMap[a.mealCode] - orderMap[b.mealCode];
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, number> = {
[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 { function createEmptyMenu(): WeeklyMenu {
return { return {
menuId: `menu-${Date.now()}`, menuId: `menu-${Date.now()}`,
weekNumber: "", weekNumber: "",
title: { zh: "食谱", en: "Recipe" }, title: {zh: "食谱", en: "Recipe"},
days: WEEK_DAYS.map((d) => ({ code: d.code, name: { zh: d.label } })), days: WEEK_DAYS.map((d) => ({code: d.code, name: {zh: d.label}})),
mealCategories: MEAL_TYPES.map((m, i) => ({ mealCategories: MEAL_TYPES.map((m, i) => ({
code: m.code, code: m.code,
name: { zh: m.label }, name: {zh: m.label},
sort: i + 1, sort: i + 1,
})), })),
meals: [], meals: [],
}; };
} }
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const formData = await request.formData(); const formData = await request.formData();
const file = formData.get("file") as File | null; const file = formData.get("file") as File | null;
if (!file) { if (!file) {
return NextResponse.json({ error: "未上传文件" }, { status: 400 }); 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}
);
} }
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 }
);
}
} }
+4
View File
@@ -1,3 +1,7 @@
allowBuilds:
msw: true
sharp: true
unrs-resolver: true
ignoredBuiltDependencies: ignoredBuiltDependencies:
- sharp - sharp
- unrs-resolver - unrs-resolver