From 2088019ed74d9155a5f01de20b806c913b272ff4 Mon Sep 17 00:00:00 2001 From: Hera Zhao Date: Tue, 7 Apr 2026 21:59:41 +0000 Subject: [PATCH 1/3] Fix overlay: always show buttons, restrict translation, fix save data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Always show favorite/save-to-diary buttons (login check on click) - Restrict translation editor to senior_editor/admin only (canManage) - Fix save: map ingredient oil→oil_name for API, reload recipes after - Ensures next open shows the saved data Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/components/RecipeDetailOverlay.vue | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/RecipeDetailOverlay.vue b/frontend/src/components/RecipeDetailOverlay.vue index 897f16d..2495607 100644 --- a/frontend/src/components/RecipeDetailOverlay.vue +++ b/frontend/src/components/RecipeDetailOverlay.vue @@ -5,7 +5,7 @@
-
+
@@ -122,7 +122,7 @@ @@ -787,9 +787,10 @@ async function saveRecipe() { name: editName.value.trim(), note: editNote.value.trim(), tags: editTags.value, - ingredients, + ingredients: ingredients.map(i => ({ oil_name: i.oil, drops: i.drops })), } await recipesStore.saveRecipe(payload) + await recipesStore.loadRecipes() ui.showToast('保存成功') emit('close') } catch (e) { From 6804835e85e4c0686c5f8d3e2e17b88379ca660c Mon Sep 17 00:00:00 2001 From: Hera Zhao Date: Tue, 7 Apr 2026 22:08:05 +0000 Subject: [PATCH 2/3] Persist recipe English name translation to database - Add en_name column to recipes table (migration in database.py) - Include en_name in recipe API responses and RecipeUpdate model - Save en_name when admin/senior_editor applies translation - Load en_name on overlay open, so translation persists across sessions Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/database.py | 2 ++ backend/main.py | 18 +++++++++++------- .../src/components/RecipeDetailOverlay.vue | 19 +++++++++++++++---- frontend/src/stores/recipes.js | 1 + 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/backend/database.py b/backend/database.py index fb6d174..b62f382 100644 --- a/backend/database.py +++ b/backend/database.py @@ -238,6 +238,8 @@ def init_db(): c.execute("ALTER TABLE recipes ADD COLUMN owner_id INTEGER") if "updated_by" not in cols: c.execute("ALTER TABLE recipes ADD COLUMN updated_by INTEGER") + if "en_name" not in cols: + c.execute("ALTER TABLE recipes ADD COLUMN en_name TEXT DEFAULT ''") # Seed admin user if no users exist count = c.execute("SELECT COUNT(*) FROM users").fetchone()[0] diff --git a/backend/main.py b/backend/main.py index b213cee..0931c4c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -95,6 +95,7 @@ class RecipeIn(BaseModel): class RecipeUpdate(BaseModel): name: Optional[str] = None + en_name: Optional[str] = None note: Optional[str] = None ingredients: Optional[list[IngredientIn]] = None tags: Optional[list[str]] = None @@ -308,7 +309,7 @@ def symptom_search(body: dict, user=Depends(get_current_user)): conn = get_db() # Search in recipe names rows = conn.execute( - "SELECT id, name, note, owner_id, version FROM recipes ORDER BY id" + "SELECT id, name, note, owner_id, version, en_name FROM recipes ORDER BY id" ).fetchall() exact = [] related = [] @@ -696,6 +697,7 @@ def _recipe_to_dict(conn, row): return { "id": rid, "name": row["name"], + "en_name": row["en_name"] if "en_name" in row.keys() else "", "note": row["note"], "owner_id": row["owner_id"], "owner_name": (owner["display_name"] or owner["username"]) if owner else None, @@ -710,19 +712,19 @@ def list_recipes(user=Depends(get_current_user)): conn = get_db() # Admin sees all; others see admin-owned (adopted) + their own if user["role"] == "admin": - rows = conn.execute("SELECT id, name, note, owner_id, version FROM recipes ORDER BY id").fetchall() + rows = conn.execute("SELECT id, name, note, owner_id, version, en_name FROM recipes ORDER BY id").fetchall() else: admin = conn.execute("SELECT id FROM users WHERE role = 'admin' LIMIT 1").fetchone() admin_id = admin["id"] if admin else 1 user_id = user.get("id") if user_id: rows = conn.execute( - "SELECT id, name, note, owner_id, version FROM recipes WHERE owner_id = ? OR owner_id = ? ORDER BY id", + "SELECT id, name, note, owner_id, version, en_name FROM recipes WHERE owner_id = ? OR owner_id = ? ORDER BY id", (admin_id, user_id) ).fetchall() else: rows = conn.execute( - "SELECT id, name, note, owner_id, version FROM recipes WHERE owner_id = ? ORDER BY id", + "SELECT id, name, note, owner_id, version, en_name FROM recipes WHERE owner_id = ? ORDER BY id", (admin_id,) ).fetchall() result = [_recipe_to_dict(conn, r) for r in rows] @@ -733,7 +735,7 @@ def list_recipes(user=Depends(get_current_user)): @app.get("/api/recipes/{recipe_id}") def get_recipe(recipe_id: int): conn = get_db() - row = conn.execute("SELECT id, name, note, owner_id, version FROM recipes WHERE id = ?", (recipe_id,)).fetchone() + row = conn.execute("SELECT id, name, note, owner_id, version, en_name FROM recipes WHERE id = ?", (recipe_id,)).fetchone() if not row: conn.close() raise HTTPException(404, "Recipe not found") @@ -804,6 +806,8 @@ def update_recipe(recipe_id: int, update: RecipeUpdate, user=Depends(get_current c.execute("UPDATE recipes SET name = ? WHERE id = ?", (update.name, recipe_id)) if update.note is not None: c.execute("UPDATE recipes SET note = ? WHERE id = ?", (update.note, recipe_id)) + if update.en_name is not None: + c.execute("UPDATE recipes SET en_name = ? WHERE id = ?", (update.en_name, recipe_id)) if update.ingredients is not None: c.execute("DELETE FROM recipe_ingredients WHERE recipe_id = ?", (recipe_id,)) for ing in update.ingredients: @@ -833,7 +837,7 @@ def delete_recipe(recipe_id: int, user=Depends(get_current_user)): conn = get_db() row = _check_recipe_permission(conn, recipe_id, user) # Save full snapshot for undo - full = conn.execute("SELECT id, name, note, owner_id, version FROM recipes WHERE id = ?", (recipe_id,)).fetchone() + full = conn.execute("SELECT id, name, note, owner_id, version, en_name FROM recipes WHERE id = ?", (recipe_id,)).fetchone() snapshot = _recipe_to_dict(conn, full) log_audit(conn, user["id"], "delete_recipe", "recipe", recipe_id, row["name"], json.dumps(snapshot, ensure_ascii=False)) @@ -1336,7 +1340,7 @@ def recipes_by_inventory(user=Depends(get_current_user)): if not inv: conn.close() return [] - rows = conn.execute("SELECT id, name, note, owner_id, version FROM recipes ORDER BY id").fetchall() + rows = conn.execute("SELECT id, name, note, owner_id, version, en_name FROM recipes ORDER BY id").fetchall() result = [] for r in rows: recipe = _recipe_to_dict(conn, r) diff --git a/frontend/src/components/RecipeDetailOverlay.vue b/frontend/src/components/RecipeDetailOverlay.vue index 2495607..d2a6027 100644 --- a/frontend/src/components/RecipeDetailOverlay.vue +++ b/frontend/src/components/RecipeDetailOverlay.vue @@ -518,9 +518,20 @@ function copyText() { }) } -function applyTranslation() { - // translations stored in customOilNameEn / customRecipeNameEn +async function applyTranslation() { showTranslationEditor.value = false + // Persist en_name to backend + if (recipe.value._id && customRecipeNameEn.value) { + try { + await api.put(`/api/recipes/${recipe.value._id}`, { + en_name: customRecipeNameEn.value, + version: recipe.value._version, + }) + ui.showToast('翻译已保存') + } catch (e) { + ui.showToast('翻译保存失败') + } + } cardImageUrl.value = null nextTick(() => generateCardImage()) } @@ -535,7 +546,7 @@ function getCardOilName(name) { function getCardRecipeName() { if (cardLang.value === 'en') { - return customRecipeNameEn.value || recipeNameEn(recipe.value.name) + return customRecipeNameEn.value || recipe.value.en_name || recipeNameEn(recipe.value.name) } return recipe.value.name } @@ -640,7 +651,7 @@ onMounted(() => { editTags.value = [...(r.tags || [])] editIngredients.value = (r.ingredients || []).map(i => ({ oil: i.oil, drops: i.drops })) // Init translation defaults - customRecipeNameEn.value = recipeNameEn(r.name) + customRecipeNameEn.value = r.en_name || recipeNameEn(r.name) const enMap = {} ;(r.ingredients || []).forEach(ing => { enMap[ing.oil] = oilEn(ing.oil) || ing.oil diff --git a/frontend/src/stores/recipes.js b/frontend/src/stores/recipes.js index 0f494aa..a4ce180 100644 --- a/frontend/src/stores/recipes.js +++ b/frontend/src/stores/recipes.js @@ -16,6 +16,7 @@ export const useRecipesStore = defineStore('recipes', () => { _owner_name: r._owner_name ?? r.owner_name ?? '', _version: r._version ?? r.version ?? 1, name: r.name, + en_name: r.en_name ?? '', note: r.note ?? '', tags: r.tags ?? [], ingredients: (r.ingredients ?? []).map((ing) => ({ From 846058fa0f44ea3e95bdb840d8b8dac2254a62c0 Mon Sep 17 00:00:00 2001 From: Hera Zhao Date: Tue, 7 Apr 2026 22:19:44 +0000 Subject: [PATCH 3/3] Raise LoginModal z-index above recipe overlay Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/components/LoginModal.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/LoginModal.vue b/frontend/src/components/LoginModal.vue index 84f7204..0a1b2b9 100644 --- a/frontend/src/components/LoginModal.vue +++ b/frontend/src/components/LoginModal.vue @@ -119,7 +119,7 @@ async function submit() { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.35); - z-index: 5000; + z-index: 6000; display: flex; align-items: center; justify-content: center;