From 8b308b6adb3600faca23046d6e675017f87f2fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AF=92=E5=AF=92?= <2596194220@qq.com> Date: Sun, 24 May 2026 21:45:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=A4=90=E9=A3=9F=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=8B=96=E6=8B=BD=E6=8E=92=E5=BA=8F=EF=BC=8C?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=8E=92=E5=88=97=E9=A1=BA=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 改动了餐食类型标签(水果餐→早点/水果餐、体弱儿专属→体弱儿餐)及排序(体弱儿餐在午点前); 筛选按钮改为可拖拽排序,影响表格和导出顺序; 修复复合标签(早点/水果餐)导出无英文翻译的bug; 排序结果持久化到localStorage --- app/api/recipe/route.ts | 8 +-- app/page.tsx | 123 +++++++++++++++++++++++++++++++++++----- 2 files changed, 113 insertions(+), 18 deletions(-) diff --git a/app/api/recipe/route.ts b/app/api/recipe/route.ts index f74c6bf..54af381 100644 --- a/app/api/recipe/route.ts +++ b/app/api/recipe/route.ts @@ -43,9 +43,9 @@ interface WeeklyMenu { // 餐食类型配置 const MEAL_TYPES = [ { code: MealTypeCode.BREAKFAST, label: "早餐" }, - { code: MealTypeCode.FRUIT, label: "水果餐" }, + { code: MealTypeCode.FRUIT, label: "早点/水果餐" }, { code: MealTypeCode.LUNCH, label: "中餐" }, - { code: MealTypeCode.SPECIAL, label: "体弱儿专属" }, + { code: MealTypeCode.SPECIAL, label: "体弱儿餐" }, { code: MealTypeCode.SNACK, label: "午点" }, ]; @@ -217,8 +217,8 @@ function transformTableToWeeklyMenu(tableData: string[][][]): WeeklyMenu { [MealTypeCode.BREAKFAST]: 0, [MealTypeCode.FRUIT]: 1, [MealTypeCode.LUNCH]: 2, - [MealTypeCode.SNACK]: 3, - [MealTypeCode.SPECIAL]: 4, + [MealTypeCode.SPECIAL]: 3, + [MealTypeCode.SNACK]: 4, }; return orderMap[a.mealCode] - orderMap[b.mealCode]; }), diff --git a/app/page.tsx b/app/page.tsx index ae4aae9..945ce64 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,6 @@ "use client"; -import {useCallback, useEffect, useState} from "react"; +import {useCallback, useEffect, useMemo, useState} from "react"; import { AlertCircle, Apple, @@ -74,10 +74,10 @@ interface WeeklyMenu { // 餐食类型配置 const MEAL_TYPES = [ {code: MealTypeCode.BREAKFAST, label: "早餐", icon: Sun}, - {code: MealTypeCode.FRUIT, label: "水果餐", icon: Apple}, + {code: MealTypeCode.FRUIT, label: "早点/水果餐", icon: Apple}, {code: MealTypeCode.LUNCH, label: "中餐", icon: UtensilsCrossed}, - {code: MealTypeCode.SNACK, label: "午点", icon: Cookie}, {code: MealTypeCode.SPECIAL, label: "体弱儿餐", icon: Heart}, + {code: MealTypeCode.SNACK, label: "午点", icon: Cookie}, ]; type UploadState = "idle" | "uploading" | "parsing" | "done" | "error"; @@ -127,6 +127,69 @@ export default function RecipeTranslationPage() { orientation: "landscape", }); + // 餐食类型拖拽排序(持久化到 localStorage) + const [mealOrder, setMealOrder] = useState(() => { + try { + const saved = localStorage.getItem("mealOrder"); + if (saved) { + const parsed = JSON.parse(saved); + if (Array.isArray(parsed) && parsed.every((c: string) => MEAL_TYPES.some(m => m.code === c))) { + return parsed; + } + } + } catch {} + return MEAL_TYPES.map(m => m.code); + }); + + // 按当前排序的餐食类型列表 + const orderedMealTypes = useMemo(() => + mealOrder.map(code => MEAL_TYPES.find(m => m.code === code)!) + , [mealOrder]); + + // 持久化排序顺序 + useEffect(() => { + localStorage.setItem("mealOrder", JSON.stringify(mealOrder)); + }, [mealOrder]); + + // 拖拽排序状态 + const [dragIndex, setDragIndex] = useState(null); + const [dropIndex, setDropIndex] = useState(null); + + const handleMealDragStart = (index: number) => { + setDragIndex(index); + }; + + const handleMealDragOver = (e: React.DragEvent, index: number) => { + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + if (index !== dragIndex) { + setDropIndex(index); + } + }; + + const handleMealDragLeave = () => { + setDropIndex(null); + }; + + const handleMealDrop = (index: number) => { + if (dragIndex === null || dragIndex === index) { + setDragIndex(null); + setDropIndex(null); + return; + } + const newOrder = [...mealOrder]; + const [moved] = newOrder.splice(dragIndex, 1); + newOrder.splice(index, 0, moved); + setMealOrder(newOrder); + setDragIndex(null); + setDropIndex(null); + }; + + const handleMealDragEnd = () => { + setDragIndex(null); + setDropIndex(null); + }; + // 加载导出配置 useEffect(() => { fetch("/api/config") @@ -231,6 +294,11 @@ export default function RecipeTranslationPage() { }; const {untranslatedColor, orientation, secondPageHeader} = exportConfig; + // 使用当前拖拽排序后的餐食类型顺序 + const exportMealTypes = mealOrder.map( + code => MEAL_TYPES.find(m => m.code === code)! + ); + // 2. 收集所有需要翻译的内容(星期、餐食类型、菜品) const allNames: string[] = []; @@ -241,9 +309,16 @@ export default function RecipeTranslationPage() { } } - // 收集餐食类型名称 - for (const meal of MEAL_TYPES) { + // 收集餐食类型名称(按当前排序) + for (const meal of exportMealTypes) { allNames.push(meal.label); + // 复合标签(如"早点/水果餐")拆分添加各部分,确保能被翻译 + if (meal.label.includes("/")) { + for (const part of meal.label.split("/")) { + const trimmed = part.trim(); + if (trimmed) allNames.push(trimmed); + } + } } // 收集所有菜品名称(去重) @@ -273,7 +348,15 @@ export default function RecipeTranslationPage() { const translations = result.translations as Record; // 3. 构建翻译映射函数 - const t = (zh: string): string => translations[zh] || ""; + const t = (zh: string): string => { + const cached = translations[zh]; + if (cached) return cached; + // 复合标签拆分翻译:如 "早点/水果餐" → 分别翻译各部分再组合 + if (zh.includes("/")) { + return zh.split("/").map(p => translations[p.trim()] || p.trim()).join(" / "); + } + return ""; + }; // 4. 构建表格数据 - 生成所有数据行 const dayRows: TableRow[] = []; @@ -295,7 +378,7 @@ export default function RecipeTranslationPage() { }), ]; - for (const mealType of MEAL_TYPES) { + for (const mealType of exportMealTypes) { const mealData = data.meals.find( (m) => m.dayCode === day.code && m.mealCode === mealType.code ); @@ -356,7 +439,7 @@ export default function RecipeTranslationPage() { width: {size: 12, type: WidthType.PERCENTAGE}, }), ); - for (const mealType of MEAL_TYPES) { + for (const mealType of exportMealTypes) { const zhText = mealType.label; const enText = t(zhText); headerCells.push( @@ -435,7 +518,7 @@ export default function RecipeTranslationPage() { console.error("Export error:", err); alert("导出失败,请重试"); } - }, [data, fileName]); + }, [data, fileName, mealOrder]); const handleFileUpload = useCallback( async (file: File) => { @@ -597,7 +680,7 @@ export default function RecipeTranslationPage() { {/* 餐食类型筛选 + 操作按钮 */}
- {/* 餐食类型筛选 */} + {/* 餐食类型筛选(拖拽排序) */}
- {MEAL_TYPES.map((meal) => { + {orderedMealTypes.map((meal, index) => { const Icon = meal.icon; + const isDragging = dragIndex === index; + const isDropTarget = dropIndex === index && dragIndex !== null && dragIndex !== index; return (