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/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; diff --git a/frontend/src/components/RecipeDetailOverlay.vue b/frontend/src/components/RecipeDetailOverlay.vue index d5c9537..d392ac7 100644 --- a/frontend/src/components/RecipeDetailOverlay.vue +++ b/frontend/src/components/RecipeDetailOverlay.vue @@ -36,6 +36,17 @@ >English + +
+ +
+
@@ -382,6 +393,7 @@ const viewMode = ref('card') const cardRef = ref(null) const cardImageUrl = ref(null) const cardLang = ref('zh') +const selectedCardVolume = ref('单次') const showTranslationEditor = ref(false) const customRecipeNameEn = ref('') const customOilNameEn = ref({}) @@ -547,9 +559,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()) } @@ -564,7 +587,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 } @@ -669,7 +692,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) => ({